diff --git a/src/traffic_top/CMakeLists.txt b/src/traffic_top/CMakeLists.txt index a15a9221727..aee497cbf40 100644 --- a/src/traffic_top/CMakeLists.txt +++ b/src/traffic_top/CMakeLists.txt @@ -15,9 +15,14 @@ # ####################### -add_executable(traffic_top traffic_top.cc ${CMAKE_SOURCE_DIR}/src/shared/rpc/IPCSocketClient.cc) -target_include_directories(traffic_top PRIVATE ${CURSES_INCLUDE_DIRS}) -target_link_libraries(traffic_top PRIVATE ts::tscore ts::inkevent libswoc::libswoc ${CURSES_LIBRARIES}) +add_executable( + traffic_top traffic_top.cc Stats.cc Display.cc Output.cc ${CMAKE_SOURCE_DIR}/src/shared/rpc/IPCSocketClient.cc +) + +target_include_directories(traffic_top PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(traffic_top PRIVATE ts::tscore ts::inkevent libswoc::libswoc) + install(TARGETS traffic_top) clang_tidy_check(traffic_top) diff --git a/src/traffic_top/Display.cc b/src/traffic_top/Display.cc new file mode 100644 index 00000000000..559c0a774b9 --- /dev/null +++ b/src/traffic_top/Display.cc @@ -0,0 +1,2127 @@ +/** @file + + Display class implementation for traffic_top using direct ANSI output. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include "Display.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace traffic_top +{ + +// ANSI escape sequences +namespace +{ + // Move cursor to row, col (1-based for ANSI) + void + moveTo(int row, int col) + { + printf("\033[%d;%dH", row + 1, col + 1); + } + + // Set foreground color + void + setColor(short colorIdx) + { + switch (colorIdx) { + case ColorPair::Red: + printf("\033[31m"); + break; + case ColorPair::Green: + printf("\033[32m"); + break; + case ColorPair::Yellow: + printf("\033[33m"); + break; + case ColorPair::Blue: + printf("\033[34m"); + break; + case ColorPair::Magenta: + case ColorPair::Border3: + printf("\033[35m"); + break; + case ColorPair::Cyan: + case ColorPair::Border: + printf("\033[36m"); + break; + case ColorPair::Grey: + case ColorPair::Dim: + printf("\033[90m"); + break; + case ColorPair::Border2: + printf("\033[34m"); + break; + case ColorPair::Border4: // Bright blue + printf("\033[94m"); + break; + case ColorPair::Border5: // Bright yellow + printf("\033[93m"); + break; + case ColorPair::Border6: // Bright red + printf("\033[91m"); + break; + case ColorPair::Border7: // Bright green + printf("\033[92m"); + break; + default: + printf("\033[0m"); + break; + } + } + + void + resetColor() + { + printf("\033[0m"); + } + + void + setBold() + { + printf("\033[1m"); + } + + void + clearScreen() + { + printf("\033[2J\033[H"); + } + + void + hideCursor() + { + printf("\033[?25l"); + } + + void + showCursor() + { + printf("\033[?25h"); + } + +} // anonymous namespace + +// Layout breakpoints for common terminal sizes: +// 80x24 - Classic VT100/xterm default (2 columns) +// 120x40 - Common larger terminal (3 columns) +// 160x50 - Wide terminal (4 columns) +// 300x75 - Extra large/tiled display (4 columns, wider boxes) +constexpr int WIDTH_MEDIUM = 120; // Larger terminal (minimum for 3-column layout) +constexpr int WIDTH_LARGE = 160; // Wide terminal (minimum for 4-column layout) + +constexpr int LABEL_WIDTH_SM = 12; // Small label width (80-col terminals) +constexpr int LABEL_WIDTH_MD = 14; // Medium label width (120-col terminals) +constexpr int LABEL_WIDTH_LG = 18; // Large label width (160+ terminals) + +Display::Display() = default; + +Display::~Display() +{ + if (_initialized) { + shutdown(); + } +} + +bool +Display::detectUtf8Support() +{ + const char *lang = getenv("LANG"); + const char *lc_all = getenv("LC_ALL"); + const char *lc_type = getenv("LC_CTYPE"); + + auto has_utf8 = [](const char *s) { + if (!s) { + return false; + } + // Check for UTF-8 or UTF8 (case-insensitive) + for (const char *p = s; *p; ++p) { + if ((*p == 'U' || *p == 'u') && (*(p + 1) == 'T' || *(p + 1) == 't') && (*(p + 2) == 'F' || *(p + 2) == 'f')) { + if (*(p + 3) == '-' && *(p + 4) == '8') { + return true; + } + if (*(p + 3) == '8') { + return true; + } + } + } + return false; + }; + + return has_utf8(lc_all) || has_utf8(lc_type) || has_utf8(lang); +} + +bool +Display::initialize() +{ + if (_initialized) { + return true; + } + + // Enable UTF-8 locale + setlocale(LC_ALL, ""); + + // Auto-detect UTF-8 support from environment + _ascii_mode = !detectUtf8Support(); + + // Save original terminal settings and configure raw mode + if (tcgetattr(STDIN_FILENO, &_orig_termios) == 0) { + _termios_saved = true; + + struct termios raw = _orig_termios; + // Disable canonical mode (line buffering) and echo + raw.c_lflag &= ~(ICANON | ECHO); + // Set minimum characters for read to 0 (non-blocking) + raw.c_cc[VMIN] = 0; + raw.c_cc[VTIME] = 0; + tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); + } + + // Get terminal size + struct winsize ws; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) { + _width = ws.ws_col; + _height = ws.ws_row; + } else { + _width = 80; + _height = 24; + } + + // Setup terminal for direct output + hideCursor(); + printf("\033[?1049h"); // Switch to alternate screen buffer + fflush(stdout); + + _initialized = true; + return true; +} + +void +Display::shutdown() +{ + if (_initialized) { + showCursor(); + printf("\033[?1049l"); // Switch back to normal screen buffer + resetColor(); + fflush(stdout); + + // Restore original terminal settings + if (_termios_saved) { + tcsetattr(STDIN_FILENO, TCSAFLUSH, &_orig_termios); + } + + _initialized = false; + } +} + +int +Display::getInput(int timeout_ms) +{ + // Use select() for timeout-based input + fd_set readfds; + struct timeval tv; + struct timeval *tv_ptr = nullptr; + + FD_ZERO(&readfds); + FD_SET(STDIN_FILENO, &readfds); + + if (timeout_ms >= 0) { + tv.tv_sec = timeout_ms / 1000; + tv.tv_usec = (timeout_ms % 1000) * 1000; + tv_ptr = &tv; + } + + int result = select(STDIN_FILENO + 1, &readfds, nullptr, nullptr, tv_ptr); + if (result <= 0) { + return KEY_NONE; // Timeout or error + } + + // Read the character + unsigned char c; + if (read(STDIN_FILENO, &c, 1) != 1) { + return KEY_NONE; + } + + // Check for escape sequence (arrow keys, etc.) + if (c == 0x1B) { // ESC + // Check if more characters are available (escape sequence) + FD_ZERO(&readfds); + FD_SET(STDIN_FILENO, &readfds); + tv.tv_sec = 0; + tv.tv_usec = 50000; // 50ms timeout to detect escape sequences + + if (select(STDIN_FILENO + 1, &readfds, nullptr, nullptr, &tv) > 0) { + unsigned char seq[2]; + if (read(STDIN_FILENO, &seq[0], 1) == 1 && seq[0] == '[') { + if (read(STDIN_FILENO, &seq[1], 1) == 1) { + switch (seq[1]) { + case 'A': + return KEY_UP; + case 'B': + return KEY_DOWN; + case 'C': + return KEY_RIGHT; + case 'D': + return KEY_LEFT; + } + } + } + } + // Just ESC key pressed (no sequence) + return 0x1B; + } + + return c; +} + +void +Display::getTerminalSize(int &width, int &height) const +{ + width = _width; + height = _height; +} + +void +Display::render(Stats &stats, Page page, [[maybe_unused]] bool absolute) +{ + // Update terminal size + struct winsize ws; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) { + _width = ws.ws_col; + _height = ws.ws_row; + } + + clearScreen(); + + switch (page) { + case Page::Main: + renderMainPage(stats); + break; + case Page::Response: + renderResponsePage(stats); + break; + case Page::Connection: + renderConnectionPage(stats); + break; + case Page::Cache: + renderCachePage(stats); + break; + case Page::SSL: + renderSSLPage(stats); + break; + case Page::Errors: + renderErrorsPage(stats); + break; + case Page::Performance: + renderPerformancePage(stats); + break; + case Page::Graphs: + renderGraphsPage(stats); + break; + case Page::Help: { + std::string version; + stats.getStat("version", version); + renderHelpPage(stats.getHost(), version); + break; + } + default: + break; + } + + fflush(stdout); +} + +void +Display::drawBox(int x, int y, int width, int height, const std::string &title, short colorIdx) +{ + setColor(colorIdx); + + // Top border with rounded corners + moveTo(y, x); + printf("%s", boxChar(BoxChars::TopLeft, BoxChars::AsciiTopLeft)); + for (int i = 1; i < width - 1; ++i) { + printf("%s", boxChar(BoxChars::Horizontal, BoxChars::AsciiHorizontal)); + } + printf("%s", boxChar(BoxChars::TopRight, BoxChars::AsciiTopRight)); + + // Title centered in top border + if (!title.empty() && static_cast(title.length()) < width - 4) { + int title_x = x + (width - static_cast(title.length()) - 2) / 2; + moveTo(y, title_x); + setBold(); + printf(" %s ", title.c_str()); + resetColor(); + setColor(colorIdx); + } + + // Sides + for (int i = 1; i < height - 1; ++i) { + moveTo(y + i, x); + printf("%s", boxChar(BoxChars::Vertical, BoxChars::AsciiVertical)); + moveTo(y + i, x + width - 1); + printf("%s", boxChar(BoxChars::Vertical, BoxChars::AsciiVertical)); + } + + // Bottom border with rounded corners + moveTo(y + height - 1, x); + printf("%s", boxChar(BoxChars::BottomLeft, BoxChars::AsciiBottomLeft)); + for (int i = 1; i < width - 1; ++i) { + printf("%s", boxChar(BoxChars::Horizontal, BoxChars::AsciiHorizontal)); + } + printf("%s", boxChar(BoxChars::BottomRight, BoxChars::AsciiBottomRight)); + + resetColor(); +} + +void +Display::drawSectionHeader(int y, int x1, int x2, const std::string &title) +{ + setColor(ColorPair::Border); + + // Draw top border line + moveTo(y, x1); + printf("%s", boxChar(BoxChars::TopLeft, BoxChars::AsciiTopLeft)); + for (int x = x1 + 1; x < x2 - 1; ++x) { + printf("%s", boxChar(BoxChars::Horizontal, BoxChars::AsciiHorizontal)); + } + if (x2 < _width) { + printf("%s", boxChar(BoxChars::TopRight, BoxChars::AsciiTopRight)); + } + + // Center the title + int title_len = static_cast(title.length()); + int title_x = x1 + (x2 - x1 - title_len - 2) / 2; + moveTo(y, title_x); + setBold(); + printf(" %s ", title.c_str()); + resetColor(); +} + +void +Display::drawStatTable(int x, int y, const std::vector &items, Stats &stats, int labelWidth) +{ + int row = y; + for (const auto &key : items) { + if (row >= _height - 2) { + break; // Don't overflow into status bar + } + + std::string prettyName; + double value = 0; + StatType type; + + stats.getStat(key, value, prettyName, type); + + // Truncate label if needed + if (static_cast(prettyName.length()) > labelWidth) { + prettyName = prettyName.substr(0, labelWidth - 1); + } + + // Draw label with cyan color for visual hierarchy + moveTo(row, x); + setColor(ColorPair::Cyan); + printf("%-*s", labelWidth, prettyName.c_str()); + resetColor(); + + printStatValue(x + labelWidth, row, value, type); + ++row; + } +} + +void +Display::drawStatGrid(int x, int y, int boxWidth, const std::vector &items, Stats &stats, int cols) +{ + // Calculate column width based on box width and number of columns + // Each stat needs: label (8 chars) + value (6 chars) + space (1 char) = 15 chars minimum + int colWidth = (boxWidth - 2) / cols; // -2 for box borders + int labelWidth = 8; + + int row = y; + int col = 0; + + for (const auto &key : items) { + if (row >= _height - 2) { + break; + } + + std::string prettyName; + double value = 0; + StatType type; + + stats.getStat(key, value, prettyName, type); + + // Truncate label if needed + if (static_cast(prettyName.length()) > labelWidth) { + prettyName = prettyName.substr(0, labelWidth); + } + + int statX = x + (col * colWidth); + + // Draw label with trailing space + moveTo(row, statX); + setColor(ColorPair::Cyan); + printf("%-*s ", labelWidth, prettyName.c_str()); // Note the space after %s + resetColor(); + + // Draw value (compact format for grid) + char buffer[16]; + char suffix = ' '; + double display = value; + short color = ColorPair::Green; + + if (isPercentage(type)) { + if (value < 0.01) { + color = ColorPair::Grey; + } + snprintf(buffer, sizeof(buffer), "%3.0f%%", display); + } else { + if (value > 1000000000.0) { + display = value / 1000000000.0; + suffix = 'G'; + color = ColorPair::Red; + } else if (value > 1000000.0) { + display = value / 1000000.0; + suffix = 'M'; + color = ColorPair::Yellow; + } else if (value > 1000.0) { + display = value / 1000.0; + suffix = 'K'; + color = ColorPair::Cyan; + } else if (value < 0.01) { + color = ColorPair::Grey; + } + snprintf(buffer, sizeof(buffer), "%5.0f%c", display, suffix); + } + + setColor(color); + setBold(); + printf("%s", buffer); + resetColor(); + + ++col; + if (col >= cols) { + col = 0; + ++row; + } + } +} + +void +Display::printStatValue(int x, int y, double value, StatType type) +{ + char buffer[32]; + char suffix = ' '; + double display = value; + short color = ColorPair::Green; + bool show_pct = isPercentage(type); + + if (!show_pct) { + // Format large numbers with SI prefixes + if (value > 1000000000000.0) { + display = value / 1000000000000.0; + suffix = 'T'; + color = ColorPair::Red; + } else if (value > 1000000000.0) { + display = value / 1000000000.0; + suffix = 'G'; + color = ColorPair::Red; + } else if (value > 1000000.0) { + display = value / 1000000.0; + suffix = 'M'; + color = ColorPair::Yellow; + } else if (value > 1000.0) { + display = value / 1000.0; + suffix = 'K'; + color = ColorPair::Cyan; + } else if (value < 0.01) { + color = ColorPair::Grey; + } + snprintf(buffer, sizeof(buffer), "%7.1f%c", display, suffix); + } else { + // Percentage display with color coding based on context + if (value > 90) { + color = ColorPair::Green; + } else if (value > 70) { + color = ColorPair::Cyan; + } else if (value > 50) { + color = ColorPair::Yellow; + } else if (value > 20) { + color = ColorPair::Yellow; + } else if (value < 0.01) { + color = ColorPair::Grey; + } else { + color = ColorPair::Green; + } + snprintf(buffer, sizeof(buffer), "%6.1f%%", display); + } + + moveTo(y, x); + setColor(color); + setBold(); + printf("%s", buffer); + resetColor(); +} + +void +Display::drawProgressBar(int x, int y, double percent, int width) +{ + // Clamp percentage + if (percent < 0) + percent = 0; + if (percent > 100) + percent = 100; + + int filled = static_cast((percent / 100.0) * width); + + // Choose color based on percentage + short color; + if (percent > 90) { + color = ColorPair::Red; + } else if (percent > 70) { + color = ColorPair::Yellow; + } else if (percent > 50) { + color = ColorPair::Cyan; + } else if (percent < 0.01) { + color = ColorPair::Grey; + } else { + color = ColorPair::Green; + } + + moveTo(y, x); + setColor(color); + for (int i = 0; i < filled; ++i) { + printf("#"); + } + + // Draw empty portion + setColor(ColorPair::Grey); + for (int i = filled; i < width; ++i) { + printf("-"); + } + resetColor(); +} + +void +Display::drawGraphLine(int x, int y, const std::vector &data, int width, bool colored) +{ + moveTo(y, x); + + // Take the last 'width' data points, or pad with zeros at the start + size_t start = 0; + if (data.size() > static_cast(width)) { + start = data.size() - width; + } + + int drawn = 0; + + // Pad with empty blocks if data is shorter than width + int padding = width - static_cast(data.size() - start); + for (int i = 0; i < padding; ++i) { + if (_ascii_mode) { + printf("%c", GraphChars::AsciiBlocks[0]); + } else { + printf("%s", GraphChars::Blocks[0]); + } + ++drawn; + } + + // Draw the actual data + for (size_t i = start; i < data.size() && drawn < width; ++i) { + double val = data[i]; + if (val < 0.0) + val = 0.0; + if (val > 1.0) + val = 1.0; + + // Map value to block index (0-8) + int blockIdx = static_cast(val * 8.0); + if (blockIdx > 8) + blockIdx = 8; + + // Color based on value (gradient: blue -> cyan -> green -> yellow -> red) + if (colored) { + if (val < 0.2) { + setColor(ColorPair::Blue); + } else if (val < 0.4) { + setColor(ColorPair::Cyan); + } else if (val < 0.6) { + setColor(ColorPair::Green); + } else if (val < 0.8) { + setColor(ColorPair::Yellow); + } else { + setColor(ColorPair::Red); + } + } + + if (_ascii_mode) { + printf("%c", GraphChars::AsciiBlocks[blockIdx]); + } else { + printf("%s", GraphChars::Blocks[blockIdx]); + } + ++drawn; + } + + if (colored) { + resetColor(); + } +} + +void +Display::drawMultiGraphBox(int x, int y, int width, + const std::vector, std::string>> &graphs, + const std::string &title) +{ + int height = static_cast(graphs.size()) + 2; // +2 for top/bottom borders + + // Draw box + if (title.empty()) { + // Simple separator + moveTo(y, x); + setColor(ColorPair::Border); + printf("%s", boxChar(BoxChars::TopLeft, BoxChars::AsciiTopLeft)); + for (int i = 1; i < width - 1; ++i) { + printf("%s", boxChar(BoxChars::Horizontal, BoxChars::AsciiHorizontal)); + } + printf("%s", boxChar(BoxChars::TopRight, BoxChars::AsciiTopRight)); + resetColor(); + } else { + drawBox(x, y, width, height, title, ColorPair::Border); + } + + // Draw each graph row + int contentWidth = width - 4; // -2 for borders, -2 for padding + int labelWidth = 12; // Fixed label width + int valueWidth = 10; // Fixed value width + int graphWidth = contentWidth - labelWidth - valueWidth - 1; // -1 for space after label + + int row = y + 1; + for (const auto &[label, data, value] : graphs) { + if (row >= y + height - 1) { + break; + } + + // Position and draw border + moveTo(row, x); + setColor(ColorPair::Border); + printf("%s", boxChar(BoxChars::Vertical, BoxChars::AsciiVertical)); + resetColor(); + + // Draw label (cyan) + printf(" "); + setColor(ColorPair::Cyan); + std::string truncLabel = label.substr(0, labelWidth); + printf("%-*s", labelWidth, truncLabel.c_str()); + resetColor(); + + // Draw graph + printf(" "); + drawGraphLine(x + 2 + labelWidth + 1, row, data, graphWidth, true); + + // Draw value (right-aligned) + moveTo(row, x + width - valueWidth - 2); + setColor(ColorPair::Green); + setBold(); + printf("%*s", valueWidth, value.c_str()); + resetColor(); + + // Right border + moveTo(row, x + width - 1); + setColor(ColorPair::Border); + printf("%s", boxChar(BoxChars::Vertical, BoxChars::AsciiVertical)); + resetColor(); + + ++row; + } + + // Bottom border (if no title, we need to draw it) + if (title.empty()) { + moveTo(y + height - 1, x); + setColor(ColorPair::Border); + printf("%s", boxChar(BoxChars::BottomLeft, BoxChars::AsciiBottomLeft)); + for (int i = 1; i < width - 1; ++i) { + printf("%s", boxChar(BoxChars::Horizontal, BoxChars::AsciiHorizontal)); + } + printf("%s", boxChar(BoxChars::BottomRight, BoxChars::AsciiBottomRight)); + resetColor(); + } +} + +void +Display::drawStatusBar(const std::string &host, Page page, bool absolute, bool connected) +{ + int status_y = _height - 1; + + // Fill status bar with blue background + moveTo(status_y, 0); + printf("\033[44m\033[97m"); // Blue background, bright white text + for (int x = 0; x < _width; ++x) { + printf(" "); + } + + // Time with icon - cyan colored + time_t now = time(nullptr); + struct tm nowtm; + char timeBuf[32]; + localtime_r(&now, &nowtm); + strftime(timeBuf, sizeof(timeBuf), "%H:%M:%S", &nowtm); + + moveTo(status_y, 1); + printf("\033[96m"); // Bright cyan + if (!_ascii_mode) { + printf("⏱ %s", timeBuf); + } else { + printf("%s", timeBuf); + } + + // Host with connection status indicator + std::string hostDisplay; + moveTo(status_y, 12); + if (connected) { + if (!_ascii_mode) { + hostDisplay = "● " + host; + } else { + hostDisplay = "[OK] " + host; + } + printf("\033[92m"); // Bright green + } else { + if (!_ascii_mode) { + hostDisplay = "○ connecting..."; + } else { + hostDisplay = "[..] connecting..."; + } + printf("\033[93m"); // Bright yellow + } + if (hostDisplay.length() > 25) { + hostDisplay = hostDisplay.substr(0, 22) + "..."; + } + printf("%-25s", hostDisplay.c_str()); + + // Page indicator - bright white + printf("\033[97m"); // Bright white + int pageNum = static_cast(page) + 1; + int total = getPageCount(); + moveTo(status_y, 40); + printf("[%d/%d] ", pageNum, total); + printf("\033[93m%s", getPageName(page)); // Yellow page name + + // Mode indicator - show ABS or RATE clearly + moveTo(status_y, 60); + if (absolute) { + printf("\033[30m\033[43m ABS \033[0m\033[44m"); // Black on yellow background + } else { + printf("\033[30m\033[42m RATE \033[0m\033[44m"); // Black on green background + } + + // Key hints (right-aligned) - dimmer color + printf("\033[37m"); // Normal white (dimmer) + std::string hints; + if (_width > 110) { + hints = absolute ? "q:Quit h:Help 1-8:Pages a:Rate" : "q:Quit h:Help 1-8:Pages a:Abs"; + } else if (_width > 80) { + hints = "q h 1-8 a"; + } else { + hints = "q h a"; + } + int hints_x = _width - static_cast(hints.length()) - 2; + if (hints_x > 68) { + moveTo(status_y, hints_x); + printf("%s", hints.c_str()); + } + + printf("\033[0m"); // Reset +} + +const char * +Display::getPageName(Page page) +{ + switch (page) { + case Page::Main: + return "Overview"; + case Page::Response: + return "Responses"; + case Page::Connection: + return "Connections"; + case Page::Cache: + return "Cache"; + case Page::SSL: + return "SSL/TLS"; + case Page::Errors: + return "Errors"; + case Page::Performance: + return "Performance"; + case Page::Graphs: + return "Graphs"; + case Page::Help: + return "Help"; + default: + return "Unknown"; + } +} + +void +Display::renderMainPage(Stats &stats) +{ + // Layout based on LAYOUT.md specifications: + // 80x24 - 2x2 grid of 40-char boxes (2 stat columns per box) + // 120x40 - 3 boxes per row x 5-6 rows + // 160x40 - 4 boxes per row x multiple rows + + if (_width >= WIDTH_LARGE) { + // 160x40: 4 boxes per row (40 chars each) + render160Layout(stats); + } else if (_width >= WIDTH_MEDIUM) { + // 120x40: 3 boxes per row (40 chars each) + render120Layout(stats); + } else { + // 80x24: 2 boxes per row (40 chars each) + render80Layout(stats); + } +} + +namespace +{ + // Format a stat value to a string with suffix (right-aligned number, suffix attached) + std::string + formatStatValue(double value, StatType type, int width = 5) + { + char buffer[32]; + char suffix = ' '; + double display = value; + + if (isPercentage(type)) { + // Format percentage (use rounding for accurate display) + snprintf(buffer, sizeof(buffer), "%*ld%%", width - 1, std::lround(display)); + } else { + // Format with SI suffix + if (value >= 1000000000000.0) { + display = value / 1000000000000.0; + suffix = 'T'; + } else if (value >= 1000000000.0) { + display = value / 1000000000.0; + suffix = 'G'; + } else if (value >= 1000000.0) { + display = value / 1000000.0; + suffix = 'M'; + } else if (value >= 1000.0) { + display = value / 1000.0; + suffix = 'K'; + } + + // Use rounding for accurate display (e.g., 1.9K displays as 2K, not 1K) + if (suffix != ' ') { + snprintf(buffer, sizeof(buffer), "%*ld%c", width - 1, std::lround(display), suffix); + } else { + snprintf(buffer, sizeof(buffer), "%*ld ", width - 1, std::lround(display)); + } + } + + return buffer; + } + + // Get color for a stat value + short + getStatColor(double value, StatType type) + { + if (value < 0.01) { + return ColorPair::Grey; + } + + if (isPercentage(type)) { + if (value > 90) + return ColorPair::Green; + if (value > 70) + return ColorPair::Cyan; + if (value > 50) + return ColorPair::Yellow; + return ColorPair::Green; + } + + // Color by magnitude + if (value >= 1000000000.0) + return ColorPair::Red; + if (value >= 1000000.0) + return ColorPair::Yellow; + if (value >= 1000.0) + return ColorPair::Cyan; + return ColorPair::Green; + } +} // anonymous namespace + +void +Display::drawStatPairRow(int x, int y, const std::string &key1, const std::string &key2, Stats &stats, short borderColor) +{ + // Format per LAYOUT.md: + // | Label1 Value1 Label2 Value2 | + // Total: 40 chars including borders + // Content: 38 chars = 1 space + stat1(17) + gap(3) + stat2(16) + 1 space + + constexpr int GAP_WIDTH = 3; + constexpr int LABEL1_W = 12; + constexpr int LABEL2_W = 11; + constexpr int VALUE_W = 5; + + moveTo(y, x); + setColor(borderColor); + printf("%s", boxChar(BoxChars::Vertical, BoxChars::AsciiVertical)); + resetColor(); + printf(" "); + + // First stat + std::string prettyName1; + double value1 = 0; + StatType type1; + stats.getStat(key1, value1, prettyName1, type1); + + // Truncate label if needed + if (prettyName1.length() > static_cast(LABEL1_W)) { + prettyName1 = prettyName1.substr(0, LABEL1_W); + } + + setColor(ColorPair::Cyan); + printf("%-*s", LABEL1_W, prettyName1.c_str()); + resetColor(); + + std::string valStr1 = formatStatValue(value1, type1, VALUE_W); + setColor(getStatColor(value1, type1)); + setBold(); + printf("%s", valStr1.c_str()); + resetColor(); + + // Gap + printf("%*s", GAP_WIDTH, ""); + + // Second stat + std::string prettyName2; + double value2 = 0; + StatType type2; + stats.getStat(key2, value2, prettyName2, type2); + + if (prettyName2.length() > static_cast(LABEL2_W)) { + prettyName2 = prettyName2.substr(0, LABEL2_W); + } + + setColor(ColorPair::Cyan); + printf("%-*s", LABEL2_W, prettyName2.c_str()); + resetColor(); + + std::string valStr2 = formatStatValue(value2, type2, VALUE_W); + setColor(getStatColor(value2, type2)); + setBold(); + printf("%s", valStr2.c_str()); + resetColor(); + + printf(" "); + setColor(borderColor); + printf("%s", boxChar(BoxChars::Vertical, BoxChars::AsciiVertical)); + resetColor(); +} + +void +Display::render80Layout(Stats &stats) +{ + // 80x24 Layout: + // 2x2 grid of 40-char boxes + // Top row: CLIENT | ORIGIN (9 content rows each) + // Bottom row: CACHE | REQS/RESPONSES (9 content rows each) + + constexpr int BOX_WIDTH = 40; + constexpr int TOP_HEIGHT = 11; // 9 content rows + 2 borders + constexpr int BOT_HEIGHT = 11; + int y2 = TOP_HEIGHT; // Start of second row (after first row ends) + + // Draw all four boxes + drawBox(0, 0, BOX_WIDTH, TOP_HEIGHT, "CLIENT", ColorPair::Border); + drawBox(BOX_WIDTH, 0, BOX_WIDTH, TOP_HEIGHT, "ORIGIN", ColorPair::Border4); + drawBox(0, y2, BOX_WIDTH, BOT_HEIGHT, "CACHE", ColorPair::Border7); + drawBox(BOX_WIDTH, y2, BOX_WIDTH, BOT_HEIGHT, "REQS/RESPONSES", ColorPair::Border5); + + // CLIENT box content (top left) - cyan border + drawStatPairRow(0, 1, "client_req", "client_conn", stats, ColorPair::Border); + drawStatPairRow(0, 2, "client_curr_conn", "client_actv_conn", stats, ColorPair::Border); + drawStatPairRow(0, 3, "client_req_conn", "client_dyn_ka", stats, ColorPair::Border); + drawStatPairRow(0, 4, "client_avg_size", "client_net", stats, ColorPair::Border); + drawStatPairRow(0, 5, "client_req_time", "client_head", stats, ColorPair::Border); + drawStatPairRow(0, 6, "client_body", "client_conn_h1", stats, ColorPair::Border); + drawStatPairRow(0, 7, "client_conn_h2", "ssl_curr_sessions", stats, ColorPair::Border); + drawStatPairRow(0, 8, "ssl_handshake_success", "ssl_error_ssl", stats, ColorPair::Border); + drawStatPairRow(0, 9, "fresh_time", "cold_time", stats, ColorPair::Border); + + // ORIGIN box content (top right) - bright blue border + drawStatPairRow(BOX_WIDTH, 1, "server_req", "server_conn", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, 2, "server_curr_conn", "server_req_conn", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, 3, "conn_fail", "abort", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, 4, "server_avg_size", "server_net", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, 5, "ka_total", "ka_count", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, 6, "server_head", "server_body", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, 7, "dns_lookups", "dns_hits", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, 8, "dns_ratio", "dns_entry", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, 9, "other_err", "t_conn_fail", stats, ColorPair::Border4); + + // CACHE box content (bottom left) - bright green border + drawStatPairRow(0, y2 + 1, "disk_used", "ram_used", stats, ColorPair::Border7); + drawStatPairRow(0, y2 + 2, "disk_total", "ram_total", stats, ColorPair::Border7); + drawStatPairRow(0, y2 + 3, "ram_ratio", "fresh", stats, ColorPair::Border7); + drawStatPairRow(0, y2 + 4, "reval", "cold", stats, ColorPair::Border7); + drawStatPairRow(0, y2 + 5, "changed", "not", stats, ColorPair::Border7); + drawStatPairRow(0, y2 + 6, "no", "entries", stats, ColorPair::Border7); + drawStatPairRow(0, y2 + 7, "lookups", "cache_writes", stats, ColorPair::Border7); + drawStatPairRow(0, y2 + 8, "read_active", "write_active", stats, ColorPair::Border7); + drawStatPairRow(0, y2 + 9, "cache_updates", "cache_deletes", stats, ColorPair::Border7); + + // REQS/RESPONSES box content (bottom right) - bright yellow border + drawStatPairRow(BOX_WIDTH, y2 + 1, "get", "post", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, y2 + 2, "head", "put", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, y2 + 3, "delete", "options", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, y2 + 4, "200", "206", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, y2 + 5, "301", "304", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, y2 + 6, "404", "502", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, y2 + 7, "2xx", "3xx", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, y2 + 8, "4xx", "5xx", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, y2 + 9, "503", "504", stats, ColorPair::Border5); +} + +void +Display::render120Layout(Stats &stats) +{ + // 120x40 Layout: 3 boxes per row (40 chars each) + // For 40 lines: 39 available (1 status bar) + // 4 rows of boxes that don't share borders + + constexpr int BOX_WIDTH = 40; + int available = _height - 1; // Leave room for status bar + + // Calculate box heights: divide available space among 4 rows + // For 40 lines: 39 / 4 = 9 with 3 left over + int base_height = available / 4; + int extra = available % 4; + int row1_height = base_height + (extra > 0 ? 1 : 0); + int row2_height = base_height + (extra > 1 ? 1 : 0); + int row3_height = base_height + (extra > 2 ? 1 : 0); + int row4_height = base_height; + + int row = 0; + + // Row 1: CACHE | REQUESTS | CONNECTIONS + // Consistent colors: CACHE=Green, REQUESTS=Yellow, CONNECTIONS=Blue + drawBox(0, row, BOX_WIDTH, row1_height, "CACHE", ColorPair::Border7); + drawBox(BOX_WIDTH, row, BOX_WIDTH, row1_height, "REQUESTS", ColorPair::Border5); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, row1_height, "CONNECTIONS", ColorPair::Border2); + + drawStatPairRow(0, row + 1, "disk_used", "disk_total", stats, ColorPair::Border7); + drawStatPairRow(0, row + 2, "ram_used", "ram_total", stats, ColorPair::Border7); + drawStatPairRow(0, row + 3, "entries", "avg_size", stats, ColorPair::Border7); + drawStatPairRow(0, row + 4, "lookups", "cache_writes", stats, ColorPair::Border7); + drawStatPairRow(0, row + 5, "read_active", "write_active", stats, ColorPair::Border7); + if (row1_height > 7) + drawStatPairRow(0, row + 6, "cache_updates", "cache_deletes", stats, ColorPair::Border7); + + drawStatPairRow(BOX_WIDTH, row + 1, "client_req", "server_req", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, row + 2, "get", "post", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, row + 3, "head", "put", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, row + 4, "delete", "options", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, row + 5, "100", "101", stats, ColorPair::Border5); + if (row1_height > 7) + drawStatPairRow(BOX_WIDTH, row + 6, "201", "204", stats, ColorPair::Border5); + + drawStatPairRow(BOX_WIDTH * 2, row + 1, "client_conn", "client_curr_conn", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "client_actv_conn", "server_conn", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "server_curr_conn", "server_req_conn", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "client_conn_h1", "client_conn_h2", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "h2_streams_total", "h2_streams_current", stats, ColorPair::Border2); + if (row1_height > 7) + drawStatPairRow(BOX_WIDTH * 2, row + 6, "net_open_conn", "net_throttled", stats, ColorPair::Border2); + + row += row1_height; + + // Row 2: HIT RATES | RESPONSES | BANDWIDTH + // Consistent colors: HIT RATES=Red, RESPONSES=Yellow, BANDWIDTH=Magenta + drawBox(0, row, BOX_WIDTH, row2_height, "HIT RATES", ColorPair::Border6); + drawBox(BOX_WIDTH, row, BOX_WIDTH, row2_height, "RESPONSES", ColorPair::Border5); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, row2_height, "BANDWIDTH", ColorPair::Border3); + + drawStatPairRow(0, row + 1, "ram_ratio", "fresh", stats, ColorPair::Border6); + drawStatPairRow(0, row + 2, "reval", "cold", stats, ColorPair::Border6); + drawStatPairRow(0, row + 3, "changed", "not", stats, ColorPair::Border6); + drawStatPairRow(0, row + 4, "no", "ram_hit", stats, ColorPair::Border6); + drawStatPairRow(0, row + 5, "ram_miss", "fresh_time", stats, ColorPair::Border6); + if (row2_height > 7) + drawStatPairRow(0, row + 6, "reval_time", "cold_time", stats, ColorPair::Border6); + + drawStatPairRow(BOX_WIDTH, row + 1, "200", "206", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, row + 2, "301", "304", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, row + 3, "404", "502", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, row + 4, "503", "504", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, row + 5, "2xx", "3xx", stats, ColorPair::Border5); + if (row2_height > 7) + drawStatPairRow(BOX_WIDTH, row + 6, "4xx", "5xx", stats, ColorPair::Border5); + + drawStatPairRow(BOX_WIDTH * 2, row + 1, "client_head", "client_body", stats, ColorPair::Border3); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "server_head", "server_body", stats, ColorPair::Border3); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "client_avg_size", "server_avg_size", stats, ColorPair::Border3); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "client_net", "server_net", stats, ColorPair::Border3); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "client_size", "server_size", stats, ColorPair::Border3); + if (row2_height > 7) + drawStatPairRow(BOX_WIDTH * 2, row + 6, "client_req_time", "total_time", stats, ColorPair::Border3); + + row += row2_height; + + // Row 3: SSL/TLS | DNS | ERRORS + // Consistent colors: SSL/TLS=Magenta, DNS=Cyan, ERRORS=Red + drawBox(0, row, BOX_WIDTH, row3_height, "SSL/TLS", ColorPair::Border3); + drawBox(BOX_WIDTH, row, BOX_WIDTH, row3_height, "DNS", ColorPair::Border); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, row3_height, "ERRORS", ColorPair::Border6); + + drawStatPairRow(0, row + 1, "ssl_success_in", "ssl_success_out", stats, ColorPair::Border3); + drawStatPairRow(0, row + 2, "ssl_session_hit", "ssl_session_miss", stats, ColorPair::Border3); + drawStatPairRow(0, row + 3, "tls_v12", "tls_v13", stats, ColorPair::Border3); + drawStatPairRow(0, row + 4, "ssl_client_bad_cert", "ssl_origin_bad_cert", stats, ColorPair::Border3); + drawStatPairRow(0, row + 5, "ssl_error_ssl", "ssl_error_syscall", stats, ColorPair::Border3); + if (row3_height > 7) + drawStatPairRow(0, row + 6, "ssl_attempts_in", "ssl_attempts_out", stats, ColorPair::Border3); + + drawStatPairRow(BOX_WIDTH, row + 1, "dns_lookups", "dns_hits", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH, row + 2, "dns_ratio", "dns_entry", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH, row + 3, "dns_serve_stale", "dns_in_flight", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH, row + 4, "dns_success", "dns_fail", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH, row + 5, "dns_lookup_time", "dns_success_time", stats, ColorPair::Border); + if (row3_height > 7) + drawStatPairRow(BOX_WIDTH, row + 6, "dns_total", "dns_retries", stats, ColorPair::Border); + + drawStatPairRow(BOX_WIDTH * 2, row + 1, "conn_fail", "abort", stats, ColorPair::Border6); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "client_abort", "other_err", stats, ColorPair::Border6); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "cache_read_errors", "cache_write_errors", stats, ColorPair::Border6); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "txn_aborts", "txn_other_errors", stats, ColorPair::Border6); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "h2_stream_errors", "h2_conn_errors", stats, ColorPair::Border6); + if (row3_height > 7) + drawStatPairRow(BOX_WIDTH * 2, row + 6, "err_client_read", "cache_lookup_fail", stats, ColorPair::Border6); + + row += row3_height; + + // Row 4: HTTP METHODS | RESPONSE TIMES | HTTP CODES + // These boxes show different stats from rows 1-3 + if (row + row4_height <= _height - 1) { + drawBox(0, row, BOX_WIDTH, row4_height, "HTTP METHODS", ColorPair::Border); + drawBox(BOX_WIDTH, row, BOX_WIDTH, row4_height, "RESPONSE TIMES", ColorPair::Border4); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, row4_height, "HTTP CODES", ColorPair::Border2); + + // HTTP Methods breakdown + drawStatPairRow(0, row + 1, "get", "post", stats, ColorPair::Border); + drawStatPairRow(0, row + 2, "head", "put", stats, ColorPair::Border); + drawStatPairRow(0, row + 3, "delete", "options", stats, ColorPair::Border); + drawStatPairRow(0, row + 4, "client_req", "server_req", stats, ColorPair::Border); + drawStatPairRow(0, row + 5, "client_req_conn", "server_req_conn", stats, ColorPair::Border); + if (row4_height > 7) + drawStatPairRow(0, row + 6, "client_dyn_ka", "client_req_time", stats, ColorPair::Border); + + // Response times for different cache states + drawStatPairRow(BOX_WIDTH, row + 1, "fresh_time", "reval_time", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, row + 2, "cold_time", "changed_time", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, row + 3, "not_time", "no_time", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, row + 4, "total_time", "client_req_time", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, row + 5, "ssl_handshake_time", "ka_total", stats, ColorPair::Border4); + if (row4_height > 7) + drawStatPairRow(BOX_WIDTH, row + 6, "ka_count", "ssl_origin_reused", stats, ColorPair::Border4); + + // Additional HTTP codes not shown elsewhere + drawStatPairRow(BOX_WIDTH * 2, row + 1, "100", "101", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "201", "204", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "302", "307", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "400", "401", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "403", "500", stats, ColorPair::Border2); + if (row4_height > 7) + drawStatPairRow(BOX_WIDTH * 2, row + 6, "501", "505", stats, ColorPair::Border2); + } +} + +void +Display::render160Layout(Stats &stats) +{ + // 160x40 Layout: 4 boxes per row (40 chars each) + // For 40 lines: 39 available (1 status bar) + // 4 rows of boxes that don't share borders + + constexpr int BOX_WIDTH = 40; + int available = _height - 1; // Leave room for status bar + + // Calculate box heights: divide available space among 4 rows + // For 40 lines: 39 / 4 = 9 with 3 left over + int base_height = available / 4; + int extra = available % 4; + int row1_height = base_height + (extra > 0 ? 1 : 0); + int row2_height = base_height + (extra > 1 ? 1 : 0); + int row3_height = base_height + (extra > 2 ? 1 : 0); + int row4_height = base_height; + + int row = 0; + + // Row 1: CACHE | CLIENT | ORIGIN | REQUESTS + // Consistent colors: CACHE=Green, CLIENT=Cyan, ORIGIN=Bright Blue, REQUESTS=Yellow + drawBox(0, row, BOX_WIDTH, row1_height, "CACHE", ColorPair::Border7); + drawBox(BOX_WIDTH, row, BOX_WIDTH, row1_height, "CLIENT", ColorPair::Border); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, row1_height, "ORIGIN", ColorPair::Border4); + drawBox(BOX_WIDTH * 3, row, BOX_WIDTH, row1_height, "REQUESTS", ColorPair::Border5); + + drawStatPairRow(0, row + 1, "disk_used", "disk_total", stats, ColorPair::Border7); + drawStatPairRow(0, row + 2, "ram_used", "ram_total", stats, ColorPair::Border7); + drawStatPairRow(0, row + 3, "entries", "avg_size", stats, ColorPair::Border7); + drawStatPairRow(0, row + 4, "lookups", "cache_writes", stats, ColorPair::Border7); + drawStatPairRow(0, row + 5, "read_active", "write_active", stats, ColorPair::Border7); + if (row1_height > 7) + drawStatPairRow(0, row + 6, "cache_updates", "cache_deletes", stats, ColorPair::Border7); + + drawStatPairRow(BOX_WIDTH, row + 1, "client_req", "client_conn", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH, row + 2, "client_curr_conn", "client_actv_conn", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH, row + 3, "client_req_conn", "client_dyn_ka", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH, row + 4, "client_avg_size", "client_net", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH, row + 5, "client_req_time", "client_head", stats, ColorPair::Border); + if (row1_height > 7) + drawStatPairRow(BOX_WIDTH, row + 6, "client_body", "conn_fail", stats, ColorPair::Border); + + drawStatPairRow(BOX_WIDTH * 2, row + 1, "server_req", "server_conn", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "server_curr_conn", "server_req_conn", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "conn_fail", "abort", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "server_avg_size", "server_net", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "ka_total", "ka_count", stats, ColorPair::Border4); + if (row1_height > 7) + drawStatPairRow(BOX_WIDTH * 2, row + 6, "server_head", "server_body", stats, ColorPair::Border4); + + drawStatPairRow(BOX_WIDTH * 3, row + 1, "get", "post", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 2, "head", "put", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 3, "delete", "options", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 4, "1xx", "2xx", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 5, "3xx", "4xx", stats, ColorPair::Border5); + if (row1_height > 7) + drawStatPairRow(BOX_WIDTH * 3, row + 6, "5xx", "client_req_conn", stats, ColorPair::Border5); + + row += row1_height; + + // Row 2: HIT RATES | CONNECTIONS | SSL/TLS | RESPONSES + // Consistent colors: HIT RATES=Red, CONNECTIONS=Blue, SSL/TLS=Magenta, RESPONSES=Yellow + drawBox(0, row, BOX_WIDTH, row2_height, "HIT RATES", ColorPair::Border6); + drawBox(BOX_WIDTH, row, BOX_WIDTH, row2_height, "CONNECTIONS", ColorPair::Border2); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, row2_height, "SSL/TLS", ColorPair::Border3); + drawBox(BOX_WIDTH * 3, row, BOX_WIDTH, row2_height, "RESPONSES", ColorPair::Border5); + + drawStatPairRow(0, row + 1, "ram_ratio", "fresh", stats, ColorPair::Border6); + drawStatPairRow(0, row + 2, "reval", "cold", stats, ColorPair::Border6); + drawStatPairRow(0, row + 3, "changed", "not", stats, ColorPair::Border6); + drawStatPairRow(0, row + 4, "no", "ram_hit", stats, ColorPair::Border6); + drawStatPairRow(0, row + 5, "ram_miss", "fresh_time", stats, ColorPair::Border6); + if (row2_height > 7) + drawStatPairRow(0, row + 6, "reval_time", "cold_time", stats, ColorPair::Border6); + + drawStatPairRow(BOX_WIDTH, row + 1, "client_conn_h1", "client_curr_conn_h1", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH, row + 2, "client_conn_h2", "client_curr_conn_h2", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH, row + 3, "h2_streams_total", "h2_streams_current", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH, row + 4, "client_actv_conn_h1", "client_actv_conn_h2", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH, row + 5, "net_throttled", "net_open_conn", stats, ColorPair::Border2); + if (row2_height > 7) + drawStatPairRow(BOX_WIDTH, row + 6, "client_dyn_ka", "ssl_curr_sessions", stats, ColorPair::Border2); + + drawStatPairRow(BOX_WIDTH * 2, row + 1, "ssl_success_in", "ssl_success_out", stats, ColorPair::Border3); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "ssl_session_hit", "ssl_session_miss", stats, ColorPair::Border3); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "tls_v12", "tls_v13", stats, ColorPair::Border3); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "ssl_client_bad_cert", "ssl_origin_bad_cert", stats, ColorPair::Border3); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "ssl_error_ssl", "ssl_error_syscall", stats, ColorPair::Border3); + if (row2_height > 7) + drawStatPairRow(BOX_WIDTH * 2, row + 6, "ssl_attempts_in", "ssl_attempts_out", stats, ColorPair::Border3); + + drawStatPairRow(BOX_WIDTH * 3, row + 1, "200", "201", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 2, "204", "206", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 3, "301", "302", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 4, "304", "307", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 5, "400", "404", stats, ColorPair::Border5); + if (row2_height > 7) + drawStatPairRow(BOX_WIDTH * 3, row + 6, "500", "502", stats, ColorPair::Border5); + + row += row2_height; + + // Row 3: BANDWIDTH | DNS | ERRORS | TOTALS + // Consistent colors: BANDWIDTH=Magenta, DNS=Cyan, ERRORS=Red, TOTALS=Blue + drawBox(0, row, BOX_WIDTH, row3_height, "BANDWIDTH", ColorPair::Border3); + drawBox(BOX_WIDTH, row, BOX_WIDTH, row3_height, "DNS", ColorPair::Border); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, row3_height, "ERRORS", ColorPair::Border6); + drawBox(BOX_WIDTH * 3, row, BOX_WIDTH, row3_height, "TOTALS", ColorPair::Border2); + + drawStatPairRow(0, row + 1, "client_head", "client_body", stats, ColorPair::Border3); + drawStatPairRow(0, row + 2, "server_head", "server_body", stats, ColorPair::Border3); + drawStatPairRow(0, row + 3, "client_avg_size", "server_avg_size", stats, ColorPair::Border3); + drawStatPairRow(0, row + 4, "client_net", "server_net", stats, ColorPair::Border3); + drawStatPairRow(0, row + 5, "client_size", "server_size", stats, ColorPair::Border3); + if (row3_height > 7) + drawStatPairRow(0, row + 6, "client_req_time", "total_time", stats, ColorPair::Border3); + + drawStatPairRow(BOX_WIDTH, row + 1, "dns_lookups", "dns_hits", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH, row + 2, "dns_ratio", "dns_entry", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH, row + 3, "dns_serve_stale", "dns_in_flight", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH, row + 4, "dns_success", "dns_fail", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH, row + 5, "dns_lookup_time", "dns_success_time", stats, ColorPair::Border); + if (row3_height > 7) + drawStatPairRow(BOX_WIDTH, row + 6, "dns_total", "dns_retries", stats, ColorPair::Border); + + drawStatPairRow(BOX_WIDTH * 2, row + 1, "conn_fail", "abort", stats, ColorPair::Border6); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "client_abort", "other_err", stats, ColorPair::Border6); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "cache_read_errors", "cache_write_errors", stats, ColorPair::Border6); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "txn_aborts", "txn_other_errors", stats, ColorPair::Border6); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "h2_stream_errors", "h2_conn_errors", stats, ColorPair::Border6); + if (row3_height > 7) + drawStatPairRow(BOX_WIDTH * 2, row + 6, "err_client_read", "cache_lookup_fail", stats, ColorPair::Border6); + + drawStatPairRow(BOX_WIDTH * 3, row + 1, "client_req", "server_req", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 3, row + 2, "client_conn", "server_conn", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 3, row + 3, "2xx", "3xx", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 3, row + 4, "4xx", "5xx", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 3, row + 5, "abort", "conn_fail", stats, ColorPair::Border2); + if (row3_height > 7) + drawStatPairRow(BOX_WIDTH * 3, row + 6, "other_err", "t_conn_fail", stats, ColorPair::Border2); + + row += row3_height; + + // Row 4: HTTP CODES | CACHE DETAIL | ORIGIN DETAIL | MISC STATS + // Consistent colors: HTTP CODES=Yellow, CACHE DETAIL=Green, ORIGIN DETAIL=Bright Blue, MISC=Cyan + if (row + row4_height <= _height - 1) { + drawBox(0, row, BOX_WIDTH, row4_height, "HTTP CODES", ColorPair::Border5); + drawBox(BOX_WIDTH, row, BOX_WIDTH, row4_height, "CACHE DETAIL", ColorPair::Border7); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, row4_height, "ORIGIN DETAIL", ColorPair::Border4); + drawBox(BOX_WIDTH * 3, row, BOX_WIDTH, row4_height, "MISC STATS", ColorPair::Border); + + drawStatPairRow(0, row + 1, "100", "101", stats, ColorPair::Border5); + drawStatPairRow(0, row + 2, "200", "201", stats, ColorPair::Border5); + drawStatPairRow(0, row + 3, "204", "206", stats, ColorPair::Border5); + drawStatPairRow(0, row + 4, "301", "302", stats, ColorPair::Border5); + drawStatPairRow(0, row + 5, "304", "307", stats, ColorPair::Border5); + if (row4_height > 7) + drawStatPairRow(0, row + 6, "400", "401", stats, ColorPair::Border5); + + drawStatPairRow(BOX_WIDTH, row + 1, "ram_hit", "ram_miss", stats, ColorPair::Border7); + drawStatPairRow(BOX_WIDTH, row + 2, "update_active", "cache_updates", stats, ColorPair::Border7); + drawStatPairRow(BOX_WIDTH, row + 3, "cache_deletes", "avg_size", stats, ColorPair::Border7); + drawStatPairRow(BOX_WIDTH, row + 4, "fresh", "reval", stats, ColorPair::Border7); + drawStatPairRow(BOX_WIDTH, row + 5, "cold", "changed", stats, ColorPair::Border7); + if (row4_height > 7) + drawStatPairRow(BOX_WIDTH, row + 6, "not", "no", stats, ColorPair::Border7); + + drawStatPairRow(BOX_WIDTH * 2, row + 1, "ssl_origin_reused", "ssl_origin_bad_cert", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "ssl_origin_expired", "ssl_origin_revoked", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "ssl_origin_unknown_ca", "ssl_origin_verify_fail", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "ssl_origin_decrypt_fail", "ssl_origin_wrong_ver", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "ssl_origin_other", "ssl_handshake_time", stats, ColorPair::Border4); + if (row4_height > 7) + drawStatPairRow(BOX_WIDTH * 2, row + 6, "tls_v10", "tls_v11", stats, ColorPair::Border4); + + drawStatPairRow(BOX_WIDTH * 3, row + 1, "txn_aborts", "txn_possible_aborts", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH * 3, row + 2, "txn_other_errors", "h2_session_die_error", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH * 3, row + 3, "h2_session_die_high_error", "err_conn_fail", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH * 3, row + 4, "err_client_abort", "err_client_read", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH * 3, row + 5, "changed_time", "not_time", stats, ColorPair::Border); + if (row4_height > 7) + drawStatPairRow(BOX_WIDTH * 3, row + 6, "no_time", "client_dyn_ka", stats, ColorPair::Border); + } +} + +void +Display::renderResponsePage(Stats &stats) +{ + // Layout: 80x24 -> 2 cols, 120x40 -> 3 cols, 160+ -> 5 cols + int box_height = std::min(10, _height - 4); + + if (_width >= WIDTH_LARGE) { + // Wide terminal: 5 columns for each response class + int w = _width / 5; + + drawBox(0, 0, w, box_height, "1xx", ColorPair::Border); + drawBox(w, 0, w, box_height, "2xx", ColorPair::Border2); + drawBox(w * 2, 0, w, box_height, "3xx", ColorPair::Border3); + drawBox(w * 3, 0, w, box_height, "4xx", ColorPair::Border); + drawBox(w * 4, 0, _width - w * 4, box_height, "5xx", ColorPair::Border2); + + std::vector r1 = {"100", "101", "1xx"}; + drawStatTable(2, 1, r1, stats, 6); + + std::vector r2 = {"200", "201", "204", "206", "2xx"}; + drawStatTable(w + 2, 1, r2, stats, 6); + + std::vector r3 = {"301", "302", "304", "307", "3xx"}; + drawStatTable(w * 2 + 2, 1, r3, stats, 6); + + std::vector r4 = {"400", "401", "403", "404", "408", "4xx"}; + drawStatTable(w * 3 + 2, 1, r4, stats, 6); + + std::vector r5 = {"500", "502", "503", "504", "5xx"}; + drawStatTable(w * 4 + 2, 1, r5, stats, 6); + + // Extended codes if height allows + if (_height > box_height + 8) { + int y2 = box_height + 1; + int h2 = std::min(_height - box_height - 3, 8); + + drawBox(0, y2, _width / 2, h2, "4xx EXTENDED", ColorPair::Border3); + drawBox(_width / 2, y2, _width - _width / 2, h2, "METHODS", ColorPair::Border); + + std::vector r4ext = {"405", "406", "409", "410", "413", "414", "416"}; + drawStatTable(2, y2 + 1, r4ext, stats, 6); + + std::vector methods = {"get", "head", "post", "put", "delete"}; + drawStatTable(_width / 2 + 2, y2 + 1, methods, stats, 8); + } + + } else if (_width >= WIDTH_MEDIUM) { + // Medium terminal: 3 columns + int w = _width / 3; + + drawBox(0, 0, w, box_height, "1xx/2xx", ColorPair::Border); + drawBox(w, 0, w, box_height, "3xx/4xx", ColorPair::Border2); + drawBox(w * 2, 0, _width - w * 2, box_height, "5xx/ERR", ColorPair::Border3); + + std::vector r12 = {"1xx", "200", "201", "206", "2xx"}; + drawStatTable(2, 1, r12, stats, 6); + + std::vector r34 = {"301", "302", "304", "3xx", "404", "4xx"}; + drawStatTable(w + 2, 1, r34, stats, 6); + + std::vector r5e = {"500", "502", "503", "5xx", "conn_fail"}; + drawStatTable(w * 2 + 2, 1, r5e, stats, 8); + + } else { + // Classic 80x24: 3x2 grid layout for response codes and methods + // For 24 lines: 23 usable (1 status bar), need 3 rows of boxes + int w = _width / 2; + int available = _height - 1; // Leave room for status bar + + // Each box needs: 2 border rows + content rows + // 1xx/2xx: 5 stats max -> 7 rows + // 3xx/4xx: 5 stats max -> 7 rows + // 5xx/Methods: 5 stats max -> remaining + int row1_height = 7; + int row2_height = 7; + int row3_height = available - row1_height - row2_height; + + // Top row: 1xx and 2xx + drawBox(0, 0, w, row1_height, "1xx", ColorPair::Border); + drawBox(w, 0, _width - w, row1_height, "2xx", ColorPair::Border2); + + std::vector r1 = {"100", "101", "1xx"}; + drawStatTable(2, 1, r1, stats, 6); + + std::vector r2 = {"200", "201", "204", "206", "2xx"}; + drawStatTable(w + 2, 1, r2, stats, 6); + + // Middle row: 3xx and 4xx + int y2 = row1_height; + drawBox(0, y2, w, row2_height, "3xx", ColorPair::Border3); + drawBox(w, y2, _width - w, row2_height, "4xx", ColorPair::Border); + + std::vector r3 = {"301", "302", "304", "307", "3xx"}; + drawStatTable(2, y2 + 1, r3, stats, 6); + + std::vector r4 = {"400", "401", "403", "404", "4xx"}; + drawStatTable(w + 2, y2 + 1, r4, stats, 6); + + // Bottom row: 5xx and Methods + int y3 = y2 + row2_height; + if (row3_height > 2) { + drawBox(0, y3, w, row3_height, "5xx", ColorPair::Border2); + drawBox(w, y3, _width - w, row3_height, "METHODS", ColorPair::Border3); + + std::vector r5 = {"500", "502", "503", "504", "5xx"}; + drawStatTable(2, y3 + 1, r5, stats, 6); + + std::vector methods = {"get", "head", "post", "put", "delete"}; + drawStatTable(w + 2, y3 + 1, methods, stats, 8); + } + } +} + +void +Display::renderConnectionPage(Stats &stats) +{ + // Layout with protocol, client, origin, bandwidth, and network stats + // For 80x24: 3 rows of boxes, each with enough height for their stats + int w = _width / 2; + int label_width = (_width >= WIDTH_MEDIUM) ? LABEL_WIDTH_MD : LABEL_WIDTH_SM; + + // Calculate box heights based on available space (leave 1 row for status bar) + int available = _height - 1; // Leave room for status bar + int row1_height = 7; // HTTP/1.x (3 stats) and HTTP/2 (5 stats) + int row2_height = 7; // CLIENT (5 stats) and ORIGIN (4 stats) + int row3_height = available - row1_height - row2_height; // BANDWIDTH and NETWORK + + // Adjust if terminal is too small + if (available < 20) { + row1_height = 5; + row2_height = 5; + row3_height = available - row1_height - row2_height; + } + + // Top row: HTTP/1.x and HTTP/2 + drawBox(0, 0, w, row1_height, "HTTP/1.x", ColorPair::Border); + drawBox(w, 0, _width - w, row1_height, "HTTP/2", ColorPair::Border2); + + std::vector h1 = {"client_conn_h1", "client_curr_conn_h1", "client_actv_conn_h1"}; + drawStatTable(2, 1, h1, stats, label_width); + + std::vector h2 = {"client_conn_h2", "client_curr_conn_h2", "client_actv_conn_h2", "h2_streams_total", + "h2_streams_current"}; + drawStatTable(w + 2, 1, h2, stats, label_width); + + // Middle row: Client and Origin + int y2 = row1_height; + drawBox(0, y2, w, row2_height, "CLIENT", ColorPair::Border3); + drawBox(w, y2, _width - w, row2_height, "ORIGIN", ColorPair::Border); + + std::vector client = {"client_req", "client_conn", "client_curr_conn", "client_actv_conn", "client_req_conn"}; + drawStatTable(2, y2 + 1, client, stats, label_width); + + std::vector origin = {"server_req", "server_conn", "server_curr_conn", "server_req_conn"}; + drawStatTable(w + 2, y2 + 1, origin, stats, label_width); + + // Bottom row: Bandwidth and Network + int y3 = y2 + row2_height; + if (row3_height > 2) { + drawBox(0, y3, w, row3_height, "BANDWIDTH", ColorPair::Border2); + drawBox(w, y3, _width - w, row3_height, "NETWORK", ColorPair::Border3); + + std::vector bw = {"client_head", "client_body", "client_net", "client_avg_size"}; + drawStatTable(2, y3 + 1, bw, stats, label_width); + + std::vector net = {"server_head", "server_body", "server_net", "server_avg_size"}; + drawStatTable(w + 2, y3 + 1, net, stats, label_width); + } +} + +void +Display::renderCachePage(Stats &stats) +{ + // Layout: 80x24 -> 2 cols, 120x40 -> 3 cols, 160+ -> 4 cols + int box_height = std::min(10, _height / 2); + + if (_width >= WIDTH_LARGE) { + // Wide terminal: 4 columns + int w = _width / 4; + int label_width = LABEL_WIDTH_MD; + + drawBox(0, 0, w, box_height, "STORAGE", ColorPair::Border); + drawBox(w, 0, w, box_height, "OPERATIONS", ColorPair::Border2); + drawBox(w * 2, 0, w, box_height, "HIT/MISS", ColorPair::Border3); + drawBox(w * 3, 0, _width - w * 3, box_height, "LATENCY", ColorPair::Border); + + std::vector storage = {"disk_used", "disk_total", "ram_used", "ram_total", "entries", "avg_size"}; + drawStatTable(2, 1, storage, stats, label_width); + + std::vector ops = {"lookups", "cache_writes", "cache_updates", "cache_deletes", "read_active", "write_active"}; + drawStatTable(w + 2, 1, ops, stats, label_width); + + std::vector hits = {"ram_ratio", "ram_hit", "ram_miss", "fresh", "reval", "cold"}; + drawStatTable(w * 2 + 2, 1, hits, stats, label_width); + + std::vector times = {"fresh_time", "reval_time", "cold_time", "changed_time"}; + drawStatTable(w * 3 + 2, 1, times, stats, label_width); + + // DNS section + if (_height > box_height + 8) { + int y2 = box_height + 1; + int h2 = std::min(_height - box_height - 3, 6); + + drawBox(0, y2, _width, h2, "DNS CACHE", ColorPair::Border2); + + std::vector dns = {"dns_lookups", "dns_hits", "dns_ratio", "dns_entry"}; + drawStatTable(2, y2 + 1, dns, stats, label_width); + } + + } else { + // Classic/Medium terminal: 2x3 grid layout + int w = _width / 2; + int label_width = (_width >= WIDTH_MEDIUM) ? LABEL_WIDTH_MD : LABEL_WIDTH_SM; + int available = _height - 1; // Leave room for status bar + + // Storage/Operations: 6 stats -> 8 rows + // Hit Rates/Latency: 7 stats / 6 stats -> 9 rows + // DNS: 4 stats -> remaining + int row1_height = 8; + int row2_height = 9; + int row3_height = available - row1_height - row2_height; + + // Adjust for smaller terminals + if (available < 22) { + row1_height = 7; + row2_height = 7; + row3_height = available - row1_height - row2_height; + } + + // Top row: Storage and Operations + drawBox(0, 0, w, row1_height, "STORAGE", ColorPair::Border); + drawBox(w, 0, _width - w, row1_height, "OPERATIONS", ColorPair::Border2); + + std::vector storage = {"disk_used", "disk_total", "ram_used", "ram_total", "entries", "avg_size"}; + drawStatTable(2, 1, storage, stats, label_width); + + std::vector ops = {"lookups", "cache_writes", "cache_updates", "cache_deletes", "read_active", "write_active"}; + drawStatTable(w + 2, 1, ops, stats, label_width); + + // Middle row: Hit Rates and Latency + int y2 = row1_height; + drawBox(0, y2, w, row2_height, "HIT RATES", ColorPair::Border3); + drawBox(w, y2, _width - w, row2_height, "LATENCY (ms)", ColorPair::Border); + + std::vector hits = {"ram_ratio", "fresh", "reval", "cold", "changed", "not", "no"}; + drawStatTable(2, y2 + 1, hits, stats, label_width); + + std::vector latency = {"fresh_time", "reval_time", "cold_time", "changed_time", "not_time", "no_time"}; + drawStatTable(w + 2, y2 + 1, latency, stats, label_width); + + // Bottom row: DNS + int y3 = y2 + row2_height; + if (row3_height > 2) { + drawBox(0, y3, _width, row3_height, "DNS", ColorPair::Border2); + + std::vector dns = {"dns_lookups", "dns_hits", "dns_ratio", "dns_entry"}; + drawStatTable(2, y3 + 1, dns, stats, label_width); + } + } +} + +void +Display::renderSSLPage(Stats &stats) +{ + // SSL page with comprehensive SSL/TLS metrics + int w = _width / 2; + int label_width = (_width >= WIDTH_MEDIUM) ? LABEL_WIDTH_LG : LABEL_WIDTH_MD; + int available = _height - 1; // Leave room for status bar + + // Handshakes/Sessions: 5 stats -> 7 rows + // Origin Errors/TLS: 5/4 stats -> 7 rows + // Client/General Errors: remaining + int row1_height = 7; + int row2_height = 7; + int row3_height = available - row1_height - row2_height; + + // Adjust for smaller terminals + if (available < 20) { + row1_height = 6; + row2_height = 6; + row3_height = available - row1_height - row2_height; + } + + // Top row: Handshakes and Sessions + drawBox(0, 0, w, row1_height, "HANDSHAKES", ColorPair::Border); + drawBox(w, 0, _width - w, row1_height, "SESSIONS", ColorPair::Border2); + + std::vector handshake = {"ssl_attempts_in", "ssl_success_in", "ssl_attempts_out", "ssl_success_out", + "ssl_handshake_time"}; + drawStatTable(2, 1, handshake, stats, label_width); + + std::vector session = {"ssl_session_hit", "ssl_session_miss", "ssl_sess_new", "ssl_sess_evict", "ssl_origin_reused"}; + drawStatTable(w + 2, 1, session, stats, label_width); + + // Middle row: Origin Errors and TLS Versions + int y2 = row1_height; + drawBox(0, y2, w, row2_height, "ORIGIN ERRORS", ColorPair::Border3); + drawBox(w, y2, _width - w, row2_height, "TLS VERSIONS", ColorPair::Border); + + std::vector origin_err = {"ssl_origin_bad_cert", "ssl_origin_expired", "ssl_origin_revoked", "ssl_origin_unknown_ca", + "ssl_origin_verify_fail"}; + drawStatTable(2, y2 + 1, origin_err, stats, label_width); + + std::vector tls_ver = {"tls_v10", "tls_v11", "tls_v12", "tls_v13"}; + drawStatTable(w + 2, y2 + 1, tls_ver, stats, label_width); + + // Bottom row: Client Errors and General Errors + int y3 = y2 + row2_height; + if (row3_height > 2) { + drawBox(0, y3, w, row3_height, "CLIENT ERRORS", ColorPair::Border2); + drawBox(w, y3, _width - w, row3_height, "GENERAL ERRORS", ColorPair::Border3); + + std::vector client_err = {"ssl_client_bad_cert"}; + drawStatTable(2, y3 + 1, client_err, stats, label_width); + + std::vector general_err = {"ssl_error_ssl", "ssl_error_syscall", "ssl_error_async"}; + drawStatTable(w + 2, y3 + 1, general_err, stats, label_width); + } +} + +void +Display::renderErrorsPage(Stats &stats) +{ + // Comprehensive error page with all error categories + int w = _width / 2; + int label_width = (_width >= WIDTH_MEDIUM) ? LABEL_WIDTH_MD : LABEL_WIDTH_SM; + int available = _height - 1; // Leave room for status bar + + // Connection/Transaction: 3 stats -> 5 rows + // Cache/Origin: 3 stats -> 5 rows + // HTTP/2/HTTP: 4/6 stats -> remaining + int row1_height = 5; + int row2_height = 5; + int row3_height = available - row1_height - row2_height; + + // Top row: Connection and Transaction errors + drawBox(0, 0, w, row1_height, "CONNECTION", ColorPair::Border); + drawBox(w, 0, _width - w, row1_height, "TRANSACTION", ColorPair::Border2); + + std::vector conn = {"err_conn_fail", "err_client_abort", "err_client_read"}; + drawStatTable(2, 1, conn, stats, label_width); + + std::vector tx = {"txn_aborts", "txn_possible_aborts", "txn_other_errors"}; + drawStatTable(w + 2, 1, tx, stats, label_width); + + // Middle row: Cache and Origin errors + int y2 = row1_height; + drawBox(0, y2, w, row2_height, "CACHE", ColorPair::Border3); + drawBox(w, y2, _width - w, row2_height, "ORIGIN", ColorPair::Border); + + std::vector cache_err = {"cache_read_errors", "cache_write_errors", "cache_lookup_fail"}; + drawStatTable(2, y2 + 1, cache_err, stats, label_width); + + std::vector origin_err = {"conn_fail", "abort", "other_err"}; + drawStatTable(w + 2, y2 + 1, origin_err, stats, label_width); + + // Bottom row: HTTP/2 and HTTP response errors + int y3 = y2 + row2_height; + if (row3_height > 2) { + drawBox(0, y3, w, row3_height, "HTTP/2", ColorPair::Border2); + drawBox(w, y3, _width - w, row3_height, "HTTP", ColorPair::Border3); + + std::vector h2_err = {"h2_stream_errors", "h2_conn_errors", "h2_session_die_error", "h2_session_die_high_error"}; + drawStatTable(2, y3 + 1, h2_err, stats, label_width); + + std::vector http_err = {"400", "404", "4xx", "500", "502", "5xx"}; + drawStatTable(w + 2, y3 + 1, http_err, stats, 6); + } +} + +void +Display::renderPerformancePage(Stats &stats) +{ + // Performance page showing HTTP milestone timing data in chronological order + // Milestones are cumulative nanoseconds, displayed as ms/s + int label_width = (_width >= WIDTH_MEDIUM) ? LABEL_WIDTH_MD : LABEL_WIDTH_SM; + int available = _height - 1; // Leave room for status bar + + // All milestones in chronological order of when they occur during a request + // clang-format off + std::vector milestones = { + "ms_sm_start", // 1. State machine starts + "ms_ua_begin", // 2. Client connection begins + "ms_ua_first_read", // 3. First read from client + "ms_ua_read_header", // 4. Client headers fully read + "ms_cache_read_begin", // 5. Start checking cache + "ms_cache_read_end", // 6. Done checking cache + "ms_dns_begin", // 7. DNS lookup starts (if cache miss) + "ms_dns_end", // 8. DNS lookup ends + "ms_server_connect", // 9. Start connecting to origin + "ms_server_first_connect", // 10. First connection to origin + "ms_server_connect_end", // 11. Connection established + "ms_server_begin_write", // 12. Start writing to origin + "ms_server_first_read", // 13. First read from origin + "ms_server_read_header", // 14. Origin headers received + "ms_cache_write_begin", // 15. Start writing to cache + "ms_cache_write_end", // 16. Done writing to cache + "ms_ua_begin_write", // 17. Start writing to client + "ms_server_close", // 18. Origin connection closed + "ms_ua_close", // 19. Client connection closed + "ms_sm_finish" // 20. State machine finished + }; + // clang-format on + + // For wider terminals, use two columns + if (_width >= WIDTH_MEDIUM) { + // Two-column layout + int col_width = _width / 2; + int box_height = available; + int stats_per_col = static_cast(milestones.size() + 1) / 2; + + drawBox(0, 0, col_width, box_height, "MILESTONES (ms/s)", ColorPair::Border); + drawBox(col_width, 0, _width - col_width, box_height, "MILESTONES (cont)", ColorPair::Border); + + // Left column - first half of milestones + int max_left = std::min(stats_per_col, box_height - 2); + std::vector left_stats(milestones.begin(), milestones.begin() + max_left); + drawStatTable(2, 1, left_stats, stats, label_width); + + // Right column - second half of milestones + int max_right = std::min(static_cast(milestones.size()) - stats_per_col, box_height - 2); + if (max_right > 0) { + std::vector right_stats(milestones.begin() + stats_per_col, milestones.begin() + stats_per_col + max_right); + drawStatTable(col_width + 2, 1, right_stats, stats, label_width); + } + } else { + // Single column for narrow terminals + drawBox(0, 0, _width, available, "MILESTONES (ms/s)", ColorPair::Border); + + int max_stats = std::min(static_cast(milestones.size()), available - 2); + milestones.resize(max_stats); + drawStatTable(2, 1, milestones, stats, label_width); + } +} + +void +Display::renderGraphsPage(Stats &stats) +{ + // Layout graphs based on terminal width + // 80x24: Two 40-char boxes side by side, then 80-char multi-graph box + // 120x40: Three 40-char boxes, then 120-char wide graphs + // 160+: Four 40-char boxes + + // Helper lambda to format value with suffix + auto formatValue = [](double value, const char *suffix = "") -> std::string { + char buffer[32]; + if (value >= 1000000000.0) { + snprintf(buffer, sizeof(buffer), "%.0fG%s", value / 1000000000.0, suffix); + } else if (value >= 1000000.0) { + snprintf(buffer, sizeof(buffer), "%.0fM%s", value / 1000000.0, suffix); + } else if (value >= 1000.0) { + snprintf(buffer, sizeof(buffer), "%.0fK%s", value / 1000.0, suffix); + } else { + snprintf(buffer, sizeof(buffer), "%.0f%s", value, suffix); + } + return buffer; + }; + + // Get current values + double clientReq = 0, clientNet = 0, serverNet = 0, ramRatio = 0; + double clientConn = 0, serverConn = 0, lookups = 0, cacheWrites = 0; + stats.getStat("client_req", clientReq); + stats.getStat("client_net", clientNet); + stats.getStat("server_net", serverNet); + stats.getStat("ram_ratio", ramRatio); + stats.getStat("client_curr_conn", clientConn); + stats.getStat("server_curr_conn", serverConn); + stats.getStat("lookups", lookups); + stats.getStat("cache_writes", cacheWrites); + + // Build graph data + std::vector, std::string>> networkGraphs = { + {"Net In", stats.getHistory("client_net"), formatValue(clientNet * 8, " b/s")}, + {"Net Out", stats.getHistory("server_net"), formatValue(serverNet * 8, " b/s")}, + }; + + std::vector, std::string>> cacheGraphs = { + {"Hit Rate", stats.getHistory("ram_ratio", 100.0), formatValue(ramRatio, "%")}, + {"Lookups", stats.getHistory("lookups"), formatValue(lookups, "/s")}, + {"Writes", stats.getHistory("cache_writes"), formatValue(cacheWrites, "/s")}, + }; + + std::vector, std::string>> connGraphs = { + {"Client", stats.getHistory("client_curr_conn"), formatValue(clientConn)}, + {"Origin", stats.getHistory("server_curr_conn"), formatValue(serverConn)}, + }; + + std::vector, std::string>> requestGraphs = { + {"Requests", stats.getHistory("client_req"), formatValue(clientReq, "/s")}, + }; + + if (_width >= WIDTH_LARGE) { + // Wide terminal (160+): 4 columns of 40-char boxes + int w = 40; + + drawMultiGraphBox(0, 0, w, networkGraphs, "NETWORK"); + drawMultiGraphBox(w, 0, w, cacheGraphs, "CACHE"); + drawMultiGraphBox(w * 2, 0, w, connGraphs, "CONNECTIONS"); + drawMultiGraphBox(w * 3, 0, _width - w * 3, requestGraphs, "REQUESTS"); + + // Second row: Wide bandwidth history if height allows + if (_height > 10) { + std::vector, std::string>> allGraphs = { + {"Client In", stats.getHistory("client_net"), formatValue(clientNet * 8, " b/s")}, + {"Origin Out", stats.getHistory("server_net"), formatValue(serverNet * 8, " b/s")}, + {"Requests", stats.getHistory("client_req"), formatValue(clientReq, "/s")}, + {"Hit Rate", stats.getHistory("ram_ratio", 100.0), formatValue(ramRatio, "%")}, + }; + drawMultiGraphBox(0, 6, _width, allGraphs, "TRAFFIC OVERVIEW"); + } + + } else if (_width >= WIDTH_MEDIUM) { + // Medium terminal (120): 3 columns of 40-char boxes + int w = 40; + + drawMultiGraphBox(0, 0, w, networkGraphs, "NETWORK"); + drawMultiGraphBox(w, 0, w, cacheGraphs, "CACHE"); + drawMultiGraphBox(w * 2, 0, _width - w * 2, connGraphs, "CONNECTIONS"); + + // Second row: requests graph spanning full width + if (_height > 8) { + std::vector, std::string>> overviewGraphs = { + {"Requests", stats.getHistory("client_req"), formatValue(clientReq, "/s")}, + {"Hit Rate", stats.getHistory("ram_ratio", 100.0), formatValue(ramRatio, "%")}, + {"Client", stats.getHistory("client_curr_conn"), formatValue(clientConn)}, + }; + drawMultiGraphBox(0, 6, _width, overviewGraphs, "OVERVIEW"); + } + + } else { + // Classic terminal (80): 2 columns of 40-char boxes + 80-char overview + int w = _width / 2; + + // Combine network graphs for smaller box + std::vector, std::string>> leftGraphs = { + {"Net In", stats.getHistory("client_net"), formatValue(clientNet * 8, " b/s")}, + {"Net Out", stats.getHistory("server_net"), formatValue(serverNet * 8, " b/s")}, + }; + + std::vector, std::string>> rightGraphs = { + {"Hit Rate", stats.getHistory("ram_ratio", 100.0), formatValue(ramRatio, "%")}, + {"Requests", stats.getHistory("client_req"), formatValue(clientReq, "/s")}, + }; + + drawMultiGraphBox(0, 0, w, leftGraphs, "NETWORK"); + drawMultiGraphBox(w, 0, _width - w, rightGraphs, "CACHE"); + + // Second row: full-width overview + if (_height > 8) { + std::vector, std::string>> overviewGraphs = { + {"Bandwidth", stats.getHistory("client_net"), formatValue(clientNet * 8, " b/s")}, + {"Hit Rate", stats.getHistory("ram_ratio", 100.0), formatValue(ramRatio, "%")}, + {"Requests", stats.getHistory("client_req"), formatValue(clientReq, "/s")}, + {"Connections", stats.getHistory("client_curr_conn"), formatValue(clientConn)}, + }; + drawMultiGraphBox(0, 5, _width, overviewGraphs, "TRAFFIC OVERVIEW"); + } + } +} + +void +Display::renderHelpPage(const std::string &host, const std::string &version) +{ + int box_width = std::min(80, _width - 4); + int box_x = (_width - box_width) / 2; + + drawBox(box_x, 0, box_width, _height - 2, "HELP", ColorPair::Border); + + int y = 2; + int x = box_x + 2; + int col2 = box_x + box_width / 2; + + moveTo(y++, x); + setBold(); + setColor(ColorPair::Cyan); + printf("TRAFFIC_TOP - ATS Real-time Monitor"); + resetColor(); + y++; + + moveTo(y++, x); + setBold(); + printf("Navigation"); + resetColor(); + + moveTo(y++, x); + printf(" 1-8 Switch to page N"); + moveTo(y++, x); + printf(" Left/m Previous page"); + moveTo(y++, x); + printf(" Right/r Next page"); + moveTo(y++, x); + printf(" h or ? Show this help"); + moveTo(y++, x); + printf(" a Toggle absolute/rate mode"); + moveTo(y++, x); + printf(" b/ESC Back (from help)"); + moveTo(y++, x); + printf(" q Quit"); + y++; + + moveTo(y++, x); + setBold(); + printf("Pages"); + resetColor(); + + moveTo(y++, x); + printf(" 1 Overview Cache, requests, connections"); + moveTo(y++, x); + printf(" 2 Responses HTTP response code breakdown"); + moveTo(y++, x); + printf(" 3 Connections HTTP/1.x vs HTTP/2 details"); + moveTo(y++, x); + printf(" 4 Cache Storage, operations, hit rates"); + moveTo(y++, x); + printf(" 5 SSL/TLS Handshake and session stats"); + moveTo(y++, x); + printf(" 6 Errors Connection and HTTP errors"); + moveTo(y++, x); + printf(" 7/p Performance HTTP milestones timing (ms/s)"); + moveTo(y++, x); + printf(" 8/g Graphs Real-time graphs"); + y++; + + // Right column - Cache definitions + int y2 = 4; + moveTo(y2++, col2); + setBold(); + printf("Cache States"); + resetColor(); + + moveTo(y2, col2); + setColor(ColorPair::Green); + printf(" Fresh"); + resetColor(); + moveTo(y2++, col2 + 12); + printf("Served from cache"); + + moveTo(y2, col2); + setColor(ColorPair::Cyan); + printf(" Reval"); + resetColor(); + moveTo(y2++, col2 + 12); + printf("Revalidated with origin"); + + moveTo(y2, col2); + setColor(ColorPair::Yellow); + printf(" Cold"); + resetColor(); + moveTo(y2++, col2 + 12); + printf("Cache miss"); + + moveTo(y2, col2); + setColor(ColorPair::Yellow); + printf(" Changed"); + resetColor(); + moveTo(y2++, col2 + 12); + printf("Cache entry updated"); + + // Connection info + y2 += 2; + moveTo(y2++, col2); + setBold(); + printf("Connection"); + resetColor(); + + moveTo(y2++, col2); + printf(" Host: %s", host.c_str()); + moveTo(y2++, col2); + printf(" ATS: %s", version.empty() ? "unknown" : version.c_str()); + + // Footer + moveTo(_height - 3, x); + setColor(ColorPair::Cyan); + printf("Press any key to return..."); + resetColor(); +} + +} // namespace traffic_top diff --git a/src/traffic_top/Display.h b/src/traffic_top/Display.h new file mode 100644 index 00000000000..65cbbd616de --- /dev/null +++ b/src/traffic_top/Display.h @@ -0,0 +1,342 @@ +/** @file + + Display class for traffic_top using direct ANSI terminal output. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#pragma once + +#include +#include +#include +#include + +#include "Stats.h" +#include "StatType.h" + +namespace traffic_top +{ + +/// Color indices used for selecting colors +namespace ColorPair +{ + constexpr short Red = 1; + constexpr short Yellow = 2; + constexpr short Green = 3; + constexpr short Blue = 4; + constexpr short Grey = 5; + constexpr short Cyan = 6; + constexpr short Border = 7; // Primary border color (cyan) + constexpr short Border2 = 8; // Secondary border color (blue) + constexpr short Border3 = 9; // Tertiary border color (magenta) + constexpr short Dim = 10; + constexpr short Magenta = 11; + // Bright border colors + constexpr short Border4 = 12; // Bright blue + constexpr short Border5 = 13; // Bright yellow + constexpr short Border6 = 14; // Bright red + constexpr short Border7 = 15; // Bright green +} // namespace ColorPair + +/// Unicode box-drawing characters with rounded corners +namespace BoxChars +{ + constexpr const char *TopLeft = "╭"; + constexpr const char *TopRight = "╮"; + constexpr const char *BottomLeft = "╰"; + constexpr const char *BottomRight = "╯"; + constexpr const char *Horizontal = "─"; + constexpr const char *Vertical = "│"; + + // ASCII fallback + constexpr const char *AsciiTopLeft = "+"; + constexpr const char *AsciiTopRight = "+"; + constexpr const char *AsciiBottomLeft = "+"; + constexpr const char *AsciiBottomRight = "+"; + constexpr const char *AsciiHorizontal = "-"; + constexpr const char *AsciiVertical = "|"; +} // namespace BoxChars + +/// Unicode block characters for graphs (8 height levels) +namespace GraphChars +{ + // Block characters from empty to full (index 0-8) + constexpr const char *Blocks[] = { + " ", // 0 - empty + "▁", // 1 - lower 1/8 + "▂", // 2 - lower 2/8 + "▃", // 3 - lower 3/8 + "▄", // 4 - lower 4/8 + "▅", // 5 - lower 5/8 + "▆", // 6 - lower 6/8 + "▇", // 7 - lower 7/8 + "█" // 8 - full block + }; + + // ASCII fallback characters + constexpr const char AsciiBlocks[] = {' ', '_', '.', '-', '=', '+', '#', '#', '#'}; + + constexpr int NumLevels = 9; +} // namespace GraphChars + +/// Available display pages +enum class Page { + Main = 0, + Response = 1, + Connection = 2, + Cache = 3, + SSL = 4, + Errors = 5, + Performance = 6, + Graphs = 7, + Help = 8, + PageCount = 9 +}; + +/** + * Display manager for traffic_top curses interface. + */ +class Display +{ +public: + Display(); + ~Display(); + + // Non-copyable, non-movable + Display(const Display &) = delete; + Display &operator=(const Display &) = delete; + Display(Display &&) = delete; + Display &operator=(Display &&) = delete; + + /** + * Initialize curses and colors. + * @return true on success + */ + bool initialize(); + + /** + * Clean up terminal. + */ + void shutdown(); + + /** + * Get keyboard input with timeout. + * @param timeout_ms Timeout in milliseconds (0 = non-blocking, -1 = blocking) + * @return Character code, or -1 if no input within timeout. + * Special keys: KEY_LEFT=0x104, KEY_RIGHT=0x105, KEY_UP=0x103, KEY_DOWN=0x102 + */ + int getInput(int timeout_ms); + + /// Special key codes (compatible with ncurses KEY_* values) + static constexpr int KEY_NONE = -1; + static constexpr int KEY_UP = 0x103; + static constexpr int KEY_DOWN = 0x102; + static constexpr int KEY_LEFT = 0x104; + static constexpr int KEY_RIGHT = 0x105; + + /** + * Set whether to use ASCII box characters instead of Unicode. + */ + void + setAsciiMode(bool ascii) + { + _ascii_mode = ascii; + } + + /** + * Render the current page. + */ + void render(Stats &stats, Page page, bool absolute); + + /** + * Get terminal dimensions. + */ + void getTerminalSize(int &width, int &height) const; + + /** + * Draw a box around a region with rounded corners. + * @param x Starting column + * @param y Starting row + * @param width Box width + * @param height Box height + * @param title Title to display in top border + * @param colorIdx Color pair index for the border (use ColorPair::Border, Border2, Border3) + */ + void drawBox(int x, int y, int width, int height, const std::string &title = "", short colorIdx = ColorPair::Border); + + /** + * Draw a stat table. + * @param x Starting column + * @param y Starting row + * @param items List of stat keys to display + * @param stats Stats object to fetch values from + * @param labelWidth Width for the label column + */ + void drawStatTable(int x, int y, const std::vector &items, Stats &stats, int labelWidth = 14); + + /** + * Draw stats in a grid layout with multiple columns per row. + * @param x Starting column + * @param y Starting row + * @param boxWidth Width of the containing box + * @param items List of stat keys to display + * @param stats Stats object to fetch values from + * @param cols Number of columns + */ + void drawStatGrid(int x, int y, int boxWidth, const std::vector &items, Stats &stats, int cols = 3); + + /** + * Format and print a stat value with appropriate color. + */ + void printStatValue(int x, int y, double value, StatType type); + + /** + * Draw a mini progress bar for percentage values. + * @param x Starting column + * @param y Row + * @param percent Value 0-100 + * @param width Bar width in characters + */ + void drawProgressBar(int x, int y, double percent, int width = 8); + + /** + * Draw a graph line using block characters. + * @param x Starting column + * @param y Row + * @param data Vector of values (0.0-1.0 normalized) + * @param width Width of graph in characters + * @param colored Whether to use color gradient + */ + void drawGraphLine(int x, int y, const std::vector &data, int width, bool colored = true); + + /** + * Draw a multi-graph box with label, graph, and value on each row. + * Format: | LABEL ▂▁▁▂▃▄▅▆▇ VALUE | + * @param x Starting column + * @param y Starting row + * @param width Box width + * @param graphs Vector of (label, data, value) tuples + * @param title Optional title for the box header + */ + void drawMultiGraphBox(int x, int y, int width, + const std::vector, std::string>> &graphs, + const std::string &title = ""); + + /** + * Draw the status bar at the bottom of the screen. + */ + void drawStatusBar(const std::string &host, Page page, bool absolute, bool connected); + + /** + * Get page name for display. + */ + static const char *getPageName(Page page); + + /** + * Get total number of pages. + */ + static int + getPageCount() + { + return static_cast(Page::PageCount) - 1; + } // Exclude Help + +private: + // ------------------------------------------------------------------------- + // Page rendering functions + // ------------------------------------------------------------------------- + // Each page has a dedicated render function that draws the appropriate + // stats and layout for that category. + + void renderMainPage(Stats &stats); ///< Overview page with cache, connections, requests + void renderResponsePage(Stats &stats); ///< HTTP response code breakdown (2xx, 4xx, 5xx, etc.) + void renderConnectionPage(Stats &stats); ///< HTTP/1.x vs HTTP/2 connection details + void renderCachePage(Stats &stats); ///< Cache storage, operations, hit rates + void renderSSLPage(Stats &stats); ///< SSL/TLS handshake and session statistics + void renderErrorsPage(Stats &stats); ///< Connection errors, HTTP errors, cache errors + void renderPerformancePage(Stats &stats); ///< HTTP milestone timing (request lifecycle) + void renderGraphsPage(Stats &stats); ///< Real-time graphs + void renderHelpPage(const std::string &host, const std::string &version); + + // ------------------------------------------------------------------------- + // Responsive layout functions for the main overview page + // ------------------------------------------------------------------------- + // The main page adapts its layout based on terminal width: + // - 80 columns: 2 boxes per row, 2 rows (minimal layout) + // - 120 columns: 3 boxes per row, more stats visible + // - 160+ columns: 4 boxes per row, full stat coverage + // See LAYOUT.md for detailed layout specifications. + + void render80Layout(Stats &stats); ///< Layout for 80-column terminals + void render120Layout(Stats &stats); ///< Layout for 120-column terminals + void render160Layout(Stats &stats); ///< Layout for 160+ column terminals + + /** + * Draw a row of stat pairs inside a 40-char box. + * This is the core layout primitive for the main page boxes. + * + * Format: | Label1 Value1 Label2 Value2 | + * ^-- border border--^ + * + * @param x Box starting column (where the left border is) + * @param y Row number + * @param key1 First stat key (from lookup table) + * @param key2 Second stat key (from lookup table) + * @param stats Stats object to fetch values from + * @param borderColor Color for the vertical borders + */ + void drawStatPairRow(int x, int y, const std::string &key1, const std::string &key2, Stats &stats, + short borderColor = ColorPair::Border); + + /** + * Draw a section header line spanning between two x positions. + */ + void drawSectionHeader(int y, int x1, int x2, const std::string &title); + + /** + * Helper to select Unicode or ASCII box-drawing character. + * @param unicode The Unicode character to use normally + * @param ascii The ASCII fallback character + * @return The appropriate character based on _ascii_mode + */ + const char * + boxChar(const char *unicode, const char *ascii) const + { + return _ascii_mode ? ascii : unicode; + } + + /** + * Detect UTF-8 support from environment variables (LANG, LC_ALL, etc.). + * Used to auto-detect whether to use Unicode or ASCII box characters. + * @return true if UTF-8 appears to be supported + */ + static bool detectUtf8Support(); + + // ------------------------------------------------------------------------- + // State variables + // ------------------------------------------------------------------------- + bool _initialized = false; ///< True after successful initialize() call + bool _ascii_mode = false; ///< True = use ASCII box chars, False = use Unicode + int _width = 80; ///< Current terminal width in columns + int _height = 24; ///< Current terminal height in rows + struct termios _orig_termios; ///< Original terminal settings (restored on shutdown) + bool _termios_saved = false; ///< True if _orig_termios has valid saved state +}; + +} // namespace traffic_top diff --git a/src/traffic_top/LAYOUT.md b/src/traffic_top/LAYOUT.md new file mode 100644 index 00000000000..0d736c09326 --- /dev/null +++ b/src/traffic_top/LAYOUT.md @@ -0,0 +1,277 @@ +# traffic_top Layout Documentation + +This document shows the exact layouts for different terminal sizes. +All layouts use ASCII characters and are exactly the width specified. + +## Column Format + +Each 40-character box contains two stat columns: + +``` +| Disk Used 120G RAM Used 512M | +``` + +- **Box width**: 40 characters total (including `|` borders) +- **Content width**: 38 characters inside borders +- **Stat 1**: 17 characters (label + spaces + number + suffix) +- **Column gap**: 3 spaces between stat pairs +- **Stat 2**: 16 characters (label + spaces + number + suffix) +- **Padding**: 1 space after `|` and 1 space before `|` +- **Numbers are right-aligned** at a fixed position +- **Suffix follows the number** (%, K, M, G, T attached to number) +- **Values without suffix** have trailing space to maintain alignment +- **Labels and values never touch** - always at least 1 space between + +Breakdown: `| ` (2) + stat1 (17) + gap (3) + stat2 (16) + ` |` (2) = 40 ✓ + +## 80x24 Terminal (2 boxes) + +``` ++--------------- CLIENT ---------------++--------------- ORIGIN ---------------+ +| Requests 15K Connections 800 || Requests 12K Connections 400 | +| Current Conn 500 Active Conn 450 || Current Conn 200 Req/Conn 30 | +| Req/Conn 19 Dynamic KA 400 || Connect Fail 5 Aborts 2 | +| Avg Size 45K Net (Mb/s) 850 || Avg Size 52K Net (Mb/s) 620 | +| Resp Time 12 Head Bytes 18M || Keep Alive 380 Conn Reuse 350 | +| Body Bytes 750M HTTP/1 Conn 200 || Head Bytes 15M Body Bytes 600M | +| HTTP/2 Conn 300 SSL Session 450 || DNS Lookups 800 DNS Hits 720 | +| SSL Handshk 120 SSL Errors 3 || DNS Ratio 90% DNS Entry 500 | +| Hit Latency 2 Miss Laten 45 || Error 12 Other Err 5 | ++--------------------------------------++--------------------------------------+ ++--------------- CACHE ----------------++----------- REQS/RESPONSES -----------+ +| Disk Used 120G RAM Used 512M || GET 15K POST 800 | +| Disk Total 500G RAM Total 1G || HEAD 200 PUT 50 | +| RAM Hit 85% Fresh 72% || DELETE 10 OPTIONS 25 | +| Revalidate 12% Cold 8% || 200 78% 206 5% | +| Changed 3% Not Cached 2% || 301 2% 304 12% | +| No Cache 3% Entries 50K || 404 1% 502 0% | +| Lookups 25K Writes 8K || 2xx 83% 3xx 14% | +| Read Act 150 Write Act 45 || 4xx 2% 5xx 1% | +| Updates 500 Deletes 100 || Error 15 Other Err 3 | ++--------------------------------------++--------------------------------------+ + 12:30:45 proxy.example.com [1/6] Overview q h 1-6 +``` + +## 120x40 Terminal (3 boxes) + +``` ++--------------- CACHE ----------------++-------------- REQUESTS --------------++------------ CONNECTIONS -------------+ +| Disk Used 120G Disk Total 500G || Client Req 15K Server Req 12K || Client Conn 800 Current 500 | +| RAM Used 512M RAM Total 1G || GET 12K POST 800 || Active Conn 450 Server Con 400 | +| RAM Ratio 85% Entries 50K || HEAD 200 PUT 50 || Server Curr 200 Req/Conn 30 | +| Lookups 25K Writes 8K || DELETE 10 OPTIONS 25 || HTTP/1 Conn 200 HTTP/2 300 | +| Read Active 150 Write Act 45 || PURGE 5 PUSH 2 || Keep Alive 380 Conn Reuse 350 | +| Updates 500 Deletes 100 || CONNECT 15 TRACE 0 || Dynamic KA 400 Throttled 5 | ++--------------------------------------++--------------------------------------++--------------------------------------+ ++------------- HIT RATES --------------++------------- RESPONSES --------------++------------- BANDWIDTH --------------+ +| RAM Hit 85% Fresh 72% || 200 78% 206 5% || Client Head 18M Client Bod 750M | +| Revalidate 12% Cold 8% || 301 2% 304 12% || Server Head 15M Server Bod 600M | +| Changed 3% Not Cached 2% || 404 1% 502 0% || Avg ReqSize 45K Avg Resp 52K | +| No Cache 3% Error 1% || 503 0% 504 0% || Net In Mbs 850 Net Out 620 | +| Fresh Time 2ms Reval Time 15 || 2xx 83% 3xx 14% || Head Bytes 33M Body Bytes 1G | +| Cold Time 45 Changed T 30 || 4xx 2% 5xx 1% || Avg Latency 12ms Max Laten 450 | ++--------------------------------------++--------------------------------------++--------------------------------------+ ++-------------- SSL/TLS ---------------++---------------- DNS -----------------++--------------- ERRORS ---------------+ +| SSL Success 450 SSL Fail 3 || DNS Lookups 800 DNS Hits 720 || Connect Fail 5 Aborts 2 | +| SSL Session 450 SSL Handshk 120 || DNS Ratio 90% DNS Entry 500 || Client Abrt 15 Origin Err 12 | +| Session Hit 400 Session Mis 50 || Pending 5 In Flight 12 || CacheRdErr 3 Cache Writ 1 | +| TLS 1.2 200 TLS 1.3 250 || Expired 10 Evicted 25 || Timeout 20 Other Err 8 | +| Client Cert 50 Origin SSL 380 || Avg Lookup 2ms Max Lookup 45 || HTTP Err 10 Parse Err 2 | +| Renegotiate 10 Resumption 350 || Failed 5 Retries 12 || DNS Fail 5 SSL Err 3 | ++--------------------------------------++--------------------------------------++--------------------------------------+ ++--------------- CLIENT ---------------++--------------- ORIGIN ---------------++--------------- TOTALS ---------------+ +| Requests 15K Connections 800 || Requests 12K Connections 400 || Total Req 150M Total Conn 5M | +| Current Con 500 Active Conn 450 || Current Con 200 Req/Conn 30 || Total Bytes 50T Uptime 45d | +| Avg Size 45K Net (Mb/s) 850 || Avg Size 52K Net (Mb/s) 620 || Cache Size 120G RAM Cache 512M | +| Resp Time 12 Head Bytes 18M || Keep Alive 380 Conn Reuse 350 || Hit Rate 85% Bandwidth 850M | +| Body Bytes 750M Errors 15 || Head Bytes 15M Body Bytes 600M || Avg Resp 12ms Peak Req 25K | +| HTTP/1 Conn 300 HTTP/2 Con 300 || Errors 12 Other Err 5 || Errors/hr 50 Uptime % 99% | ++--------------------------------------++--------------------------------------++--------------------------------------+ ++------------- HTTP CODES -------------++------------ CACHE DETAIL ------------++--------------- SYSTEM ---------------+ +| 100 0 101 0 || Lookup Act 150 Lookup Suc 24K || Thread Cnt 32 Event Loop 16 | +| 200 78% 201 1% || Read Active 150 Read Succ 20K || Memory Use 2.5G Peak Mem 3G | +| 204 2% 206 5% || Write Act 45 Write Succ 8K || Open FDs 5K Max FDs 64K | +| 301 2% 302 1% || Update Act 10 Update Suc 500 || CPU User 45% CPU System 15% | +| 304 12% 307 0% || Delete Act 5 Delete Suc 100 || IO Read 850M IO Write 620M | ++--------------------------------------++--------------------------------------++--------------------------------------+ + 12:30:45 proxy.example.com [1/3] Overview q h 1-3 +``` + +## 160x40 Terminal (4 boxes) + +``` ++--------------- CACHE ----------------++--------------- CLIENT ---------------++--------------- ORIGIN ---------------++-------------- REQUESTS --------------+ +| Disk Used 120G Disk Total 500G || Requests 15K Connections 800 || Requests 12K Connections 400 || GET 12K POST 800 | +| RAM Used 512M RAM Total 1G || Current Con 500 Active Conn 450 || Current Con 200 Req/Conn 30 || HEAD 200 PUT 50 | +| Entries 50K Avg Size 45K || Req/Conn 19 Dynamic KA 400 || Connect Fai 5 Aborts 2 || DELETE 10 OPTIONS 25 | +| Lookups 25K Writes 8K || Avg Size 45K Net (Mb/s) 850 || Avg Size 52K Net (Mb/s) 620 || PURGE 5 PUSH 2 | +| Read Active 150 Write Act 45 || Resp Time 12 Head Bytes 18M || Keep Alive 380 Conn Reuse 350 || CONNECT 15 TRACE 0 | +| Updates 500 Deletes 100 || Body Bytes 750M Errors 15 || Head Bytes 15M Body Bytes 600M || Total Req 150M Req/sec 15K | ++--------------------------------------++--------------------------------------++--------------------------------------++--------------------------------------+ ++------------- HIT RATES --------------++------------ CONNECTIONS -------------++-------------- SSL/TLS ---------------++------------- RESPONSES --------------+ +| RAM Hit 85% Fresh 72% || HTTP/1 Clnt 200 HTTP/1 Orig 80 || SSL Success 450 SSL Fail 3 || 200 78% 206 5% | +| Revalidate 12% Cold 8% || HTTP/2 Clnt 300 HTTP/2 Orig 120 || SSL Session 450 SSL Handshk 120 || 301 2% 304 12% | +| Changed 3% Not Cached 2% || HTTP/3 Clnt 50 HTTP/3 Orig 20 || Session Hit 400 Session Mis 50 || 404 1% 502 0% | +| No Cache 3% Error 1% || Keep Alive 380 Conn Reuse 350 || TLS 1.2 200 TLS 1.3 250 || 503 0% 504 0% | +| Fresh Time 2ms Reval Time 15 || Throttled 5 Queued 2 || Client Cert 50 Origin SSL 380 || 2xx 83% 3xx 14% | +| Cold Time 45 Changed T 30 || Idle Timeou 10 Max Conns 5K || Renegotiate 10 Resumption 350 || 4xx 2% 5xx 1% | ++--------------------------------------++--------------------------------------++--------------------------------------++--------------------------------------+ ++------------- BANDWIDTH --------------++---------------- DNS -----------------++--------------- ERRORS ---------------++--------------- TOTALS ---------------+ +| Client Head 18M Client Bod 750M || DNS Lookups 800 DNS Hits 720 || Connect Fai 5 Aborts 2 || Total Req 150M Total Conn 5M | +| Server Head 15M Server Bod 600M || DNS Ratio 90% DNS Entry 500 || Client Abrt 15 Origin Err 12 || Total Bytes 50T Uptime 45d | +| Avg ReqSize 45K Avg Resp 52K || Pending 5 In Flight 12 || CacheRdErr 3 Cache Writ 1 || Cache Size 120G RAM Cache 512M | +| Net In Mbs 850 Net Out 620 || Expired 10 Evicted 25 || Timeout 20 Other Err 8 || Hit Rate 85% Bandwidth 850M | +| Head Bytes 33M Body Bytes 1G || Avg Lookup 2ms Max Lookup 45 || HTTP Err 10 Parse Err 2 || Avg Resp 12ms Peak Req 25K | +| Avg Latency 12ms Max Laten 450 || Failed 5 Retries 12 || DNS Fail 5 SSL Err 3 || Errors/hr 50 Uptime % 99% | ++--------------------------------------++--------------------------------------++--------------------------------------++--------------------------------------+ ++------------- HTTP CODES -------------++------------ CACHE DETAIL ------------++----------- ORIGIN DETAIL ------------++------------- MISC STATS -------------+ +| 100 0 101 0 || Lookup Act 150 Lookup Suc 24K || Req Active 50 Req Pending 12 || Thread Cnt 32 Event Loop 16 | +| 200 78% 201 1% || Read Active 150 Read Succ 20K || Conn Active 200 Conn Pend 25 || Memory Use 2.5G Peak Mem 3G | +| 204 2% 206 5% || Write Act 45 Write Succ 8K || DNS Pending 5 DNS Active 12 || Open FDs 5K Max FDs 64K | +| 301 2% 302 1% || Update Act 10 Update Suc 500 || SSL Active 50 SSL Pend 10 || CPU User 45% CPU System 15% | +| 304 12% 307 0% || Delete Act 5 Delete Suc 100 || Retry Queue 10 Retry Act 5 || IO Read 850M IO Write 620M | +| 400 1% 401 0% || Evacuate 5 Scan 2 || Timeout Que 5 Timeout Act 2 || Net Pkts 100K Dropped 50 | +| 403 0% 404 1% || Fragment 1 15K Fragment 2 3K || Error Queue 5 Error Act 2 || Ctx Switch 50K Interrupts 25K | +| 500 0% 502 0% || Fragment 3+ 500 Avg Frags 1.2 || Health Chk 100 Health OK 98 || GC Runs 100 GC Time 50ms | +| 503 0% 504 0% || Bytes Writ 50T Bytes Read 45T || Circuit Opn 0 Circuit Cls 5 || Log Writes 10K Log Bytes 500M | ++--------------------------------------++--------------------------------------++--------------------------------------++--------------------------------------+ ++------------- PROTOCOLS --------------++-------------- TIMEOUTS --------------++--------------- QUEUES ---------------++------------- RESOURCES --------------+ +| HTTP/1.0 50 HTTP/1.1 150 || Connect TO 10 Read TO 5 || Accept Queu 25 Active Q 50 || Threads Idl 16 Threads Bu 16 | +| HTTP/2 300 HTTP/3 50 || Write TO 3 DNS TO 2 || Pending Q 12 Retry Q 5 || Disk Free 380G Disk Used 120G | ++--------------------------------------++--------------------------------------++--------------------------------------++--------------------------------------+ + 12:30:45 proxy.example.com [1/2] Overview q h 1-2 +``` + +## Page Layouts + +Page count varies by terminal size due to available space: + +### 80x24 Terminal (6 Pages) +1. **Overview** - Cache, Reqs/Responses, Client, Origin +2. **Responses** - HTTP response code breakdown (1xx, 2xx, 3xx, 4xx, 5xx) +3. **Connections** - HTTP/1.x vs HTTP/2, keep-alive, bandwidth +4. **Cache** - Detailed cache statistics, hit rates, latency +5. **SSL/TLS** - SSL handshake stats, session cache, errors +6. **Errors** - Connection, transaction, cache, origin errors + +### 120x40 Terminal (3 Pages) +1. **Overview** - All major operational stats (shown above) +2. **Details** - HTTP codes, responses, SSL/TLS, DNS, errors combined +3. **System** - Cache detail, system resources, timeouts, totals + +### 160x40 Terminal (2 Pages) +1. **Overview** - All major operational stats (shown above) +2. **Details** - Deep dives into HTTP codes, cache internals, system + +## Status Bar + +The status bar appears on the last line and contains: +- Timestamp (HH:MM:SS) +- Connection status indicator +- Hostname +- Current page indicator [N/X] where X = 6, 3, or 2 based on terminal size +- Key hints (q h 1-X) + +## Color Scheme (Interactive Mode) + +- Box borders: Cyan, Blue, Magenta (alternating) +- Labels: Cyan +- Values: Color-coded by magnitude + - Grey: Zero or very small values + - Green: Normal values + - Cyan: Thousands (K suffix) + - Yellow: Millions (M suffix) + - Red: Billions (G suffix) +- Percentages: Green (>90%), Cyan (>70%), Yellow (>50%), Grey (<1%) + +## Notes + +- Values are formatted with SI suffixes (K, M, G, T) +- Percentages show as integer with % suffix +- Numbers are right-aligned, suffix follows immediately +- Values without suffix have trailing space for alignment +- Unicode box-drawing characters used by default +- Use -a flag for ASCII box characters (+, -, |) + +## Graph Page Layouts + +Graphs use Unicode block characters for visualization: +`▁▂▃▄▅▆▇█` (8 height levels from 0% to 100%) + +### Multi-Graph Box Format + +Title and graphs inside the box allow multiple metrics per box: + +``` +| LABEL ▂▁▁▂▁▁▁▂▃▄▃▂▃▃▂▂▃▂ VALUE | +``` + +### 40-char Multi-Graph Box + +``` ++--------------------------------------+ +| Bandwidth ▂▁▁▂▁▁▁▂▃▄▃▂▃▃▂▂▃▂ 850M | +| Hit Rate ▅▅▆▇▇██▇▇▆▅▄▃▂▂▁▁▂ 85% | +| Requests ▂▂▄▄▄▃▂▇▂▇▆▂▂▂▃▄▆▇ 15K | +| Connections ▁▁▂▂▃▃▄▄▅▅▅▆▆▇▇██ 800 | ++--------------------------------------+ +``` + +### 80x24 Graph Page (two 40-char boxes + 80-char box) + +``` ++-------------- NETWORK ---------------++------------- CACHE I/O --------------+ +| Net In ▂▁▁▂▁▁▁▂▃▄▃▂▃▃▂▂▃▂ 850M || Reads ▂▂▄▄▄▃▂▇▂▇▆▂▂▂▃▄▆▇ 25K | +| Net Out ▅▅▆▇▇██▇▇▆▅▄▃▂▂▁▁▂ 620M || Writes ▁▁▂▂▃▃▄▄▅▅▅▆▆▇▇██ 8K | ++--------------------------------------++--------------------------------------+ ++------------------------------ TRAFFIC OVERVIEW ------------------------------+ +| Bandwidth ▂▁▁▂▁▁▁▂▃▄▃▂▃▃▂▂▃▂▁▁▁▁▁▁▁▁▁▂▂▃▁▃▃▃▂▁▁▂▁▁▂▁▁▁▁▁▂▃▂▁▂▂▂▂ 850 Mb/s | +| Hit Rate ▅▅▆▇▇██▇▇▆▅▄▃▂▂▁▁▂▂▃▄▅▅▆▇███▇▇▆▅▄▃▂▂▁▁▂▂▃▄▅▆▆▇███▇▇▆▅▄ 85% | +| Requests ▂▂▄▄▄▃▂▇▂▇▆▂▂▂▃▄▆▇▂▇▃▇▆▄▆▇▄▁▃▆▅▄▃▃▅▂▂▅▂▅▅▇▄▂▆▇▃▅▂▇▄██▅ 15K/s | +| Connections ▁▁▁▁▁▁▂▂▂▂▂▂▂▃▃▃▃▃▃▃▄▄▄▄▄▄▅▅▅▅▅▅▅▆▆▆▆▆▆▆▇▇▇▇▇▇▇██████ 800 | ++------------------------------------------------------------------------------+ + 12:30:45 proxy.example.com [G/6] Graphs q h 1-6 +``` + +### 120x40 Graph Page (three 40-char boxes) + +``` ++-------------- NETWORK ---------------++--------------- CACHE ----------------++-------------- DISK I/O --------------+ +| Net In ▂▁▁▂▁▁▁▂▃▄▃▂▃▃▂▂▃▂ 850M || Hit Rate ▅▅▆▇▇██▇▇▆▅▄▃▂▂▁▁▂ 85% || Reads ▂▂▄▄▄▃▂▇▂▇▆▂▂▂▃▄▆▇ 25K | +| Net Out ▅▅▆▇▇██▇▇▆▅▄▃▂▂▁▁▂ 620M || Miss Rate ▂▁▁▂▁▁▁▂▃▄▃▂▃▃▂▂▃▂ 15% || Writes ▁▁▂▂▃▃▄▄▅▅▅▆▆▇▇██ 8K | ++--------------------------------------++--------------------------------------++--------------------------------------+ ++-------------- REQUESTS --------------++------------- RESPONSES --------------++------------ CONNECTIONS -------------+ +| Client ▂▁▁▂▁▁▁▂▃▄▃▂▃▃▂▂▃▂ 15K || 2xx ▅▅▆▇▇██▇▇▆▅▄▃▂▂▁▁▂ 83% || Client ▁▁▂▂▃▃▄▄▅▅▅▆▆▇▇██ 800 | +| Origin ▅▅▆▇▇██▇▇▆▅▄▃▂▂▁▁▂ 12K || 3xx ▂▁▁▂▁▁▁▂▃▄▃▂▃▃▂▂▃▂ 14% || Origin ▂▂▄▄▄▃▂▇▂▇▆▂▂▂▃▄▆▇ 400 | ++--------------------------------------++--------------------------------------++--------------------------------------+ ++-------------------------------------------- BANDWIDTH HISTORY (last 60s) --------------------------------------------+ +| In: ▂▁▁▂▁▁▁▂▃▄▃▂▃▃▂▂▃▂▁▁▁▁▁▁▁▁▁▂▂▃▁▃▃▃▂▁▁▂▁▁▂▁▁▁▁▁▂▃▂▁▂▂▂▂▂▂▃▂▂▂▁▁▂▂▂▂▃▃▃▃▄▅▅▄▄▅▅▆▆▆▆▆▇███████████▇█ 850M | +| Out: ▅▅▆▇▇██▇▇▆▅▄▃▂▂▁▁▂▂▃▄▅▅▆▇███▇▇▆▅▄▃▂▂▁▁▂▂▃▄▅▆▆▇███▇▇▆▅▄▃▂▂▁▁▂▂▃▄▅▆▆▇███▇▇▆▅▄▃▂▂▁▁▂▂▃▄▅▆▆▇███▇▆▆▅▄ 620M | ++----------------------------------------------------------------------------------------------------------------------+ + 12:30:45 proxy.example.com [G/3] Graphs q h 1-3 +``` + +### Block Character Reference + +| Height | Char | Description | +|--------|------|-------------| +| 0% | ` ` | Empty | +| 12.5% | `▁` | Lower 1/8 | +| 25% | `▂` | Lower 2/8 | +| 37.5% | `▃` | Lower 3/8 | +| 50% | `▄` | Lower 4/8 | +| 62.5% | `▅` | Lower 5/8 | +| 75% | `▆` | Lower 6/8 | +| 87.5% | `▇` | Lower 7/8 | +| 100% | `█` | Full block | + +### Graph Color Gradient + +In interactive mode, graph bars are colored by value: +- **Blue** (0-20%): Low values +- **Cyan** (20-40%): Below average +- **Green** (40-60%): Normal +- **Yellow** (60-80%): Above average +- **Red** (80-100%): High values + +Visual: `▁▂▃▄▅▆▇█` with gradient from blue to red diff --git a/src/traffic_top/Output.cc b/src/traffic_top/Output.cc new file mode 100644 index 00000000000..c35273e952f --- /dev/null +++ b/src/traffic_top/Output.cc @@ -0,0 +1,241 @@ +/** @file + + Output formatters implementation for traffic_top batch mode. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include "Output.h" + +#include +#include +#include +#include + +namespace traffic_top +{ + +Output::Output(OutputFormat format, FILE *output_file) : _format(format), _output(output_file) +{ + // Use default summary stats if none specified + if (_stat_keys.empty()) { + _stat_keys = getDefaultSummaryKeys(); + } +} + +std::vector +getDefaultSummaryKeys() +{ + return { + "client_req", // Requests per second + "ram_ratio", // RAM cache hit rate + "fresh", // Fresh hit % + "cold", // Cold miss % + "client_curr_conn", // Current connections + "disk_used", // Disk cache used + "client_net", // Client bandwidth + "server_req", // Origin requests/sec + "200", // 200 responses % + "5xx" // 5xx errors % + }; +} + +std::vector +getAllStatKeys(Stats &stats) +{ + return stats.getStatKeys(); +} + +std::string +Output::getCurrentTimestamp() const +{ + time_t now = time(nullptr); + struct tm nowtm; + char buf[32]; + + localtime_r(&now, &nowtm); + strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &nowtm); + return std::string(buf); +} + +std::string +Output::formatValue(double value, StatType type) const +{ + std::ostringstream oss; + + if (isPercentage(type)) { + oss << std::fixed << std::setprecision(1) << value; + } else if (value >= 1000000000000.0) { + oss << std::fixed << std::setprecision(1) << (value / 1000000000000.0) << "T"; + } else if (value >= 1000000000.0) { + oss << std::fixed << std::setprecision(1) << (value / 1000000000.0) << "G"; + } else if (value >= 1000000.0) { + oss << std::fixed << std::setprecision(1) << (value / 1000000.0) << "M"; + } else if (value >= 1000.0) { + oss << std::fixed << std::setprecision(1) << (value / 1000.0) << "K"; + } else { + oss << std::fixed << std::setprecision(1) << value; + } + + return oss.str(); +} + +void +Output::printHeader() +{ + if (_format == OutputFormat::Text && _print_header && !_header_printed) { + printTextHeader(); + _header_printed = true; + } +} + +void +Output::printTextHeader() +{ + // Print column headers + if (_include_timestamp) { + fprintf(_output, "%-20s", "TIMESTAMP"); + } + + for (const auto &key : _stat_keys) { + // Get pretty name from stats (we need a Stats instance for this) + // For header, use the key name abbreviated + std::string header = key; + if (header.length() > 10) { + header = header.substr(0, 9) + "."; + } + fprintf(_output, "%12s", header.c_str()); + } + fprintf(_output, "\n"); + + // Print separator line + if (_include_timestamp) { + fprintf(_output, "--------------------"); + } + for (size_t i = 0; i < _stat_keys.size(); ++i) { + fprintf(_output, "------------"); + } + fprintf(_output, "\n"); + + fflush(_output); +} + +void +Output::printStats(Stats &stats) +{ + if (_format == OutputFormat::Text) { + printHeader(); + printTextStats(stats); + } else { + printJsonStats(stats); + } +} + +void +Output::printTextStats(Stats &stats) +{ + // Timestamp + if (_include_timestamp) { + fprintf(_output, "%-20s", getCurrentTimestamp().c_str()); + } + + // Values + for (const auto &key : _stat_keys) { + double value = 0; + std::string prettyName; + StatType type; + + if (stats.hasStat(key)) { + stats.getStat(key, value, prettyName, type); + std::string formatted = formatValue(value, type); + + if (isPercentage(type)) { + fprintf(_output, "%11s%%", formatted.c_str()); + } else { + fprintf(_output, "%12s", formatted.c_str()); + } + } else { + fprintf(_output, "%12s", "N/A"); + } + } + + fprintf(_output, "\n"); + fflush(_output); +} + +void +Output::printJsonStats(Stats &stats) +{ + fprintf(_output, "{"); + + bool first = true; + + // Timestamp + if (_include_timestamp) { + fprintf(_output, "\"timestamp\":\"%s\"", getCurrentTimestamp().c_str()); + first = false; + } + + // Host + if (!first) { + fprintf(_output, ","); + } + fprintf(_output, "\"host\":\"%s\"", stats.getHost().c_str()); + first = false; + + // Stats values + for (const auto &key : _stat_keys) { + double value = 0; + std::string prettyName; + StatType type; + + if (stats.hasStat(key)) { + stats.getStat(key, value, prettyName, type); + + if (!first) { + fprintf(_output, ","); + } + + // Use key name as JSON field + // Check for NaN or Inf + if (std::isnan(value) || std::isinf(value)) { + fprintf(_output, "\"%s\":null", key.c_str()); + } else { + fprintf(_output, "\"%s\":%.2f", key.c_str(), value); + } + first = false; + } + } + + fprintf(_output, "}\n"); + fflush(_output); +} + +void +Output::printError(const std::string &message) +{ + if (_format == OutputFormat::Json) { + fprintf(_output, "{\"error\":\"%s\",\"timestamp\":\"%s\"}\n", message.c_str(), getCurrentTimestamp().c_str()); + } else { + fprintf(stderr, "Error: %s\n", message.c_str()); + } + fflush(_output); +} + +} // namespace traffic_top diff --git a/src/traffic_top/Output.h b/src/traffic_top/Output.h new file mode 100644 index 00000000000..257744f7461 --- /dev/null +++ b/src/traffic_top/Output.h @@ -0,0 +1,139 @@ +/** @file + + Output formatters for traffic_top batch mode. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#pragma once + +#include +#include +#include + +#include "Stats.h" + +namespace traffic_top +{ + +/// Output format types +enum class OutputFormat { Text, Json }; + +/** + * Output formatter for batch mode. + * + * Supports vmstat-style text output and JSON output for + * machine consumption. + */ +class Output +{ +public: + /** + * Constructor. + * @param format Output format (text or JSON) + * @param output_file File handle to write to (defaults to stdout) + */ + explicit Output(OutputFormat format, FILE *output_file = stdout); + ~Output() = default; + + // Non-copyable + Output(const Output &) = delete; + Output &operator=(const Output &) = delete; + + /** + * Set custom stat keys to output. + * If not set, uses default summary stats. + */ + void + setStatKeys(const std::vector &keys) + { + _stat_keys = keys; + } + + /** + * Print the header line (for text format). + * Called once before the first data line. + */ + void printHeader(); + + /** + * Print a data line with current stats. + * @param stats Stats object with current values + */ + void printStats(Stats &stats); + + /** + * Print an error message. + * @param message Error message to print + */ + void printError(const std::string &message); + + /** + * Set whether to include timestamp in output. + */ + void + setIncludeTimestamp(bool include) + { + _include_timestamp = include; + } + + /** + * Set whether to print header. + */ + void + setPrintHeader(bool print) + { + _print_header = print; + } + + /** + * Get the output format. + */ + OutputFormat + getFormat() const + { + return _format; + } + +private: + void printTextHeader(); + void printTextStats(Stats &stats); + void printJsonStats(Stats &stats); + + std::string formatValue(double value, StatType type) const; + std::string getCurrentTimestamp() const; + + OutputFormat _format; + FILE *_output; + std::vector _stat_keys; + bool _include_timestamp = true; + bool _print_header = true; + bool _header_printed = false; +}; + +/** + * Get default stat keys for summary output. + */ +std::vector getDefaultSummaryKeys(); + +/** + * Get all stat keys for full output. + */ +std::vector getAllStatKeys(Stats &stats); + +} // namespace traffic_top diff --git a/src/traffic_top/README b/src/traffic_top/README index 10f33b70e45..22255f1ab9c 100644 --- a/src/traffic_top/README +++ b/src/traffic_top/README @@ -1,4 +1,99 @@ -Top type program for Apache Traffic Server that displays common -statistical information about the server. Requires the server to be -running the stats_over_http plugin. +traffic_top - Real-time Statistics Monitor for Apache Traffic Server +==================================================================== +A top-like program for Apache Traffic Server that displays real-time +statistical information about the proxy server. + +REQUIREMENTS +------------ +- Running traffic_server instance +- Access to the ATS RPC socket (typically requires running as the + traffic_server user or root) +- POSIX-compatible terminal (for interactive mode) + +USAGE +----- +Interactive mode (default): + traffic_top [options] + +Batch mode: + traffic_top -b [options] + +OPTIONS +------- + -s, --sleep SECONDS Delay between updates (default: 5) + -c, --count N Number of iterations (default: infinite in + interactive, 1 in batch) + -b, --batch Batch mode (non-interactive output) + -o, --output FILE Output file for batch mode (default: stdout) + -j, --json Output in JSON format (batch mode only) + -a, --ascii Use ASCII characters instead of Unicode boxes + -h, --help Show help message + -V, --version Show version information + +INTERACTIVE MODE +---------------- +Navigation: + 1-6 Switch between pages + Left/Right Previous/Next page + h, ? Show help + a Toggle absolute/rate display + q Quit + +Pages: + 1 - Overview Cache, client, and server summary + 2 - Responses HTTP response code breakdown + 3 - Connections HTTP/1.x vs HTTP/2, keep-alive stats + 4 - Cache Detailed cache statistics + 5 - SSL/TLS SSL handshake and session stats + 6 - Errors Error breakdown by type + +BATCH MODE +---------- +Batch mode outputs statistics in a format suitable for scripting +and monitoring systems. + +Text format (default): + traffic_top -b -c 10 -s 5 + + Outputs vmstat-style columnar data every 5 seconds for 10 iterations. + +JSON format: + traffic_top -b -j -c 0 + + Outputs JSON objects (one per line) continuously. + +Example output (text): + TIMESTAMP client_req ram_ratio fresh cold ... + -------------------- ---------- ---------- ---------- ---------- + 2024-01-15T10:30:00 1523.0 85.2% 72.1% 15.3% + +Example output (JSON): + {"timestamp":"2024-01-15T10:30:00","host":"proxy1","client_req":1523.0,...} + +TROUBLESHOOTING +--------------- +"Permission denied accessing RPC socket" + - Ensure you have permission to access the ATS runtime directory + - Run as the traffic_server user or with sudo + +"Cannot connect to ATS - is traffic_server running?" + - Verify traffic_server is running: traffic_ctl server status + - Check the RPC socket exists in the ATS runtime directory + +"No data displayed" + - Wait a few seconds for rate calculations to have baseline data + - Use -a flag to show absolute values instead of rates + +FILES +----- +Source files in src/traffic_top/: + traffic_top.cc - Main entry point and argument parsing + Stats.cc/h - Statistics collection via RPC + StatType.h - Stat type enumeration + Display.cc/h - Curses UI rendering + Output.cc/h - Batch output formatters + +SEE ALSO +-------- +traffic_ctl(8), traffic_server(8) diff --git a/src/traffic_top/StatType.h b/src/traffic_top/StatType.h new file mode 100644 index 00000000000..bf957af525c --- /dev/null +++ b/src/traffic_top/StatType.h @@ -0,0 +1,73 @@ +/** @file + + StatType enum for traffic_top statistics. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#pragma once + +namespace traffic_top +{ + +/** + * Enumeration of statistic types used for display and calculation. + * + * Each type determines how a statistic value is fetched, calculated, and displayed. + */ +enum class StatType { + Absolute = 1, ///< Absolute value, displayed as-is (e.g., disk used, current connections) + Rate = 2, ///< Rate per second, calculated from delta over time interval + Ratio = 3, ///< Ratio of two stats (numerator / denominator) + Percentage = 4, ///< Percentage (ratio * 100, displayed with % suffix) + RequestPct = 5, ///< Percentage of client requests (value / client_req * 100) + Sum = 6, ///< Sum of two rate stats + SumBits = 7, ///< Sum of two rate stats * 8 (bytes to bits conversion) + TimeRatio = 8, ///< Time ratio in milliseconds (totaltime / count) + SumAbsolute = 9, ///< Sum of two absolute stats + RateNsToMs = 10 ///< Rate in nanoseconds, converted to milliseconds (divide by 1,000,000) +}; + +/** + * Convert StatType enum to its underlying integer value. + */ +inline int +toInt(StatType type) +{ + return static_cast(type); +} + +/** + * Check if this stat type represents a percentage value. + */ +inline bool +isPercentage(StatType type) +{ + return type == StatType::Percentage || type == StatType::RequestPct; +} + +/** + * Check if this stat type needs the previous stats for rate calculation. + */ +inline bool +needsPreviousStats(StatType type) +{ + return type == StatType::Rate || type == StatType::RequestPct || type == StatType::TimeRatio || type == StatType::RateNsToMs; +} + +} // namespace traffic_top diff --git a/src/traffic_top/Stats.cc b/src/traffic_top/Stats.cc new file mode 100644 index 00000000000..97d69918812 --- /dev/null +++ b/src/traffic_top/Stats.cc @@ -0,0 +1,779 @@ +/** @file + + Stats class implementation for traffic_top. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include "Stats.h" + +#include +#include +#include +#include +#include +#include + +#include "shared/rpc/RPCRequests.h" +#include "shared/rpc/RPCClient.h" +#include "shared/rpc/yaml_codecs.h" + +namespace traffic_top +{ + +namespace +{ + // RPC communication constants + constexpr int RPC_TIMEOUT_MS = 1000; // Timeout for RPC calls in milliseconds + constexpr int RPC_RETRY_COUNT = 10; // Number of retries for RPC calls + + /// Convenience class for creating metric lookup requests + struct MetricParam : shared::rpc::RecordLookupRequest::Params { + explicit MetricParam(std::string name) + : shared::rpc::RecordLookupRequest::Params{std::move(name), shared::rpc::NOT_REGEX, shared::rpc::METRIC_REC_TYPES} + { + } + }; +} // namespace + +Stats::Stats() +{ + char hostname[256]; + hostname[sizeof(hostname) - 1] = '\0'; + if (gethostname(hostname, sizeof(hostname) - 1) == 0) { + _host = hostname; + } else { + _host = "localhost"; + } + + initializeLookupTable(); + + // Validate lookup table in debug builds +#ifndef NDEBUG + int validation_errors = validateLookupTable(); + if (validation_errors > 0) { + fprintf(stderr, "WARNING: Found %d stat lookup table validation errors\n", validation_errors); + } +#endif +} + +void +Stats::initializeLookupTable() +{ + // Version + _lookup_table.emplace("version", LookupItem("Version", "proxy.process.version.server.short", StatType::Absolute)); + + // Cache storage stats + _lookup_table.emplace("disk_used", LookupItem("Disk Used", "proxy.process.cache.bytes_used", StatType::Absolute)); + _lookup_table.emplace("disk_total", LookupItem("Disk Total", "proxy.process.cache.bytes_total", StatType::Absolute)); + _lookup_table.emplace("ram_used", LookupItem("RAM Used", "proxy.process.cache.ram_cache.bytes_used", StatType::Absolute)); + _lookup_table.emplace("ram_total", LookupItem("RAM Total", "proxy.process.cache.ram_cache.total_bytes", StatType::Absolute)); + + // Cache operations + _lookup_table.emplace("lookups", LookupItem("Lookups", "proxy.process.http.cache_lookups", StatType::Rate)); + _lookup_table.emplace("cache_writes", LookupItem("Writes", "proxy.process.http.cache_writes", StatType::Rate)); + _lookup_table.emplace("cache_updates", LookupItem("Updates", "proxy.process.http.cache_updates", StatType::Rate)); + _lookup_table.emplace("cache_deletes", LookupItem("Deletes", "proxy.process.http.cache_deletes", StatType::Rate)); + _lookup_table.emplace("read_active", LookupItem("Read Act", "proxy.process.cache.read.active", StatType::Absolute)); + _lookup_table.emplace("write_active", LookupItem("Write Act", "proxy.process.cache.write.active", StatType::Absolute)); + _lookup_table.emplace("update_active", LookupItem("Update Act", "proxy.process.cache.update.active", StatType::Absolute)); + _lookup_table.emplace("entries", LookupItem("Entries", "proxy.process.cache.direntries.used", StatType::Absolute)); + _lookup_table.emplace("avg_size", LookupItem("Avg Size", "disk_used", "entries", StatType::Ratio)); + + // DNS stats + _lookup_table.emplace("dns_entry", LookupItem("DNS Entry", "proxy.process.hostdb.cache.current_items", StatType::Absolute)); + _lookup_table.emplace("dns_hits", LookupItem("DNS Hits", "proxy.process.hostdb.total_hits", StatType::Rate)); + _lookup_table.emplace("dns_lookups", LookupItem("DNS Lookups", "proxy.process.hostdb.total_lookups", StatType::Rate)); + _lookup_table.emplace("dns_serve_stale", LookupItem("DNS Stale", "proxy.process.hostdb.total_serve_stale", StatType::Rate)); + _lookup_table.emplace("dns_ratio", LookupItem("DNS Ratio", "dns_hits", "dns_lookups", StatType::Percentage)); + _lookup_table.emplace("dns_in_flight", LookupItem("DNS InFlight", "proxy.process.dns.in_flight", StatType::Absolute)); + _lookup_table.emplace("dns_success", LookupItem("DNS Success", "proxy.process.dns.lookup_successes", StatType::Rate)); + _lookup_table.emplace("dns_fail", LookupItem("DNS Fail", "proxy.process.dns.lookup_failures", StatType::Rate)); + _lookup_table.emplace("dns_lookup_time", LookupItem("DNS Time", "proxy.process.dns.lookup_time", StatType::Absolute)); + _lookup_table.emplace("dns_success_time", LookupItem("DNS Succ Time", "proxy.process.dns.success_time", StatType::Absolute)); + _lookup_table.emplace("dns_total", LookupItem("DNS Total", "proxy.process.dns.total_dns_lookups", StatType::Rate)); + _lookup_table.emplace("dns_retries", LookupItem("DNS Retries", "proxy.process.dns.retries", StatType::Rate)); + + // Client connections - HTTP/1.x and HTTP/2 + _lookup_table.emplace("client_req", LookupItem("Requests", "proxy.process.http.incoming_requests", StatType::Rate)); + _lookup_table.emplace("client_conn_h1", + LookupItem("New Conn HTTP/1.x", "proxy.process.http.total_client_connections", StatType::Rate)); + _lookup_table.emplace("client_conn_h2", + LookupItem("New Conn HTTP/2", "proxy.process.http2.total_client_connections", StatType::Rate)); + _lookup_table.emplace("client_conn", LookupItem("New Conn", "client_conn_h1", "client_conn_h2", StatType::Sum)); + _lookup_table.emplace("client_req_conn", LookupItem("Req/Conn", "client_req", "client_conn", StatType::Ratio)); + + // Current client connections + _lookup_table.emplace("client_curr_conn_h1", + LookupItem("Curr H1", "proxy.process.http.current_client_connections", StatType::Absolute)); + _lookup_table.emplace("client_curr_conn_h2", + LookupItem("Curr H2", "proxy.process.http2.current_client_connections", StatType::Absolute)); + _lookup_table.emplace("client_curr_conn", + LookupItem("Current Conn", "client_curr_conn_h1", "client_curr_conn_h2", StatType::SumAbsolute)); + + // Active client connections + _lookup_table.emplace("client_actv_conn_h1", + LookupItem("Active H1", "proxy.process.http.current_active_client_connections", StatType::Absolute)); + _lookup_table.emplace("client_actv_conn_h2", + LookupItem("Active H2", "proxy.process.http2.current_active_client_connections", StatType::Absolute)); + _lookup_table.emplace("client_actv_conn", + LookupItem("Active Conn", "client_actv_conn_h1", "client_actv_conn_h2", StatType::SumAbsolute)); + + // Server connections + _lookup_table.emplace("server_req", LookupItem("Requests", "proxy.process.http.outgoing_requests", StatType::Rate)); + _lookup_table.emplace("server_conn", LookupItem("New Conn", "proxy.process.http.total_server_connections", StatType::Rate)); + _lookup_table.emplace("server_req_conn", LookupItem("Req/Conn", "server_req", "server_conn", StatType::Ratio)); + _lookup_table.emplace("server_curr_conn", + LookupItem("Current Conn", "proxy.process.http.current_server_connections", StatType::Absolute)); + + // Bandwidth stats + _lookup_table.emplace("client_head", + LookupItem("Header Byte", "proxy.process.http.user_agent_response_header_total_size", StatType::Rate)); + _lookup_table.emplace("client_body", + LookupItem("Body Bytes", "proxy.process.http.user_agent_response_document_total_size", StatType::Rate)); + _lookup_table.emplace("server_head", + LookupItem("Header Byte", "proxy.process.http.origin_server_response_header_total_size", StatType::Rate)); + _lookup_table.emplace("server_body", + LookupItem("Body Bytes", "proxy.process.http.origin_server_response_document_total_size", StatType::Rate)); + + // RAM cache hits/misses + _lookup_table.emplace("ram_hit", LookupItem("RAM Hits", "proxy.process.cache.ram_cache.hits", StatType::Rate)); + _lookup_table.emplace("ram_miss", LookupItem("RAM Misses", "proxy.process.cache.ram_cache.misses", StatType::Rate)); + _lookup_table.emplace("ram_hit_miss", LookupItem("RAM Hit+Miss", "ram_hit", "ram_miss", StatType::Sum)); + _lookup_table.emplace("ram_ratio", LookupItem("RAM Hit", "ram_hit", "ram_hit_miss", StatType::Percentage)); + + // Keep-alive stats + _lookup_table.emplace("ka_total", + LookupItem("KA Total", "proxy.process.net.dynamic_keep_alive_timeout_in_total", StatType::Rate)); + _lookup_table.emplace("ka_count", + LookupItem("KA Count", "proxy.process.net.dynamic_keep_alive_timeout_in_count", StatType::Rate)); + _lookup_table.emplace("client_dyn_ka", LookupItem("Dynamic KA", "ka_total", "ka_count", StatType::Ratio)); + + // Error stats + _lookup_table.emplace("client_abort", LookupItem("Cli Abort", "proxy.process.http.err_client_abort_count", StatType::Rate)); + _lookup_table.emplace("conn_fail", LookupItem("Conn Failed", "proxy.process.http.err_connect_fail_count", StatType::Rate)); + _lookup_table.emplace("abort", LookupItem("Aborts", "proxy.process.http.transaction_counts.errors.aborts", StatType::Rate)); + _lookup_table.emplace("t_conn_fail", + LookupItem("Conn Fail", "proxy.process.http.transaction_counts.errors.connect_failed", StatType::Rate)); + _lookup_table.emplace("other_err", LookupItem("Other Err", "proxy.process.http.transaction_counts.errors.other", StatType::Rate)); + + // Cache hit/miss breakdown (percentage of requests) + _lookup_table.emplace("fresh", LookupItem("Fresh", "proxy.process.http.transaction_counts.hit_fresh", StatType::RequestPct)); + _lookup_table.emplace("reval", + LookupItem("Revalidate", "proxy.process.http.transaction_counts.hit_revalidated", StatType::RequestPct)); + _lookup_table.emplace("cold", LookupItem("Cold Miss", "proxy.process.http.transaction_counts.miss_cold", StatType::RequestPct)); + _lookup_table.emplace("changed", + LookupItem("Changed", "proxy.process.http.transaction_counts.miss_changed", StatType::RequestPct)); + _lookup_table.emplace("not", + LookupItem("Not Cached", "proxy.process.http.transaction_counts.miss_not_cacheable", StatType::RequestPct)); + _lookup_table.emplace("no", + LookupItem("No Cache", "proxy.process.http.transaction_counts.miss_client_no_cache", StatType::RequestPct)); + + // Transaction times + _lookup_table.emplace( + "fresh_time", LookupItem("Fresh (ms)", "proxy.process.http.transaction_totaltime.hit_fresh", "fresh", StatType::TimeRatio)); + _lookup_table.emplace("reval_time", LookupItem("Reval (ms)", "proxy.process.http.transaction_totaltime.hit_revalidated", "reval", + StatType::TimeRatio)); + _lookup_table.emplace("cold_time", + LookupItem("Cold (ms)", "proxy.process.http.transaction_totaltime.miss_cold", "cold", StatType::TimeRatio)); + _lookup_table.emplace("changed_time", LookupItem("Chg (ms)", "proxy.process.http.transaction_totaltime.miss_changed", "changed", + StatType::TimeRatio)); + _lookup_table.emplace("not_time", LookupItem("NotCch (ms)", "proxy.process.http.transaction_totaltime.miss_not_cacheable", "not", + StatType::TimeRatio)); + _lookup_table.emplace("no_time", LookupItem("NoCch (ms)", "proxy.process.http.transaction_totaltime.miss_client_no_cache", "no", + StatType::TimeRatio)); + + // HTTP methods (percentage of requests) + _lookup_table.emplace("get", LookupItem("GET", "proxy.process.http.get_requests", StatType::RequestPct)); + _lookup_table.emplace("head", LookupItem("HEAD", "proxy.process.http.head_requests", StatType::RequestPct)); + _lookup_table.emplace("post", LookupItem("POST", "proxy.process.http.post_requests", StatType::RequestPct)); + _lookup_table.emplace("put", LookupItem("PUT", "proxy.process.http.put_requests", StatType::RequestPct)); + _lookup_table.emplace("delete", LookupItem("DELETE", "proxy.process.http.delete_requests", StatType::RequestPct)); + _lookup_table.emplace("options", LookupItem("OPTIONS", "proxy.process.http.options_requests", StatType::RequestPct)); + + // HTTP response codes (percentage of requests) + _lookup_table.emplace("100", LookupItem("100", "proxy.process.http.100_responses", StatType::RequestPct)); + _lookup_table.emplace("101", LookupItem("101", "proxy.process.http.101_responses", StatType::RequestPct)); + _lookup_table.emplace("1xx", LookupItem("1xx", "proxy.process.http.1xx_responses", StatType::RequestPct)); + _lookup_table.emplace("200", LookupItem("200", "proxy.process.http.200_responses", StatType::RequestPct)); + _lookup_table.emplace("201", LookupItem("201", "proxy.process.http.201_responses", StatType::RequestPct)); + _lookup_table.emplace("202", LookupItem("202", "proxy.process.http.202_responses", StatType::RequestPct)); + _lookup_table.emplace("203", LookupItem("203", "proxy.process.http.203_responses", StatType::RequestPct)); + _lookup_table.emplace("204", LookupItem("204", "proxy.process.http.204_responses", StatType::RequestPct)); + _lookup_table.emplace("205", LookupItem("205", "proxy.process.http.205_responses", StatType::RequestPct)); + _lookup_table.emplace("206", LookupItem("206", "proxy.process.http.206_responses", StatType::RequestPct)); + _lookup_table.emplace("2xx", LookupItem("2xx", "proxy.process.http.2xx_responses", StatType::RequestPct)); + _lookup_table.emplace("300", LookupItem("300", "proxy.process.http.300_responses", StatType::RequestPct)); + _lookup_table.emplace("301", LookupItem("301", "proxy.process.http.301_responses", StatType::RequestPct)); + _lookup_table.emplace("302", LookupItem("302", "proxy.process.http.302_responses", StatType::RequestPct)); + _lookup_table.emplace("303", LookupItem("303", "proxy.process.http.303_responses", StatType::RequestPct)); + _lookup_table.emplace("304", LookupItem("304", "proxy.process.http.304_responses", StatType::RequestPct)); + _lookup_table.emplace("305", LookupItem("305", "proxy.process.http.305_responses", StatType::RequestPct)); + _lookup_table.emplace("307", LookupItem("307", "proxy.process.http.307_responses", StatType::RequestPct)); + _lookup_table.emplace("3xx", LookupItem("3xx", "proxy.process.http.3xx_responses", StatType::RequestPct)); + _lookup_table.emplace("400", LookupItem("400", "proxy.process.http.400_responses", StatType::RequestPct)); + _lookup_table.emplace("401", LookupItem("401", "proxy.process.http.401_responses", StatType::RequestPct)); + _lookup_table.emplace("402", LookupItem("402", "proxy.process.http.402_responses", StatType::RequestPct)); + _lookup_table.emplace("403", LookupItem("403", "proxy.process.http.403_responses", StatType::RequestPct)); + _lookup_table.emplace("404", LookupItem("404", "proxy.process.http.404_responses", StatType::RequestPct)); + _lookup_table.emplace("405", LookupItem("405", "proxy.process.http.405_responses", StatType::RequestPct)); + _lookup_table.emplace("406", LookupItem("406", "proxy.process.http.406_responses", StatType::RequestPct)); + _lookup_table.emplace("407", LookupItem("407", "proxy.process.http.407_responses", StatType::RequestPct)); + _lookup_table.emplace("408", LookupItem("408", "proxy.process.http.408_responses", StatType::RequestPct)); + _lookup_table.emplace("409", LookupItem("409", "proxy.process.http.409_responses", StatType::RequestPct)); + _lookup_table.emplace("410", LookupItem("410", "proxy.process.http.410_responses", StatType::RequestPct)); + _lookup_table.emplace("411", LookupItem("411", "proxy.process.http.411_responses", StatType::RequestPct)); + _lookup_table.emplace("412", LookupItem("412", "proxy.process.http.412_responses", StatType::RequestPct)); + _lookup_table.emplace("413", LookupItem("413", "proxy.process.http.413_responses", StatType::RequestPct)); + _lookup_table.emplace("414", LookupItem("414", "proxy.process.http.414_responses", StatType::RequestPct)); + _lookup_table.emplace("415", LookupItem("415", "proxy.process.http.415_responses", StatType::RequestPct)); + _lookup_table.emplace("416", LookupItem("416", "proxy.process.http.416_responses", StatType::RequestPct)); + _lookup_table.emplace("4xx", LookupItem("4xx", "proxy.process.http.4xx_responses", StatType::RequestPct)); + _lookup_table.emplace("500", LookupItem("500", "proxy.process.http.500_responses", StatType::RequestPct)); + _lookup_table.emplace("501", LookupItem("501", "proxy.process.http.501_responses", StatType::RequestPct)); + _lookup_table.emplace("502", LookupItem("502", "proxy.process.http.502_responses", StatType::RequestPct)); + _lookup_table.emplace("503", LookupItem("503", "proxy.process.http.503_responses", StatType::RequestPct)); + _lookup_table.emplace("504", LookupItem("504", "proxy.process.http.504_responses", StatType::RequestPct)); + _lookup_table.emplace("505", LookupItem("505", "proxy.process.http.505_responses", StatType::RequestPct)); + _lookup_table.emplace("5xx", LookupItem("5xx", "proxy.process.http.5xx_responses", StatType::RequestPct)); + + // Derived bandwidth stats + _lookup_table.emplace("client_net", LookupItem("Net (Mb/s)", "client_head", "client_body", StatType::SumBits)); + _lookup_table.emplace("client_size", LookupItem("Total Size", "client_head", "client_body", StatType::Sum)); + _lookup_table.emplace("client_avg_size", LookupItem("Avg Size", "client_size", "client_req", StatType::Ratio)); + _lookup_table.emplace("server_net", LookupItem("Net (Mb/s)", "server_head", "server_body", StatType::SumBits)); + _lookup_table.emplace("server_size", LookupItem("Total Size", "server_head", "server_body", StatType::Sum)); + _lookup_table.emplace("server_avg_size", LookupItem("Avg Size", "server_size", "server_req", StatType::Ratio)); + + // Total transaction time + _lookup_table.emplace("total_time", LookupItem("Total Time", "proxy.process.http.total_transactions_time", StatType::Rate)); + _lookup_table.emplace("client_req_time", LookupItem("Resp Time", "total_time", "client_req", StatType::Ratio)); + + // SSL/TLS stats + _lookup_table.emplace("ssl_handshake_success", + LookupItem("SSL Handshk", "proxy.process.ssl.total_success_handshake_count_in", StatType::Rate)); + _lookup_table.emplace("ssl_handshake_fail", LookupItem("SSL HS Fail", "proxy.process.ssl.ssl_error_ssl", StatType::Rate)); + _lookup_table.emplace("ssl_session_hit", LookupItem("SSL Sess Hit", "proxy.process.ssl.ssl_session_cache_hit", StatType::Rate)); + _lookup_table.emplace("ssl_session_miss", + LookupItem("SSL Sess Miss", "proxy.process.ssl.ssl_session_cache_miss", StatType::Rate)); + _lookup_table.emplace("ssl_curr_sessions", + LookupItem("SSL Current Sessions", "proxy.process.ssl.user_agent_sessions", StatType::Absolute)); + + // Extended SSL/TLS handshake stats + _lookup_table.emplace("ssl_attempts_in", + LookupItem("Handshake Attempts In", "proxy.process.ssl.total_attempts_handshake_count_in", StatType::Rate)); + _lookup_table.emplace("ssl_attempts_out", LookupItem("Handshake Attempts Out", + "proxy.process.ssl.total_attempts_handshake_count_out", StatType::Rate)); + _lookup_table.emplace("ssl_success_in", + LookupItem("Handshake Success In", "proxy.process.ssl.total_success_handshake_count_in", StatType::Rate)); + _lookup_table.emplace("ssl_success_out", + LookupItem("Handshake Success Out", "proxy.process.ssl.total_success_handshake_count_out", StatType::Rate)); + _lookup_table.emplace("ssl_handshake_time", + LookupItem("Handshake Time", "proxy.process.ssl.total_handshake_time", StatType::Rate)); + + // SSL session stats + _lookup_table.emplace("ssl_sess_new", + LookupItem("Session New", "proxy.process.ssl.ssl_session_cache_new_session", StatType::Rate)); + _lookup_table.emplace("ssl_sess_evict", + LookupItem("Session Eviction", "proxy.process.ssl.ssl_session_cache_eviction", StatType::Rate)); + _lookup_table.emplace("ssl_origin_reused", + LookupItem("Origin Sess Reused", "proxy.process.ssl.origin_session_reused", StatType::Rate)); + + // SSL/TLS origin errors + _lookup_table.emplace("ssl_origin_bad_cert", LookupItem("Bad Cert", "proxy.process.ssl.origin_server_bad_cert", StatType::Rate)); + _lookup_table.emplace("ssl_origin_expired", + LookupItem("Cert Expired", "proxy.process.ssl.origin_server_expired_cert", StatType::Rate)); + _lookup_table.emplace("ssl_origin_revoked", + LookupItem("Cert Revoked", "proxy.process.ssl.origin_server_revoked_cert", StatType::Rate)); + _lookup_table.emplace("ssl_origin_unknown_ca", + LookupItem("Unknown CA", "proxy.process.ssl.origin_server_unknown_ca", StatType::Rate)); + _lookup_table.emplace("ssl_origin_verify_fail", + LookupItem("Verify Failed", "proxy.process.ssl.origin_server_cert_verify_failed", StatType::Rate)); + _lookup_table.emplace("ssl_origin_decrypt_fail", + LookupItem("Decrypt Failed", "proxy.process.ssl.origin_server_decryption_failed", StatType::Rate)); + _lookup_table.emplace("ssl_origin_wrong_ver", + LookupItem("Wrong Version", "proxy.process.ssl.origin_server_wrong_version", StatType::Rate)); + _lookup_table.emplace("ssl_origin_other", + LookupItem("Other Errors", "proxy.process.ssl.origin_server_other_errors", StatType::Rate)); + + // SSL/TLS client errors + _lookup_table.emplace("ssl_client_bad_cert", + LookupItem("Client Bad Cert", "proxy.process.ssl.user_agent_bad_cert", StatType::Rate)); + + // SSL general errors + _lookup_table.emplace("ssl_error_ssl", LookupItem("SSL Error", "proxy.process.ssl.ssl_error_ssl", StatType::Rate)); + _lookup_table.emplace("ssl_error_syscall", LookupItem("Syscall Error", "proxy.process.ssl.ssl_error_syscall", StatType::Rate)); + _lookup_table.emplace("ssl_error_async", LookupItem("Async Error", "proxy.process.ssl.ssl_error_async", StatType::Rate)); + + // TLS version stats + _lookup_table.emplace("tls_v10", LookupItem("TLSv1.0", "proxy.process.ssl.ssl_total_tlsv1", StatType::Rate)); + _lookup_table.emplace("tls_v11", LookupItem("TLSv1.1", "proxy.process.ssl.ssl_total_tlsv11", StatType::Rate)); + _lookup_table.emplace("tls_v12", LookupItem("TLSv1.2", "proxy.process.ssl.ssl_total_tlsv12", StatType::Rate)); + _lookup_table.emplace("tls_v13", LookupItem("TLSv1.3", "proxy.process.ssl.ssl_total_tlsv13", StatType::Rate)); + + // Connection error stats + _lookup_table.emplace("err_conn_fail", LookupItem("Conn Failed", "proxy.process.http.err_connect_fail_count", StatType::Rate)); + _lookup_table.emplace("err_client_abort", + LookupItem("Client Abort", "proxy.process.http.err_client_abort_count", StatType::Rate)); + _lookup_table.emplace("err_client_read", + LookupItem("Client Read Err", "proxy.process.http.err_client_read_error_count", StatType::Rate)); + + // Transaction error stats + _lookup_table.emplace("txn_aborts", LookupItem("Aborts", "proxy.process.http.transaction_counts.errors.aborts", StatType::Rate)); + _lookup_table.emplace( + "txn_possible_aborts", + LookupItem("Possible Aborts", "proxy.process.http.transaction_counts.errors.possible_aborts", StatType::Rate)); + _lookup_table.emplace("txn_other_errors", + LookupItem("Other Errors", "proxy.process.http.transaction_counts.errors.other", StatType::Rate)); + + // Cache error stats + _lookup_table.emplace("cache_read_errors", LookupItem("Cache Read Err", "proxy.process.cache.read.failure", StatType::Rate)); + _lookup_table.emplace("cache_write_errors", LookupItem("Cache Write Err", "proxy.process.cache.write.failure", StatType::Rate)); + _lookup_table.emplace("cache_lookup_fail", LookupItem("Lookup Fail", "proxy.process.cache.lookup.failure", StatType::Rate)); + + // HTTP/2 error stats + _lookup_table.emplace("h2_stream_errors", LookupItem("Stream Errors", "proxy.process.http2.stream_errors", StatType::Rate)); + _lookup_table.emplace("h2_conn_errors", LookupItem("Conn Errors", "proxy.process.http2.connection_errors", StatType::Rate)); + _lookup_table.emplace("h2_session_die_error", + LookupItem("Session Die Err", "proxy.process.http2.session_die_error", StatType::Rate)); + _lookup_table.emplace("h2_session_die_high_error", + LookupItem("High Error Rate", "proxy.process.http2.session_die_high_error_rate", StatType::Rate)); + + // HTTP/2 stream stats + _lookup_table.emplace("h2_streams_total", + LookupItem("Total Streams", "proxy.process.http2.total_client_streams", StatType::Rate)); + _lookup_table.emplace("h2_streams_current", + LookupItem("Current Streams", "proxy.process.http2.current_client_streams", StatType::Absolute)); + + // Network stats + _lookup_table.emplace("net_open_conn", + LookupItem("Open Conn", "proxy.process.net.connections_currently_open", StatType::Absolute)); + _lookup_table.emplace("net_throttled", + LookupItem("Throttled Conn", "proxy.process.net.connections_throttled_in", StatType::Rate)); + + // HTTP Milestones - timing stats in nanoseconds (cumulative), displayed as ms/s + // Listed in chronological order of when they occur during a request + + // State machine start + _lookup_table.emplace("ms_sm_start", LookupItem("SM Start", "proxy.process.http.milestone.sm_start", StatType::RateNsToMs)); + + // Client-side milestones + _lookup_table.emplace("ms_ua_begin", LookupItem("Client Begin", "proxy.process.http.milestone.ua_begin", StatType::RateNsToMs)); + _lookup_table.emplace("ms_ua_first_read", + LookupItem("Client 1st Read", "proxy.process.http.milestone.ua_first_read", StatType::RateNsToMs)); + _lookup_table.emplace("ms_ua_read_header", + LookupItem("Client Hdr Done", "proxy.process.http.milestone.ua_read_header_done", StatType::RateNsToMs)); + + // Cache read milestones + _lookup_table.emplace("ms_cache_read_begin", + LookupItem("Cache Rd Begin", "proxy.process.http.milestone.cache_open_read_begin", StatType::RateNsToMs)); + _lookup_table.emplace("ms_cache_read_end", + LookupItem("Cache Rd End", "proxy.process.http.milestone.cache_open_read_end", StatType::RateNsToMs)); + + // DNS milestones + _lookup_table.emplace("ms_dns_begin", + LookupItem("DNS Begin", "proxy.process.http.milestone.dns_lookup_begin", StatType::RateNsToMs)); + _lookup_table.emplace("ms_dns_end", LookupItem("DNS End", "proxy.process.http.milestone.dns_lookup_end", StatType::RateNsToMs)); + + // Origin server connection milestones + _lookup_table.emplace("ms_server_connect", + LookupItem("Origin Connect", "proxy.process.http.milestone.server_connect", StatType::RateNsToMs)); + _lookup_table.emplace("ms_server_first_connect", + LookupItem("Origin 1st Conn", "proxy.process.http.milestone.server_first_connect", StatType::RateNsToMs)); + _lookup_table.emplace("ms_server_connect_end", + LookupItem("Origin Conn End", "proxy.process.http.milestone.server_connect_end", StatType::RateNsToMs)); + + // Origin server I/O milestones + _lookup_table.emplace("ms_server_begin_write", + LookupItem("Origin Write", "proxy.process.http.milestone.server_begin_write", StatType::RateNsToMs)); + _lookup_table.emplace("ms_server_first_read", + LookupItem("Origin 1st Read", "proxy.process.http.milestone.server_first_read", StatType::RateNsToMs)); + _lookup_table.emplace( + "ms_server_read_header", + LookupItem("Origin Hdr Done", "proxy.process.http.milestone.server_read_header_done", StatType::RateNsToMs)); + + // Cache write milestones + _lookup_table.emplace("ms_cache_write_begin", + LookupItem("Cache Wr Begin", "proxy.process.http.milestone.cache_open_write_begin", StatType::RateNsToMs)); + _lookup_table.emplace("ms_cache_write_end", + LookupItem("Cache Wr End", "proxy.process.http.milestone.cache_open_write_end", StatType::RateNsToMs)); + + // Client write and close milestones + _lookup_table.emplace("ms_ua_begin_write", + LookupItem("Client Write", "proxy.process.http.milestone.ua_begin_write", StatType::RateNsToMs)); + _lookup_table.emplace("ms_server_close", + LookupItem("Origin Close", "proxy.process.http.milestone.server_close", StatType::RateNsToMs)); + _lookup_table.emplace("ms_ua_close", LookupItem("Client Close", "proxy.process.http.milestone.ua_close", StatType::RateNsToMs)); + + // State machine finish + _lookup_table.emplace("ms_sm_finish", LookupItem("SM Finish", "proxy.process.http.milestone.sm_finish", StatType::RateNsToMs)); +} + +bool +Stats::getStats() +{ + _old_stats = std::move(_stats); + _stats = std::make_unique>(); + + gettimeofday(&_time, nullptr); + double now = _time.tv_sec + static_cast(_time.tv_usec) / 1000000; + + _last_error = fetch_and_fill_stats(_lookup_table, _stats.get()); + if (!_last_error.empty()) { + return false; + } + + _old_time = _now; + _now = now; + _time_diff = _now - _old_time; + + // Record history for key metrics used in graphs + static const std::vector history_keys = { + "client_req", // Requests/sec + "client_net", // Client bandwidth + "server_net", // Origin bandwidth + "ram_ratio", // Cache hit rate + "client_curr_conn", // Current connections + "server_curr_conn", // Origin connections + "lookups", // Cache lookups + "cache_writes", // Cache writes + "dns_lookups", // DNS lookups + "2xx", // 2xx responses + "4xx", // 4xx responses + "5xx", // 5xx responses + }; + + for (const auto &key : history_keys) { + double value = 0; + getStat(key, value); + + auto &hist = _history[key]; + hist.push_back(value); + + // Keep history bounded + while (hist.size() > MAX_HISTORY_LENGTH) { + hist.pop_front(); + } + } + + return true; +} + +std::string +Stats::fetch_and_fill_stats(const std::map &lookup_table, std::map *stats) +{ + namespace rpc = shared::rpc; + + if (stats == nullptr) { + return "Invalid stats parameter, it shouldn't be null."; + } + + try { + rpc::RecordLookupRequest request; + + // Build the request with all metrics we need to fetch + for (const auto &[key, item] : lookup_table) { + // Only add direct metrics (not derived ones) + if (item.type == StatType::Absolute || item.type == StatType::Rate || item.type == StatType::RequestPct || + item.type == StatType::TimeRatio || item.type == StatType::RateNsToMs) { + try { + request.emplace_rec(MetricParam{item.name}); + } catch (const std::exception &e) { + return std::string("Error configuring stats request: ") + e.what(); + } + } + } + + rpc::RPCClient rpcClient; + auto const &rpcResponse = rpcClient.invoke<>(request, std::chrono::milliseconds(RPC_TIMEOUT_MS), RPC_RETRY_COUNT); + + if (!rpcResponse.is_error()) { + auto const &records = rpcResponse.result.as(); + + if (!records.errorList.empty()) { + std::stringstream ss; + for (const auto &err : records.errorList) { + ss << err << "\n"; + } + return ss.str(); + } + + for (auto &&recordInfo : records.recordList) { + (*stats)[recordInfo.name] = recordInfo.currentValue; + } + } else { + std::stringstream ss; + ss << rpcResponse.error.as(); + return ss.str(); + } + } catch (const std::exception &ex) { + std::string error_msg = ex.what(); + + // Check for permission denied error (EACCES = 13) + if (error_msg.find("(13)") != std::string::npos || error_msg.find("Permission denied") != std::string::npos) { + return "Permission denied accessing RPC socket.\n" + "Ensure you have permission to access the ATS runtime directory.\n" + "You may need to run as the traffic_server user or with sudo.\n" + "Original error: " + + error_msg; + } + + // Check for connection refused (server not running) + if (error_msg.find("ECONNREFUSED") != std::string::npos || error_msg.find("Connection refused") != std::string::npos) { + return "Cannot connect to ATS - is traffic_server running?\n" + "Original error: " + + error_msg; + } + + return error_msg; + } + + return {}; // No error +} + +int64_t +Stats::getValue(const std::string &key, const std::map *stats) const +{ + if (stats == nullptr) { + return 0; + } + auto it = stats->find(key); + if (it == stats->end()) { + return 0; + } + return std::atoll(it->second.c_str()); +} + +void +Stats::getStat(const std::string &key, double &value, StatType overrideType) +{ + std::string prettyName; + StatType type; + getStat(key, value, prettyName, type, overrideType); +} + +void +Stats::getStat(const std::string &key, std::string &value) +{ + auto it = _lookup_table.find(key); + if (it == _lookup_table.end()) { + fprintf(stderr, "ERROR: Unknown stat key '%s' not found in lookup table\n", key.c_str()); + value = ""; + return; + } + const auto &item = it->second; + + if (_stats) { + auto stats_it = _stats->find(item.name); + if (stats_it != _stats->end()) { + value = stats_it->second; + return; + } + } + value = ""; +} + +void +Stats::getStat(const std::string &key, double &value, std::string &prettyName, StatType &type, StatType overrideType) +{ + value = 0; + + auto it = _lookup_table.find(key); + if (it == _lookup_table.end()) { + fprintf(stderr, "ERROR: Unknown stat key '%s' not found in lookup table\n", key.c_str()); + prettyName = key; + type = StatType::Absolute; + return; + } + const auto &item = it->second; + + prettyName = item.pretty; + type = (overrideType != StatType::Absolute) ? overrideType : item.type; + + switch (type) { + case StatType::Absolute: + case StatType::Rate: + case StatType::RequestPct: + case StatType::TimeRatio: + case StatType::RateNsToMs: { + if (_stats) { + value = getValue(item.name, _stats.get()); + } + + // Special handling for total_time (convert from nanoseconds) + if (key == "total_time") { + value = value / 10000000; + } + + // Calculate rate if needed + if ((type == StatType::Rate || type == StatType::RequestPct || type == StatType::TimeRatio || type == StatType::RateNsToMs) && + _old_stats != nullptr && !_absolute) { + double old = getValue(item.name, _old_stats.get()); + if (key == "total_time") { + old = old / 10000000; + } + value = _time_diff > 0 ? (value - old) / _time_diff : 0; + } + + // Convert nanoseconds to milliseconds for RateNsToMs + if (type == StatType::RateNsToMs) { + value = value / 1000000.0; // ns to ms + } + break; + } + + case StatType::Ratio: + case StatType::Percentage: { + double numerator = 0; + double denominator = 0; + getStat(item.numerator, numerator); + getStat(item.denominator, denominator); + value = (denominator != 0) ? numerator / denominator : 0; + if (type == StatType::Percentage) { + value *= 100; + } + break; + } + + case StatType::Sum: + case StatType::SumBits: { + double first = 0; + double second = 0; + getStat(item.numerator, first, StatType::Rate); + getStat(item.denominator, second, StatType::Rate); + value = first + second; + if (type == StatType::SumBits) { + value *= 8; // Convert bytes to bits + } + break; + } + + case StatType::SumAbsolute: { + double first = 0; + double second = 0; + getStat(item.numerator, first); + getStat(item.denominator, second); + value = first + second; + break; + } + } + + // Post-processing for TimeRatio: calculate average time in milliseconds + // Note: transaction_totaltime metrics are already stored in milliseconds (ua_msecs_*) + if (type == StatType::TimeRatio) { + double denominator = 0; + getStat(item.denominator, denominator, StatType::Rate); + value = (denominator != 0) ? value / denominator : 0; + } + + // Post-processing for RequestPct: calculate percentage of client requests + if (type == StatType::RequestPct) { + double client_req = 0; + getStat("client_req", client_req); + value = (client_req != 0) ? value / client_req * 100 : 0; + } +} + +bool +Stats::toggleAbsolute() +{ + _absolute = !_absolute; + return _absolute; +} + +std::vector +Stats::getStatKeys() const +{ + std::vector keys; + keys.reserve(_lookup_table.size()); + for (const auto &[key, _] : _lookup_table) { + keys.push_back(key); + } + return keys; +} + +bool +Stats::hasStat(const std::string &key) const +{ + return _lookup_table.find(key) != _lookup_table.end(); +} + +const LookupItem * +Stats::getLookupItem(const std::string &key) const +{ + auto it = _lookup_table.find(key); + return (it != _lookup_table.end()) ? &it->second : nullptr; +} + +std::vector +Stats::getHistory(const std::string &key, double maxValue) const +{ + std::vector result; + + auto it = _history.find(key); + if (it == _history.end() || it->second.empty()) { + return result; + } + + const auto &hist = it->second; + + // Find max value for normalization if not specified + if (maxValue <= 0.0) { + maxValue = *std::max_element(hist.begin(), hist.end()); + if (maxValue <= 0.0) { + maxValue = 1.0; // Avoid division by zero + } + } + + // Normalize values to 0.0-1.0 range + result.reserve(hist.size()); + for (double val : hist) { + result.push_back(val / maxValue); + } + + return result; +} + +int +Stats::validateLookupTable() const +{ + int errors = 0; + + for (const auto &[key, item] : _lookup_table) { + // Check derived stats that require numerator and denominator + if (item.type == StatType::Ratio || item.type == StatType::Percentage || item.type == StatType::Sum || + item.type == StatType::SumBits || item.type == StatType::SumAbsolute || item.type == StatType::TimeRatio) { + // Numerator must be a valid key + if (item.numerator[0] != '\0' && _lookup_table.find(item.numerator) == _lookup_table.end()) { + fprintf(stderr, "WARNING: Stat '%s' references unknown numerator '%s'\n", key.c_str(), item.numerator); + ++errors; + } + + // Denominator must be a valid key + if (item.denominator[0] != '\0' && _lookup_table.find(item.denominator) == _lookup_table.end()) { + fprintf(stderr, "WARNING: Stat '%s' references unknown denominator '%s'\n", key.c_str(), item.denominator); + ++errors; + } + } + } + + return errors; +} + +} // namespace traffic_top diff --git a/src/traffic_top/Stats.h b/src/traffic_top/Stats.h new file mode 100644 index 00000000000..12f247914f8 --- /dev/null +++ b/src/traffic_top/Stats.h @@ -0,0 +1,270 @@ +/** @file + + Stats class declaration for traffic_top. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "StatType.h" + +namespace traffic_top +{ + +/** + * Defines a statistic lookup item with display name, metric name(s), and type. + */ +struct LookupItem { + /// Constructor for simple stats that map directly to a metric + LookupItem(const char *pretty_name, const char *metric_name, StatType stat_type) + : pretty(pretty_name), name(metric_name), numerator(""), denominator(""), type(stat_type) + { + } + + /// Constructor for derived stats that combine two metrics + LookupItem(const char *pretty_name, const char *num, const char *denom, StatType stat_type) + : pretty(pretty_name), name(num), numerator(num), denominator(denom), type(stat_type) + { + } + + const char *pretty; ///< Display name shown in UI + const char *name; ///< Primary metric name or numerator reference + const char *numerator; ///< Numerator stat key (for derived stats) + const char *denominator; ///< Denominator stat key (for derived stats) + StatType type; ///< How to calculate and display this stat +}; + +/** + * Stats collector and calculator for traffic_top. + * + * Fetches statistics from ATS via RPC and provides methods to + * retrieve calculated values for display. + */ +class Stats +{ +public: + Stats(); + ~Stats() = default; + + // Non-copyable, non-movable + Stats(const Stats &) = delete; + Stats &operator=(const Stats &) = delete; + Stats(Stats &&) = delete; + Stats &operator=(Stats &&) = delete; + + /** + * Fetch latest stats from the ATS RPC interface. + * @return true on success, false on error + */ + bool getStats(); + + /** + * Get last error message from stats fetch. + * @return Error message or empty string if no error + */ + const std::string & + getLastError() const + { + return _last_error; + } + + /** + * Get a stat value by key. + * @param key The stat key from the lookup table + * @param value Output: the calculated value + * @param overrideType Optional type override for calculation + */ + void getStat(const std::string &key, double &value, StatType overrideType = StatType::Absolute); + + /** + * Get a stat value with metadata. + * @param key The stat key from the lookup table + * @param value Output: the calculated value + * @param prettyName Output: the display name + * @param type Output: the stat type + * @param overrideType Optional type override for calculation + */ + void getStat(const std::string &key, double &value, std::string &prettyName, StatType &type, + StatType overrideType = StatType::Absolute); + + /** + * Get a string stat value (e.g., version). + * @param key The stat key + * @param value Output: the string value + */ + void getStat(const std::string &key, std::string &value); + + /** + * Toggle between absolute and rate display mode. + * @return New absolute mode state + */ + bool toggleAbsolute(); + + /** + * Set absolute display mode. + */ + void + setAbsolute(bool absolute) + { + _absolute = absolute; + } + + /** + * Check if currently in absolute display mode. + */ + bool + isAbsolute() const + { + return _absolute; + } + + /** + * Check if we can calculate rates (have previous stats). + */ + bool + canCalculateRates() const + { + return _old_stats != nullptr && _time_diff > 0; + } + + /** + * Get the hostname. + */ + const std::string & + getHost() const + { + return _host; + } + + /** + * Get the time difference since last stats fetch (seconds). + */ + double + getTimeDiff() const + { + return _time_diff; + } + + /** + * Get all available stat keys. + */ + std::vector getStatKeys() const; + + /** + * Check if a stat key exists. + */ + bool hasStat(const std::string &key) const; + + /** + * Get the lookup item for a stat key. + */ + const LookupItem *getLookupItem(const std::string &key) const; + + /** + * Get history data for a stat, normalized to 0.0-1.0 range. + * @param key The stat key + * @param maxValue The maximum value for normalization (0 = auto-scale) + * @return Vector of normalized values (oldest to newest) + */ + std::vector getHistory(const std::string &key, double maxValue = 0.0) const; + + /** + * Get the maximum history length. + */ + static constexpr size_t + getMaxHistoryLength() + { + return MAX_HISTORY_LENGTH; + } + + /** + * Validate the lookup table for internal consistency. + * Checks that derived stats (Ratio, Percentage, Sum, etc.) reference + * valid numerator and denominator keys. + * @return Number of validation errors found (0 = all valid) + */ + int validateLookupTable() const; + +private: + // Maximum number of historical data points to store for graphs + // At 5 second intervals, 120 points = 10 minutes of history + static constexpr size_t MAX_HISTORY_LENGTH = 120; + + /** + * Get raw metric value from the stats map. + * @param key The ATS metric name (e.g., "proxy.process.http.incoming_requests") + * @param stats Pointer to the stats map (current or old) + * @return The metric value as int64_t, or 0 if not found + */ + int64_t getValue(const std::string &key, const std::map *stats) const; + + /** + * Fetch all metrics from ATS via JSON-RPC and populate the stats map. + * @param lookup_table The lookup table defining which metrics to fetch + * @param stats Output map to populate with metric name -> value pairs + * @return Empty string on success, error message on failure + */ + std::string fetch_and_fill_stats(const std::map &lookup_table, + std::map *stats); + + /** + * Initialize the lookup table with all stat definitions. + * This defines the mapping from display keys (e.g., "client_req") to + * ATS metrics (e.g., "proxy.process.http.incoming_requests") and + * how to calculate/display each stat. + */ + void initializeLookupTable(); + + // ------------------------------------------------------------------------- + // Stats storage + // ------------------------------------------------------------------------- + // We keep two snapshots of stats to calculate rates (delta / time_diff) + std::unique_ptr> _stats; ///< Current stats snapshot + std::unique_ptr> _old_stats; ///< Previous stats snapshot + + // ------------------------------------------------------------------------- + // Configuration and metadata + // ------------------------------------------------------------------------- + std::map _lookup_table; ///< Stat key -> metric mapping + std::map> _history; ///< Historical values for graphs + std::string _host; ///< Hostname for display + std::string _last_error; ///< Last error message from RPC + + // ------------------------------------------------------------------------- + // Timing for rate calculations + // ------------------------------------------------------------------------- + double _old_time = 0; ///< Timestamp of previous stats fetch (seconds) + double _now = 0; ///< Timestamp of current stats fetch (seconds) + double _time_diff = 0; ///< Time between fetches (for rate calculation) + struct timeval _time = {0, 0}; ///< Raw timeval from gettimeofday() + + // ------------------------------------------------------------------------- + // Display mode + // ------------------------------------------------------------------------- + bool _absolute = true; ///< True = show absolute values, False = show rates +}; + +} // namespace traffic_top diff --git a/src/traffic_top/format_graphs.py b/src/traffic_top/format_graphs.py new file mode 100644 index 00000000000..396ff99de39 --- /dev/null +++ b/src/traffic_top/format_graphs.py @@ -0,0 +1,418 @@ +#!/usr/bin/env python3 +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Generate graph layouts for traffic_top using Unicode block characters. + +Uses vertical bar graphs with block characters: +▁ ▂ ▃ ▄ ▅ ▆ ▇ █ (heights 1-8) + +Color gradient (ANSI escape codes): +- Low values: Blue/Cyan +- Medium values: Green/Yellow +- High values: Orange/Red +""" + +# Unicode block characters for vertical bars (index 0 = empty, 1-8 = heights) +BLOCKS = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'] + +# ANSI color codes for gradient (blue -> cyan -> green -> yellow -> red) +COLORS = { + 'reset': '\033[0m', + 'blue': '\033[34m', + 'cyan': '\033[36m', + 'green': '\033[32m', + 'yellow': '\033[33m', + 'red': '\033[31m', + 'magenta': '\033[35m', + 'white': '\033[37m', + 'bold': '\033[1m', + 'dim': '\033[2m', +} + + +def value_to_block(value: float, max_val: float = 100.0) -> str: + """Convert a value (0-max_val) to a block character.""" + if value <= 0: + return BLOCKS[0] + normalized = min(value / max_val, 1.0) + index = int(normalized * 8) + return BLOCKS[min(index + 1, 8)] if normalized > 0 else BLOCKS[0] + + +def value_to_color(value: float, max_val: float = 100.0) -> str: + """Get color code based on value (gradient from blue to red).""" + normalized = min(value / max_val, 1.0) + if normalized < 0.2: + return COLORS['blue'] + elif normalized < 0.4: + return COLORS['cyan'] + elif normalized < 0.6: + return COLORS['green'] + elif normalized < 0.8: + return COLORS['yellow'] + else: + return COLORS['red'] + + +def generate_graph_data(length: int, pattern: str = 'wave') -> list: + """Generate sample data for graph demonstration.""" + import math + + if pattern == 'wave': + # Sine wave pattern + return [50 + 40 * math.sin(i * 0.3) for i in range(length)] + elif pattern == 'ramp': + # Rising pattern + return [min(100, i * 100 / length) for i in range(length)] + elif pattern == 'spike': + # Random spikes + import random + random.seed(42) + return [random.randint(10, 90) for _ in range(length)] + elif pattern == 'load': + # Realistic CPU/network load pattern + import random + random.seed(123) + base = 30 + data = [] + for i in range(length): + base = max(5, min(95, base + random.randint(-15, 15))) + data.append(base) + return data + else: + return [50] * length + + +def format_graph_line(data: list, width: int, colored: bool = False) -> str: + """Format a single line of graph from data points.""" + # Take last 'width' data points, or pad with zeros + if len(data) > width: + data = data[-width:] + elif len(data) < width: + data = [0] * (width - len(data)) + data + + result = "" + for val in data: + block = value_to_block(val) + if colored: + color = value_to_color(val) + result += f"{color}{block}{COLORS['reset']}" + else: + result += block + return result + + +def format_header(title: str, box_width: int) -> str: + """Format a box header.""" + content_width = box_width - 2 + title_with_spaces = f" {title} " + dashes_needed = content_width - len(title_with_spaces) + left_dashes = dashes_needed // 2 + right_dashes = dashes_needed - left_dashes + return f"+{'-' * left_dashes}{title_with_spaces}{'-' * right_dashes}+" + + +def format_separator(box_width: int) -> str: + """Format a box separator.""" + return f"+{'-' * (box_width - 2)}+" + + +def format_graph_box_40(title: str, data: list, current_val: str, max_val: str, show_color: bool = False) -> list: + """ + Generate a 40-character wide box with graph (title in header). + + Layout: + +---- TITLE (current: XX%) ----+ + | ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▂▃▄▅▆ | + | Min: 0% Max: 100% | + +-------------------------------+ + """ + lines = [] + graph_width = 36 # 40 - 2 borders - 2 padding + + # Header with current value + header_title = f"{title} ({current_val})" + lines.append(format_header(header_title, 40)) + + # Graph line + graph = format_graph_line(data, graph_width, colored=show_color) + lines.append(f"| {graph} |") + + # Min/Max labels line + min_label = "Min: 0%" + max_label = f"Max: {max_val}" + space_between = graph_width - len(min_label) - len(max_label) + lines.append(f"| {min_label}{' ' * space_between}{max_label} |") + + lines.append(format_separator(40)) + return lines + + +def format_graph_row(label: str, data: list, value: str, width: int, show_color: bool = False) -> str: + """ + Format a single graph row with title inside: | LABEL ▁▂▃▄▅ VALUE | + + Used for multi-graph boxes where each row is a separate metric. + """ + content_width = width - 4 # subtract "| " and " |" + + # Allocate space: label (fixed), graph (flexible), value (fixed) + value_width = len(value) + 1 # value + leading space + label_width = min(len(label), 12) # max 12 chars for label + graph_width = content_width - label_width - value_width - 1 # -1 for space after label + + # Build the line + label_part = label[:label_width].ljust(label_width) + graph_part = format_graph_line(data, graph_width, colored=show_color) + value_part = value.rjust(value_width) + + return f"| {label_part} {graph_part}{value_part} |" + + +def format_multi_graph_box(graphs: list, width: int = 40, title: str = None, show_color: bool = False) -> list: + """ + Generate a box with multiple graphs inside (titles inside box). + + Each graph entry: (label, data, value) + + Layout (40-char): + +--------------------------------------+ + | Bandwidth ▂▁▁▂▁▁▁▂▃▄▃▂▃▃▂▂▃▂ 850M | + | Hit Rate ▅▅▆▇▇██▇▇▆▅▄▃▂▂▁▁▂ 85% | + | Requests ▂▂▄▄▄▃▂▇▂▇▆▂▂▂▃▄▆▇ 15K | + +--------------------------------------+ + """ + lines = [] + + # Header (plain separator or with title) + if title: + lines.append(format_header(title, width)) + else: + lines.append(format_separator(width)) + + # Graph rows + for label, data, value in graphs: + lines.append(format_graph_row(label, data, value, width, show_color)) + + # Footer + lines.append(format_separator(width)) + + return lines + + +def format_graph_box_80(title: str, data: list, current_val: str, avg_val: str, max_val: str, show_color: bool = False) -> list: + """ + Generate an 80-character wide box with graph and stats. + + Layout: + +----------------------- NETWORK BANDWIDTH (850 Mb/s) ------------------------+ + | ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▂▃▄▅ | + | Min: 0 Mb/s Avg: 620 Mb/s Max: 1000 Mb/s 60s ago | + +-----------------------------------------------------------------------------+ + """ + lines = [] + graph_width = 76 # 80 - 2 borders - 2 padding + + # Header with current value + header_title = f"{title} ({current_val})" + lines.append(format_header(header_title, 80)) + + # Graph line + graph = format_graph_line(data, graph_width, colored=show_color) + lines.append(f"| {graph} |") + + # Stats line: Min, Avg, Max, Time + min_label = "Min: 0" + avg_label = f"Avg: {avg_val}" + max_label = f"Max: {max_val}" + time_label = "60s ago" + + # Distribute labels across the width + total_label_len = len(min_label) + len(avg_label) + len(max_label) + len(time_label) + remaining = graph_width - total_label_len + gap = remaining // 3 + + stats_line = f"{min_label}{' ' * gap}{avg_label}{' ' * gap}{max_label}{' ' * gap}{time_label}" + # Pad to exact width + stats_line = stats_line.ljust(graph_width) + lines.append(f"| {stats_line} |") + + lines.append(format_separator(80)) + return lines + + +def format_multi_graph_box_80(graphs: list, show_color: bool = False) -> list: + """ + Generate an 80-character wide box with multiple stacked graphs. + + Each graph entry: (title, data, current_val) + """ + lines = [] + content_width = 76 # 80 - 2 borders - 2 padding + + # Combined header + titles = " / ".join(g[0] for g in graphs) + lines.append(format_header(titles, 80)) + + for title, data, current_val in graphs: + # Label and graph on same line + label = f"{title}: {current_val}" + label_width = 18 # Fixed label width + graph_width = content_width - label_width # Remaining for graph + + graph = format_graph_line(data, graph_width, colored=show_color) + line_content = f"{label:<{label_width}}{graph}" + lines.append(f"| {line_content} |") + + lines.append(format_separator(80)) + return lines + + +def print_ascii_layout(): + """Print ASCII-only version (for documentation).""" + print("## Graph Layouts (ASCII for documentation)") + print() + print("### 40-Character Box with Graph") + print() + print("```") + + # Generate sample data + data = generate_graph_data(36, 'load') + + for line in format_graph_box_40("CPU", data, "45%", "100%", show_color=False): + print(line) + + print() + + data = generate_graph_data(36, 'wave') + for line in format_graph_box_40("HIT RATE", data, "85%", "100%", show_color=False): + print(line) + + print("```") + print() + print("### 80-Character Box with Graph") + print() + print("```") + + data = generate_graph_data(76, 'load') + for line in format_graph_box_80("NETWORK BANDWIDTH", data, "850 Mb/s", "620 Mb/s", "1000 Mb/s", show_color=False): + print(line) + + print() + + data = generate_graph_data(76, 'wave') + for line in format_graph_box_80("CACHE HIT RATE", data, "85%", "78%", "100%", show_color=False): + print(line) + + print("```") + print() + print("### 80-Character Box with Multiple Graphs") + print() + print("```") + + # graph_width = 76 - 18 (label) = 58 + graphs = [ + ("Net In", generate_graph_data(58, 'load'), "850 Mb/s"), + ("Net Out", generate_graph_data(58, 'wave'), "620 Mb/s"), + ("Req/sec", generate_graph_data(58, 'spike'), "15K"), + ] + for line in format_multi_graph_box_80(graphs, show_color=False): + print(line) + + print("```") + + +def print_colored_demo(): + """Print colored version to terminal.""" + print() + print(f"{COLORS['bold']}## Graph Demo with Colors{COLORS['reset']}") + print() + + print(f"{COLORS['cyan']}### 40-Character Box{COLORS['reset']}") + print() + + data = generate_graph_data(36, 'load') + for line in format_graph_box_40("CPU USAGE", data, "45%", "100%", show_color=True): + print(line) + + print() + + print(f"{COLORS['cyan']}### 80-Character Box{COLORS['reset']}") + print() + + data = generate_graph_data(76, 'load') + for line in format_graph_box_80("NETWORK BANDWIDTH", data, "850 Mb/s", "620 Mb/s", "1000 Mb/s", show_color=True): + print(line) + + print() + + print(f"{COLORS['cyan']}### Multi-Graph Box{COLORS['reset']}") + print() + + graphs = [ + ("Net In", generate_graph_data(58, 'load'), "850 Mb/s"), + ("Net Out", generate_graph_data(58, 'wave'), "620 Mb/s"), + ("Req/sec", generate_graph_data(58, 'spike'), "15K"), + ("Hit Rate", generate_graph_data(58, 'ramp'), "85%"), + ] + for line in format_multi_graph_box_80(graphs, show_color=True): + print(line) + + print() + print(f"{COLORS['dim']}Color gradient: ", end="") + for i in range(0, 101, 10): + color = value_to_color(i) + block = value_to_block(i) + print(f"{color}{block}{COLORS['reset']}", end="") + print(f" (0% to 100%){COLORS['reset']}") + print() + + +def print_block_reference(): + """Print reference of available block characters.""" + print() + print("## Block Character Reference") + print() + print("Unicode block characters used for graphs:") + print() + print("| Height | Char | Unicode | Description |") + print("|--------|------|---------|-------------|") + print("| 0 | ' ' | U+0020 | Empty/space |") + print("| 1 | ▁ | U+2581 | Lower 1/8 |") + print("| 2 | ▂ | U+2582 | Lower 2/8 |") + print("| 3 | ▃ | U+2583 | Lower 3/8 |") + print("| 4 | ▄ | U+2584 | Lower 4/8 |") + print("| 5 | ▅ | U+2585 | Lower 5/8 |") + print("| 6 | ▆ | U+2586 | Lower 6/8 |") + print("| 7 | ▇ | U+2587 | Lower 7/8 |") + print("| 8 | █ | U+2588 | Full block |") + print() + print("Visual scale: ▁▂▃▄▅▆▇█") + print() + + +if __name__ == "__main__": + import sys + + if "--color" in sys.argv or "-c" in sys.argv: + print_colored_demo() + elif "--blocks" in sys.argv or "-b" in sys.argv: + print_block_reference() + else: + print_ascii_layout() + print_block_reference() diff --git a/src/traffic_top/format_layout.py b/src/traffic_top/format_layout.py new file mode 100644 index 00000000000..548b94c024d --- /dev/null +++ b/src/traffic_top/format_layout.py @@ -0,0 +1,495 @@ +#!/usr/bin/env python3 +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Format traffic_top layout lines with exact character widths. + +Each 40-char box format: +| Label Value Label Value | + +Stat 1: 17 chars (label + spaces + value) +Gap: 3 spaces +Stat 2: 16 chars (label + spaces + value) +Padding: 1 space each side +Total: 1 + 1 + 17 + 3 + 16 + 1 + 1 = 40 +""" + + +def format_stat(label: str, value: str, width: int) -> str: + """Format a single stat (label + value) to exact width. + + Numbers are right-aligned at a fixed position (width - 1). + Suffix follows the number. Values without suffix get trailing space. + """ + value_str = str(value) + label_str = str(label) + + # Separate number from suffix (check longer suffixes first) + suffix = "" + num_str = value_str + for s in ["ms", "%", "K", "M", "G", "T", "d"]: + if value_str.endswith(s): + suffix = s + num_str = value_str[:-len(s)] + break + + # Always reserve 1 char for suffix position so numbers align + # Values without suffix get a trailing space + suffix_field = 1 + actual_suffix_len = len(suffix) + + # For 2-char suffix like "ms", we need more space + if actual_suffix_len > 1: + suffix_field = actual_suffix_len + + # Calculate number field width + available_for_num = width - len(label_str) - 1 - suffix_field + + if available_for_num < len(num_str): + # Truncate label if needed + label_str = label_str[:width - len(num_str) - 1 - suffix_field] + available_for_num = width - len(label_str) - 1 - suffix_field + + # Right-align the number in its field + num_field = num_str.rjust(available_for_num) + + # Build result - pad suffix to suffix_field width + if actual_suffix_len == 0: + suffix_part = " " # trailing space where suffix would be + else: + suffix_part = suffix + + return f"{label_str} {num_field}{suffix_part}" + + +def format_box_line(stats: list, box_width: int = 40) -> str: + """Format a line inside a box with 2 stat pairs.""" + content_width = box_width - 4 # 36 for 40-char box + stat1_width = 17 + gap = 3 + stat2_width = content_width - stat1_width - gap # 16 + + stat1 = format_stat(stats[0][0], stats[0][1], stat1_width) + stat2 = format_stat(stats[1][0], stats[1][1], stat2_width) + + return f"| {stat1}{' ' * gap}{stat2} |" + + +def format_multi_box_line(all_stats: list, num_boxes: int, box_width: int = 40) -> str: + """Format a line with multiple boxes side by side.""" + boxes = [format_box_line(stats, box_width) for stats in all_stats] + line = "||".join(b[1:-1] for b in boxes) + return "|" + line + "|" + + +def format_header(title: str, box_width: int = 40) -> str: + """Format a box header like '+--- TITLE ---+'""" + content_width = box_width - 2 + title_with_spaces = f" {title} " + dashes_needed = content_width - len(title_with_spaces) + left_dashes = dashes_needed // 2 + right_dashes = dashes_needed - left_dashes + return f"+{'-' * left_dashes}{title_with_spaces}{'-' * right_dashes}+" + + +def format_separator(box_width: int = 40) -> str: + """Format a box separator like '+----...----+'""" + return f"+{'-' * (box_width - 2)}+" + + +def multi_header(titles: list) -> str: + """Format multiple headers joined together.""" + return "".join(format_header(t) for t in titles) + + +def multi_separator(num_boxes: int) -> str: + """Format multiple separators joined together.""" + return "".join(format_separator() for _ in range(num_boxes)) + + +def generate_80x24(): + """Generate the 80x24 layout.""" + print("## 80x24 Terminal (2 boxes)") + print() + print("```") + + # Row 1: CACHE, REQS/RESPONSES + print(multi_header(["CACHE", "REQS/RESPONSES"])) + + rows = [ + [[("Disk Used", "120G"), ("RAM Used", "512M")], [("GET", "15K"), ("POST", "800")]], + [[("Disk Total", "500G"), ("RAM Total", "1G")], [("HEAD", "200"), ("PUT", "50")]], + [[("RAM Hit", "85%"), ("Fresh", "72%")], [("DELETE", "10"), ("OPTIONS", "25")]], + [[("Revalidate", "12%"), ("Cold", "8%")], [("200", "78%"), ("206", "5%")]], + [[("Changed", "3%"), ("Not Cached", "2%")], [("301", "2%"), ("304", "12%")]], + [[("No Cache", "3%"), ("Entries", "50K")], [("404", "1%"), ("502", "0%")]], + [[("Lookups", "25K"), ("Writes", "8K")], [("2xx", "83%"), ("3xx", "14%")]], + [[("Read Active", "150"), ("Write Act", "45")], [("4xx", "2%"), ("5xx", "1%")]], + [[("Updates", "500"), ("Deletes", "100")], [("Error", "15"), ("Other Err", "3")]], + ] + for row in rows: + print(format_multi_box_line(row, 2)) + print(multi_separator(2)) + + # Row 2: CLIENT, ORIGIN + print(multi_header(["CLIENT", "ORIGIN"])) + + rows = [ + [[("Requests", "15K"), ("Connections", "800")], [("Requests", "12K"), ("Connections", "400")]], + [[("Current Conn", "500"), ("Active Conn", "450")], [("Current Conn", "200"), ("Req/Conn", "30")]], + [[("Req/Conn", "19"), ("Dynamic KA", "400")], [("Connect Fail", "5"), ("Aborts", "2")]], + [[("Avg Size", "45K"), ("Net (Mb/s)", "850")], [("Avg Size", "52K"), ("Net (Mb/s)", "620")]], + [[("Resp Time", "12"), ("Head Bytes", "18M")], [("Keep Alive", "380"), ("Conn Reuse", "350")]], + [[("Body Bytes", "750M"), ("HTTP/1 Conn", "200")], [("Head Bytes", "15M"), ("Body Bytes", "600M")]], + [[("HTTP/2 Conn", "300"), ("SSL Session", "450")], [("DNS Lookups", "800"), ("DNS Hits", "720")]], + [[("SSL Handshk", "120"), ("SSL Errors", "3")], [("DNS Ratio", "90%"), ("DNS Entry", "500")]], + [[("Hit Latency", "2"), ("Miss Laten", "45")], [("Error", "12"), ("Other Err", "5")]], + ] + for row in rows: + print(format_multi_box_line(row, 2)) + print(multi_separator(2)) + + print(" 12:30:45 proxy.example.com [1/6] Overview q h 1-6") + print("```") + + +def generate_120x40(): + """Generate the 120x40 layout.""" + print("## 120x40 Terminal (3 boxes)") + print() + print("```") + + # Row 1: CACHE, REQUESTS, CONNECTIONS + print(multi_header(["CACHE", "REQUESTS", "CONNECTIONS"])) + rows = [ + [ + [("Disk Used", "120G"), ("Disk Total", "500G")], [("Client Req", "15K"), ("Server Req", "12K")], + [("Client Conn", "800"), ("Current", "500")] + ], + [ + [("RAM Used", "512M"), ("RAM Total", "1G")], [("GET", "12K"), ("POST", "800")], + [("Active Conn", "450"), ("Server Con", "400")] + ], + [ + [("RAM Ratio", "85%"), ("Entries", "50K")], [("HEAD", "200"), ("PUT", "50")], + [("Server Curr", "200"), ("Req/Conn", "30")] + ], + [ + [("Lookups", "25K"), ("Writes", "8K")], [("DELETE", "10"), ("OPTIONS", "25")], + [("HTTP/1 Conn", "200"), ("HTTP/2", "300")] + ], + [ + [("Read Active", "150"), ("Write Act", "45")], [("PURGE", "5"), ("PUSH", "2")], + [("Keep Alive", "380"), ("Conn Reuse", "350")] + ], + [ + [("Updates", "500"), ("Deletes", "100")], [("CONNECT", "15"), ("TRACE", "0")], + [("Dynamic KA", "400"), ("Throttled", "5")] + ], + ] + for row in rows: + print(format_multi_box_line(row, 3)) + print(multi_separator(3)) + + # Row 2: HIT RATES, RESPONSES, BANDWIDTH + print(multi_header(["HIT RATES", "RESPONSES", "BANDWIDTH"])) + rows = [ + [[("RAM Hit", "85%"), ("Fresh", "72%")], [("200", "78%"), ("206", "5%")], [("Client Head", "18M"), ("Client Bod", "750M")]], + [ + [("Revalidate", "12%"), ("Cold", "8%")], [("301", "2%"), ("304", "12%")], + [("Server Head", "15M"), ("Server Bod", "600M")] + ], + [[("Changed", "3%"), ("Not Cached", "2%")], [("404", "1%"), ("502", "0%")], [("Avg ReqSize", "45K"), ("Avg Resp", "52K")]], + [[("No Cache", "3%"), ("Error", "1%")], [("503", "0%"), ("504", "0%")], [("Net In Mbs", "850"), ("Net Out", "620")]], + [ + [("Fresh Time", "2ms"), ("Reval Time", "15")], [("2xx", "83%"), ("3xx", "14%")], + [("Head Bytes", "33M"), ("Body Bytes", "1G")] + ], + [ + [("Cold Time", "45"), ("Changed T", "30")], [("4xx", "2%"), ("5xx", "1%")], + [("Avg Latency", "12ms"), ("Max Laten", "450")] + ], + ] + for row in rows: + print(format_multi_box_line(row, 3)) + print(multi_separator(3)) + + # Row 3: SSL/TLS, DNS, ERRORS + print(multi_header(["SSL/TLS", "DNS", "ERRORS"])) + rows = [ + [ + [("SSL Success", "450"), ("SSL Fail", "3")], [("DNS Lookups", "800"), ("DNS Hits", "720")], + [("Connect Fail", "5"), ("Aborts", "2")] + ], + [ + [("SSL Session", "450"), ("SSL Handshk", "120")], [("DNS Ratio", "90%"), ("DNS Entry", "500")], + [("Client Abrt", "15"), ("Origin Err", "12")] + ], + [ + [("Session Hit", "400"), ("Session Mis", "50")], [("Pending", "5"), ("In Flight", "12")], + [("CacheRdErr", "3"), ("Cache Writ", "1")] + ], + [[("TLS 1.2", "200"), ("TLS 1.3", "250")], [("Expired", "10"), ("Evicted", "25")], [("Timeout", "20"), ("Other Err", "8")]], + [ + [("Client Cert", "50"), ("Origin SSL", "380")], [("Avg Lookup", "2ms"), ("Max Lookup", "45")], + [("HTTP Err", "10"), ("Parse Err", "2")] + ], + [ + [("Renegotiate", "10"), ("Resumption", "350")], [("Failed", "5"), ("Retries", "12")], + [("DNS Fail", "5"), ("SSL Err", "3")] + ], + ] + for row in rows: + print(format_multi_box_line(row, 3)) + print(multi_separator(3)) + + # Row 4: CLIENT, ORIGIN, TOTALS + print(multi_header(["CLIENT", "ORIGIN", "TOTALS"])) + rows = [ + [ + [("Requests", "15K"), ("Connections", "800")], [("Requests", "12K"), ("Connections", "400")], + [("Total Req", "150M"), ("Total Conn", "5M")] + ], + [ + [("Current Con", "500"), ("Active Conn", "450")], [("Current Con", "200"), ("Req/Conn", "30")], + [("Total Bytes", "50T"), ("Uptime", "45d")] + ], + [ + [("Avg Size", "45K"), ("Net (Mb/s)", "850")], [("Avg Size", "52K"), ("Net (Mb/s)", "620")], + [("Cache Size", "120G"), ("RAM Cache", "512M")] + ], + [ + [("Resp Time", "12"), ("Head Bytes", "18M")], [("Keep Alive", "380"), ("Conn Reuse", "350")], + [("Hit Rate", "85%"), ("Bandwidth", "850M")] + ], + [ + [("Body Bytes", "750M"), ("Errors", "15")], [("Head Bytes", "15M"), ("Body Bytes", "600M")], + [("Avg Resp", "12ms"), ("Peak Req", "25K")] + ], + [ + [("HTTP/1 Conn", "300"), ("HTTP/2 Con", "300")], [("Errors", "12"), ("Other Err", "5")], + [("Errors/hr", "50"), ("Uptime %", "99%")] + ], + ] + for row in rows: + print(format_multi_box_line(row, 3)) + print(multi_separator(3)) + + # Row 5: HTTP CODES, CACHE DETAIL, SYSTEM + print(multi_header(["HTTP CODES", "CACHE DETAIL", "SYSTEM"])) + rows = [ + [ + [("100", "0"), ("101", "0")], [("Lookup Act", "150"), ("Lookup Suc", "24K")], + [("Thread Cnt", "32"), ("Event Loop", "16")] + ], + [ + [("200", "78%"), ("201", "1%")], [("Read Active", "150"), ("Read Succ", "20K")], + [("Memory Use", "2.5G"), ("Peak Mem", "3G")] + ], + [[("204", "2%"), ("206", "5%")], [("Write Act", "45"), ("Write Succ", "8K")], [("Open FDs", "5K"), ("Max FDs", "64K")]], + [ + [("301", "2%"), ("302", "1%")], [("Update Act", "10"), ("Update Suc", "500")], + [("CPU User", "45%"), ("CPU System", "15%")] + ], + [ + [("304", "12%"), ("307", "0%")], [("Delete Act", "5"), ("Delete Suc", "100")], + [("IO Read", "850M"), ("IO Write", "620M")] + ], + ] + for row in rows: + print(format_multi_box_line(row, 3)) + print(multi_separator(3)) + + print( + " 12:30:45 proxy.example.com [1/3] Overview q h 1-3") + print("```") + + +def generate_160x40(): + """Generate the 160x40 layout.""" + print("## 160x40 Terminal (4 boxes)") + print() + print("```") + + # Row 1: CACHE, CLIENT, ORIGIN, REQUESTS + print(multi_header(["CACHE", "CLIENT", "ORIGIN", "REQUESTS"])) + rows = [ + [ + [("Disk Used", "120G"), ("Disk Total", "500G")], [("Requests", "15K"), ("Connections", "800")], + [("Requests", "12K"), ("Connections", "400")], [("GET", "12K"), ("POST", "800")] + ], + [ + [("RAM Used", "512M"), ("RAM Total", "1G")], [("Current Con", "500"), ("Active Conn", "450")], + [("Current Con", "200"), ("Req/Conn", "30")], [("HEAD", "200"), ("PUT", "50")] + ], + [ + [("Entries", "50K"), ("Avg Size", "45K")], [("Req/Conn", "19"), ("Dynamic KA", "400")], + [("Connect Fai", "5"), ("Aborts", "2")], [("DELETE", "10"), ("OPTIONS", "25")] + ], + [ + [("Lookups", "25K"), ("Writes", "8K")], [("Avg Size", "45K"), ("Net (Mb/s)", "850")], + [("Avg Size", "52K"), ("Net (Mb/s)", "620")], [("PURGE", "5"), ("PUSH", "2")] + ], + [ + [("Read Active", "150"), ("Write Act", "45")], [("Resp Time", "12"), ("Head Bytes", "18M")], + [("Keep Alive", "380"), ("Conn Reuse", "350")], [("CONNECT", "15"), ("TRACE", "0")] + ], + [ + [("Updates", "500"), ("Deletes", "100")], [("Body Bytes", "750M"), ("Errors", "15")], + [("Head Bytes", "15M"), ("Body Bytes", "600M")], [("Total Req", "150M"), ("Req/sec", "15K")] + ], + ] + for row in rows: + print(format_multi_box_line(row, 4)) + print(multi_separator(4)) + + # Row 2: HIT RATES, CONNECTIONS, SSL/TLS, RESPONSES + print(multi_header(["HIT RATES", "CONNECTIONS", "SSL/TLS", "RESPONSES"])) + rows = [ + [ + [("RAM Hit", "85%"), ("Fresh", "72%")], [("HTTP/1 Clnt", "200"), ("HTTP/1 Orig", "80")], + [("SSL Success", "450"), ("SSL Fail", "3")], [("200", "78%"), ("206", "5%")] + ], + [ + [("Revalidate", "12%"), ("Cold", "8%")], [("HTTP/2 Clnt", "300"), ("HTTP/2 Orig", "120")], + [("SSL Session", "450"), ("SSL Handshk", "120")], [("301", "2%"), ("304", "12%")] + ], + [ + [("Changed", "3%"), ("Not Cached", "2%")], [("HTTP/3 Clnt", "50"), ("HTTP/3 Orig", "20")], + [("Session Hit", "400"), ("Session Mis", "50")], [("404", "1%"), ("502", "0%")] + ], + [ + [("No Cache", "3%"), ("Error", "1%")], [("Keep Alive", "380"), ("Conn Reuse", "350")], + [("TLS 1.2", "200"), ("TLS 1.3", "250")], [("503", "0%"), ("504", "0%")] + ], + [ + [("Fresh Time", "2ms"), ("Reval Time", "15")], [("Throttled", "5"), ("Queued", "2")], + [("Client Cert", "50"), ("Origin SSL", "380")], [("2xx", "83%"), ("3xx", "14%")] + ], + [ + [("Cold Time", "45"), ("Changed T", "30")], [("Idle Timeou", "10"), ("Max Conns", "5K")], + [("Renegotiate", "10"), ("Resumption", "350")], [("4xx", "2%"), ("5xx", "1%")] + ], + ] + for row in rows: + print(format_multi_box_line(row, 4)) + print(multi_separator(4)) + + # Row 3: BANDWIDTH, DNS, ERRORS, TOTALS + print(multi_header(["BANDWIDTH", "DNS", "ERRORS", "TOTALS"])) + rows = [ + [ + [("Client Head", "18M"), ("Client Bod", "750M")], [("DNS Lookups", "800"), ("DNS Hits", "720")], + [("Connect Fai", "5"), ("Aborts", "2")], [("Total Req", "150M"), ("Total Conn", "5M")] + ], + [ + [("Server Head", "15M"), ("Server Bod", "600M")], [("DNS Ratio", "90%"), ("DNS Entry", "500")], + [("Client Abrt", "15"), ("Origin Err", "12")], [("Total Bytes", "50T"), ("Uptime", "45d")] + ], + [ + [("Avg ReqSize", "45K"), ("Avg Resp", "52K")], [("Pending", "5"), ("In Flight", "12")], + [("CacheRdErr", "3"), ("Cache Writ", "1")], [("Cache Size", "120G"), ("RAM Cache", "512M")] + ], + [ + [("Net In Mbs", "850"), ("Net Out", "620")], [("Expired", "10"), ("Evicted", "25")], + [("Timeout", "20"), ("Other Err", "8")], [("Hit Rate", "85%"), ("Bandwidth", "850M")] + ], + [ + [("Head Bytes", "33M"), ("Body Bytes", "1G")], [("Avg Lookup", "2ms"), ("Max Lookup", "45")], + [("HTTP Err", "10"), ("Parse Err", "2")], [("Avg Resp", "12ms"), ("Peak Req", "25K")] + ], + [ + [("Avg Latency", "12ms"), ("Max Laten", "450")], [("Failed", "5"), ("Retries", "12")], + [("DNS Fail", "5"), ("SSL Err", "3")], [("Errors/hr", "50"), ("Uptime %", "99%")] + ], + ] + for row in rows: + print(format_multi_box_line(row, 4)) + print(multi_separator(4)) + + # Row 4: HTTP CODES, CACHE DETAIL, ORIGIN DETAIL, MISC STATS + print(multi_header(["HTTP CODES", "CACHE DETAIL", "ORIGIN DETAIL", "MISC STATS"])) + rows = [ + [ + [("100", "0"), ("101", "0")], [("Lookup Act", "150"), ("Lookup Suc", "24K")], + [("Req Active", "50"), ("Req Pending", "12")], [("Thread Cnt", "32"), ("Event Loop", "16")] + ], + [ + [("200", "78%"), ("201", "1%")], [("Read Active", "150"), ("Read Succ", "20K")], + [("Conn Active", "200"), ("Conn Pend", "25")], [("Memory Use", "2.5G"), ("Peak Mem", "3G")] + ], + [ + [("204", "2%"), ("206", "5%")], [("Write Act", "45"), ("Write Succ", "8K")], + [("DNS Pending", "5"), ("DNS Active", "12")], [("Open FDs", "5K"), ("Max FDs", "64K")] + ], + [ + [("301", "2%"), ("302", "1%")], [("Update Act", "10"), ("Update Suc", "500")], + [("SSL Active", "50"), ("SSL Pend", "10")], [("CPU User", "45%"), ("CPU System", "15%")] + ], + [ + [("304", "12%"), ("307", "0%")], [("Delete Act", "5"), ("Delete Suc", "100")], + [("Retry Queue", "10"), ("Retry Act", "5")], [("IO Read", "850M"), ("IO Write", "620M")] + ], + [ + [("400", "1%"), ("401", "0%")], [("Evacuate", "5"), ("Scan", "2")], [("Timeout Que", "5"), ("Timeout Act", "2")], + [("Net Pkts", "100K"), ("Dropped", "50")] + ], + [ + [("403", "0%"), ("404", "1%")], [("Fragment 1", "15K"), ("Fragment 2", "3K")], + [("Error Queue", "5"), ("Error Act", "2")], [("Ctx Switch", "50K"), ("Interrupts", "25K")] + ], + [ + [("500", "0%"), ("502", "0%")], [("Fragment 3+", "500"), ("Avg Frags", "1.2")], + [("Health Chk", "100"), ("Health OK", "98")], [("GC Runs", "100"), ("GC Time", "50ms")] + ], + [ + [("503", "0%"), ("504", "0%")], [("Bytes Writ", "50T"), ("Bytes Read", "45T")], + [("Circuit Opn", "0"), ("Circuit Cls", "5")], [("Log Writes", "10K"), ("Log Bytes", "500M")] + ], + ] + for row in rows: + print(format_multi_box_line(row, 4)) + print(multi_separator(4)) + + # Row 5: PROTOCOLS, TIMEOUTS, QUEUES, RESOURCES + print(multi_header(["PROTOCOLS", "TIMEOUTS", "QUEUES", "RESOURCES"])) + rows = [ + [ + [("HTTP/1.0", "50"), ("HTTP/1.1", "150")], [("Connect TO", "10"), ("Read TO", "5")], + [("Accept Queu", "25"), ("Active Q", "50")], [("Threads Idl", "16"), ("Threads Bu", "16")] + ], + [ + [("HTTP/2", "300"), ("HTTP/3", "50")], [("Write TO", "3"), ("DNS TO", "2")], [("Pending Q", "12"), ("Retry Q", "5")], + [("Disk Free", "380G"), ("Disk Used", "120G")] + ], + ] + for row in rows: + print(format_multi_box_line(row, 4)) + print(multi_separator(4)) + + print( + " 12:30:45 proxy.example.com [1/2] Overview q h 1-2" + ) + print("```") + + +if __name__ == "__main__": + generate_80x24() + print() + generate_120x40() + print() + generate_160x40() diff --git a/src/traffic_top/stats.h b/src/traffic_top/stats.h deleted file mode 100644 index fa0aae1b690..00000000000 --- a/src/traffic_top/stats.h +++ /dev/null @@ -1,529 +0,0 @@ -/** @file - - Include file for the traffic_top stats. - - @section license License - - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "tscore/ink_assert.h" -#include "shared/rpc/RPCRequests.h" -#include "shared/rpc/RPCClient.h" -#include "shared/rpc/yaml_codecs.h" - -struct LookupItem { - LookupItem(const char *s, const char *n, const int t) : pretty(s), name(n), numerator(""), denominator(""), type(t) {} - LookupItem(const char *s, const char *n, const char *d, const int t) : pretty(s), name(n), numerator(n), denominator(d), type(t) - { - } - const char *pretty; - const char *name; - const char *numerator; - const char *denominator; - int type; -}; -extern size_t write_data(void *ptr, size_t size, size_t nmemb, void *stream); -extern std::string response; - -namespace constant -{ -const char global[] = "\"global\": {\n"; -const char start[] = "\"proxy.process."; -const char separator[] = "\": \""; -const char end[] = "\",\n"; -}; // namespace constant - -// Convenient definitions -namespace detail -{ -/// This is a convenience class to abstract the metric params. It makes it less verbose to add a metric info object inside the -/// record lookup object. -struct MetricParam : shared::rpc::RecordLookupRequest::Params { - MetricParam(std::string name) - : // not regex - shared::rpc::RecordLookupRequest::Params{std::move(name), shared::rpc::NOT_REGEX, shared::rpc::METRIC_REC_TYPES} - { - } -}; -} // namespace detail -//---------------------------------------------------------------------------- -class Stats -{ - using string = std::string; - template using map = std::map; - -public: - Stats() - { - char hostname[25]; - hostname[sizeof(hostname) - 1] = '\0'; - gethostname(hostname, sizeof(hostname) - 1); - _host = hostname; - - _time_diff = 0; - _old_time = 0; - _now = 0; - _time = (struct timeval){0, 0}; - _stats = nullptr; - _old_stats = nullptr; - _absolute = false; - lookup_table.insert(make_pair("version", LookupItem("Version", "proxy.process.version.server.short", 1))); - lookup_table.insert(make_pair("disk_used", LookupItem("Disk Used", "proxy.process.cache.bytes_used", 1))); - lookup_table.insert(make_pair("disk_total", LookupItem("Disk Total", "proxy.process.cache.bytes_total", 1))); - lookup_table.insert(make_pair("ram_used", LookupItem("Ram Used", "proxy.process.cache.ram_cache.bytes_used", 1))); - lookup_table.insert(make_pair("ram_total", LookupItem("Ram Total", "proxy.process.cache.ram_cache.total_bytes", 1))); - lookup_table.insert(make_pair("lookups", LookupItem("Lookups", "proxy.process.http.cache_lookups", 2))); - lookup_table.insert(make_pair("cache_writes", LookupItem("Writes", "proxy.process.http.cache_writes", 2))); - lookup_table.insert(make_pair("cache_updates", LookupItem("Updates", "proxy.process.http.cache_updates", 2))); - lookup_table.insert(make_pair("cache_deletes", LookupItem("Deletes", "proxy.process.http.cache_deletes", 2))); - lookup_table.insert(make_pair("read_active", LookupItem("Read Active", "proxy.process.cache.read.active", 1))); - lookup_table.insert(make_pair("write_active", LookupItem("Writes Active", "proxy.process.cache.write.active", 1))); - lookup_table.insert(make_pair("update_active", LookupItem("Update Active", "proxy.process.cache.update.active", 1))); - lookup_table.insert(make_pair("entries", LookupItem("Entries", "proxy.process.cache.direntries.used", 1))); - lookup_table.insert(make_pair("avg_size", LookupItem("Avg Size", "disk_used", "entries", 3))); - - lookup_table.insert(make_pair("dns_entry", LookupItem("DNS Entry", "proxy.process.hostdb.cache.current_items", 1))); - lookup_table.insert(make_pair("dns_hits", LookupItem("DNS Hits", "proxy.process.hostdb.total_hits", 2))); - lookup_table.insert(make_pair("dns_lookups", LookupItem("DNS Lookups", "proxy.process.hostdb.total_lookups", 2))); - lookup_table.insert(make_pair("dns_serve_stale", LookupItem("DNS Serve Stale", "proxy.process.hostdb.total_serve_stale", 2))); - - // Incoming HTTP/1.1 and HTTP/2 connections - some metrics are HTTP version specific - lookup_table.insert(make_pair("client_req", LookupItem("Requests", "proxy.process.http.incoming_requests", 2))); - - // total_client_connections - lookup_table.insert( - make_pair("client_conn_h1", LookupItem("New Conn HTTP/1.x", "proxy.process.http.total_client_connections", 2))); - lookup_table.insert( - make_pair("client_conn_h2", LookupItem("New Conn HTTP/2", "proxy.process.http2.total_client_connections", 2))); - lookup_table.insert(make_pair("client_conn", LookupItem("New Conn", "client_conn_h1", "client_conn_h2", 6))); - - // requests / connections - lookup_table.insert(make_pair("client_req_conn", LookupItem("Req/Conn", "client_req", "client_conn", 3))); - - // current_client_connections - lookup_table.insert( - make_pair("client_curr_conn_h1", LookupItem("Curr Conn HTTP/1.x", "proxy.process.http.current_client_connections", 1))); - lookup_table.insert( - make_pair("client_curr_conn_h2", LookupItem("Curr Conn HTTP/2", "proxy.process.http2.current_client_connections", 1))); - lookup_table.insert(make_pair("client_curr_conn", LookupItem("Curr Conn", "client_curr_conn_h1", "client_curr_conn_h2", 9))); - - // current_active_client_connections - lookup_table.insert(make_pair("client_actv_conn_h1", - LookupItem("Active Con HTTP/1.x", "proxy.process.http.current_active_client_connections", 1))); - lookup_table.insert(make_pair("client_actv_conn_h2", - LookupItem("Active Con HTTP/2", "proxy.process.http2.current_active_client_connections", 1))); - lookup_table.insert(make_pair("client_actv_conn", LookupItem("Active Con", "client_actv_conn_h1", "client_actv_conn_h2", 9))); - - lookup_table.insert(make_pair("server_req", LookupItem("Requests", "proxy.process.http.outgoing_requests", 2))); - lookup_table.insert(make_pair("server_conn", LookupItem("New Conn", "proxy.process.http.total_server_connections", 2))); - lookup_table.insert(make_pair("server_req_conn", LookupItem("Req/Conn", "server_req", "server_conn", 3))); - lookup_table.insert(make_pair("server_curr_conn", LookupItem("Curr Conn", "proxy.process.http.current_server_connections", 1))); - - lookup_table.insert( - make_pair("client_head", LookupItem("Head Bytes", "proxy.process.http.user_agent_response_header_total_size", 2))); - lookup_table.insert( - make_pair("client_body", LookupItem("Body Bytes", "proxy.process.http.user_agent_response_document_total_size", 2))); - lookup_table.insert( - make_pair("server_head", LookupItem("Head Bytes", "proxy.process.http.origin_server_response_header_total_size", 2))); - lookup_table.insert( - make_pair("server_body", LookupItem("Body Bytes", "proxy.process.http.origin_server_response_document_total_size", 2))); - - // not used directly - lookup_table.insert(make_pair("ram_hit", LookupItem("Ram Hit", "proxy.process.cache.ram_cache.hits", 2))); - lookup_table.insert(make_pair("ram_miss", LookupItem("Ram Misses", "proxy.process.cache.ram_cache.misses", 2))); - lookup_table.insert(make_pair("ka_total", LookupItem("KA Total", "proxy.process.net.dynamic_keep_alive_timeout_in_total", 2))); - lookup_table.insert(make_pair("ka_count", LookupItem("KA Count", "proxy.process.net.dynamic_keep_alive_timeout_in_count", 2))); - - lookup_table.insert(make_pair("client_abort", LookupItem("Clnt Abort", "proxy.process.http.err_client_abort_count", 2))); - lookup_table.insert(make_pair("conn_fail", LookupItem("Conn Fail", "proxy.process.http.err_connect_fail_count", 2))); - lookup_table.insert(make_pair("abort", LookupItem("Abort", "proxy.process.http.transaction_counts.errors.aborts", 2))); - lookup_table.insert( - make_pair("t_conn_fail", LookupItem("Conn Fail", "proxy.process.http.transaction_counts.errors.connect_failed", 2))); - lookup_table.insert(make_pair("other_err", LookupItem("Other Err", "proxy.process.http.transaction_counts.errors.other", 2))); - - // percentage - lookup_table.insert(make_pair("ram_ratio", LookupItem("Ram Hit", "ram_hit", "ram_hit_miss", 4))); - lookup_table.insert(make_pair("dns_ratio", LookupItem("DNS Hit", "dns_hits", "dns_lookups", 4))); - - // percentage of requests - lookup_table.insert(make_pair("fresh", LookupItem("Fresh", "proxy.process.http.transaction_counts.hit_fresh", 5))); - lookup_table.insert(make_pair("reval", LookupItem("Revalidate", "proxy.process.http.transaction_counts.hit_revalidated", 5))); - lookup_table.insert(make_pair("cold", LookupItem("Cold", "proxy.process.http.transaction_counts.miss_cold", 5))); - lookup_table.insert(make_pair("changed", LookupItem("Changed", "proxy.process.http.transaction_counts.miss_changed", 5))); - lookup_table.insert(make_pair("not", LookupItem("Not Cache", "proxy.process.http.transaction_counts.miss_not_cacheable", 5))); - lookup_table.insert(make_pair("no", LookupItem("No Cache", "proxy.process.http.transaction_counts.miss_client_no_cache", 5))); - - lookup_table.insert( - make_pair("fresh_time", LookupItem("Fresh (ms)", "proxy.process.http.transaction_totaltime.hit_fresh", "fresh", 8))); - lookup_table.insert( - make_pair("reval_time", LookupItem("Reval (ms)", "proxy.process.http.transaction_totaltime.hit_revalidated", "reval", 8))); - lookup_table.insert( - make_pair("cold_time", LookupItem("Cold (ms)", "proxy.process.http.transaction_totaltime.miss_cold", "cold", 8))); - lookup_table.insert( - make_pair("changed_time", LookupItem("Chang (ms)", "proxy.process.http.transaction_totaltime.miss_changed", "changed", 8))); - lookup_table.insert( - make_pair("not_time", LookupItem("Not (ms)", "proxy.process.http.transaction_totaltime.miss_not_cacheable", "not", 8))); - lookup_table.insert( - make_pair("no_time", LookupItem("No (ms)", "proxy.process.http.transaction_totaltime.miss_client_no_cache", "no", 8))); - - lookup_table.insert(make_pair("get", LookupItem("GET", "proxy.process.http.get_requests", 5))); - lookup_table.insert(make_pair("head", LookupItem("HEAD", "proxy.process.http.head_requests", 5))); - lookup_table.insert(make_pair("post", LookupItem("POST", "proxy.process.http.post_requests", 5))); - - lookup_table.insert(make_pair("100", LookupItem("100", "proxy.process.http.100_responses", 5))); - lookup_table.insert(make_pair("101", LookupItem("101", "proxy.process.http.101_responses", 5))); - lookup_table.insert(make_pair("1xx", LookupItem("1xx", "proxy.process.http.1xx_responses", 5))); - lookup_table.insert(make_pair("200", LookupItem("200", "proxy.process.http.200_responses", 5))); - lookup_table.insert(make_pair("201", LookupItem("201", "proxy.process.http.201_responses", 5))); - lookup_table.insert(make_pair("202", LookupItem("202", "proxy.process.http.202_responses", 5))); - lookup_table.insert(make_pair("203", LookupItem("203", "proxy.process.http.203_responses", 5))); - lookup_table.insert(make_pair("204", LookupItem("204", "proxy.process.http.204_responses", 5))); - lookup_table.insert(make_pair("205", LookupItem("205", "proxy.process.http.205_responses", 5))); - lookup_table.insert(make_pair("206", LookupItem("206", "proxy.process.http.206_responses", 5))); - lookup_table.insert(make_pair("2xx", LookupItem("2xx", "proxy.process.http.2xx_responses", 5))); - lookup_table.insert(make_pair("300", LookupItem("300", "proxy.process.http.300_responses", 5))); - lookup_table.insert(make_pair("301", LookupItem("301", "proxy.process.http.301_responses", 5))); - lookup_table.insert(make_pair("302", LookupItem("302", "proxy.process.http.302_responses", 5))); - lookup_table.insert(make_pair("303", LookupItem("303", "proxy.process.http.303_responses", 5))); - lookup_table.insert(make_pair("304", LookupItem("304", "proxy.process.http.304_responses", 5))); - lookup_table.insert(make_pair("305", LookupItem("305", "proxy.process.http.305_responses", 5))); - lookup_table.insert(make_pair("307", LookupItem("307", "proxy.process.http.307_responses", 5))); - lookup_table.insert(make_pair("3xx", LookupItem("3xx", "proxy.process.http.3xx_responses", 5))); - lookup_table.insert(make_pair("400", LookupItem("400", "proxy.process.http.400_responses", 5))); - lookup_table.insert(make_pair("401", LookupItem("401", "proxy.process.http.401_responses", 5))); - lookup_table.insert(make_pair("402", LookupItem("402", "proxy.process.http.402_responses", 5))); - lookup_table.insert(make_pair("403", LookupItem("403", "proxy.process.http.403_responses", 5))); - lookup_table.insert(make_pair("404", LookupItem("404", "proxy.process.http.404_responses", 5))); - lookup_table.insert(make_pair("405", LookupItem("405", "proxy.process.http.405_responses", 5))); - lookup_table.insert(make_pair("406", LookupItem("406", "proxy.process.http.406_responses", 5))); - lookup_table.insert(make_pair("407", LookupItem("407", "proxy.process.http.407_responses", 5))); - lookup_table.insert(make_pair("408", LookupItem("408", "proxy.process.http.408_responses", 5))); - lookup_table.insert(make_pair("409", LookupItem("409", "proxy.process.http.409_responses", 5))); - lookup_table.insert(make_pair("410", LookupItem("410", "proxy.process.http.410_responses", 5))); - lookup_table.insert(make_pair("411", LookupItem("411", "proxy.process.http.411_responses", 5))); - lookup_table.insert(make_pair("412", LookupItem("412", "proxy.process.http.412_responses", 5))); - lookup_table.insert(make_pair("413", LookupItem("413", "proxy.process.http.413_responses", 5))); - lookup_table.insert(make_pair("414", LookupItem("414", "proxy.process.http.414_responses", 5))); - lookup_table.insert(make_pair("415", LookupItem("415", "proxy.process.http.415_responses", 5))); - lookup_table.insert(make_pair("416", LookupItem("416", "proxy.process.http.416_responses", 5))); - lookup_table.insert(make_pair("429", LookupItem("429", "proxy.process.http.429_responses", 5))); - lookup_table.insert(make_pair("4xx", LookupItem("4xx", "proxy.process.http.4xx_responses", 5))); - lookup_table.insert(make_pair("500", LookupItem("500", "proxy.process.http.500_responses", 5))); - lookup_table.insert(make_pair("501", LookupItem("501", "proxy.process.http.501_responses", 5))); - lookup_table.insert(make_pair("502", LookupItem("502", "proxy.process.http.502_responses", 5))); - lookup_table.insert(make_pair("503", LookupItem("503", "proxy.process.http.503_responses", 5))); - lookup_table.insert(make_pair("504", LookupItem("504", "proxy.process.http.504_responses", 5))); - lookup_table.insert(make_pair("505", LookupItem("505", "proxy.process.http.505_responses", 5))); - lookup_table.insert(make_pair("5xx", LookupItem("5xx", "proxy.process.http.5xx_responses", 5))); - - // sum together - lookup_table.insert(make_pair("ram_hit_miss", LookupItem("Ram Hit+Miss", "ram_hit", "ram_miss", 6))); - lookup_table.insert(make_pair("client_net", LookupItem("Net (bits)", "client_head", "client_body", 7))); - lookup_table.insert(make_pair("client_size", LookupItem("Total Size", "client_head", "client_body", 6))); - lookup_table.insert(make_pair("client_avg_size", LookupItem("Avg Size", "client_size", "client_req", 3))); - - lookup_table.insert(make_pair("server_net", LookupItem("Net (bits)", "server_head", "server_body", 7))); - lookup_table.insert(make_pair("server_size", LookupItem("Total Size", "server_head", "server_body", 6))); - lookup_table.insert(make_pair("server_avg_size", LookupItem("Avg Size", "server_size", "server_req", 3))); - - lookup_table.insert(make_pair("total_time", LookupItem("Total Time", "proxy.process.http.total_transactions_time", 2))); - - // ratio - lookup_table.insert(make_pair("client_req_time", LookupItem("Resp (ms)", "total_time", "client_req", 3))); - lookup_table.insert(make_pair("client_dyn_ka", LookupItem("Dynamic KA", "ka_total", "ka_count", 3))); - } - - bool - getStats() - { - _old_stats = std::move(_stats); - _stats = std::make_unique>(); - - gettimeofday(&_time, nullptr); - double now = _time.tv_sec + (double)_time.tv_usec / 1000000; - - // We will lookup for all the metrics on one single request. - shared::rpc::RecordLookupRequest request; - - for (map::const_iterator lookup_it = lookup_table.begin(); lookup_it != lookup_table.end(); ++lookup_it) { - const LookupItem &item = lookup_it->second; - - if (item.type == 1 || item.type == 2 || item.type == 5 || item.type == 8) { - try { - // Add records names to the rpc request. - request.emplace_rec(detail::MetricParam{item.name}); - } catch (std::exception const &e) { - // Hard break, something happened when trying to set the last metric name into the request. - // This is very unlikely but just in case, we stop it. - fprintf(stderr, "Error configuring the stats request, local error: %s", e.what()); - return false; - } - } - } - // query the rpc node. - if (auto const &error = fetch_and_fill_stats(request, _stats.get()); !error.empty()) { - fprintf(stderr, "Error getting stats from the RPC node:\n%s", error.c_str()); - return false; - } - _old_time = _now; - _now = now; - _time_diff = _now - _old_time; - - return true; - } - - int64_t - getValue(const string &key, const map *stats) const - { - map::const_iterator stats_it = stats->find(key); - if (stats_it == stats->end()) { - return 0; - } - int64_t value = atoll(stats_it->second.c_str()); - return value; - } - - void - getStat(const string &key, double &value, int overrideType = 0) - { - string strtmp; - int typetmp; - getStat(key, value, strtmp, typetmp, overrideType); - } - - void - getStat(const string &key, string &value) - { - map::const_iterator lookup_it = lookup_table.find(key); - ink_assert(lookup_it != lookup_table.end()); - const LookupItem &item = lookup_it->second; - - map::const_iterator stats_it = _stats->find(item.name); - if (stats_it == _stats->end()) { - value = ""; - } else { - value = stats_it->second.c_str(); - } - } - - void - getStat(const string &key, double &value, string &prettyName, int &type, int overrideType = 0) - { - // set default value - value = 0; - - map::const_iterator lookup_it = lookup_table.find(key); - ink_assert(lookup_it != lookup_table.end()); - const LookupItem &item = lookup_it->second; - prettyName = item.pretty; - if (overrideType != 0) { - type = overrideType; - } else { - type = item.type; - } - - if (type == 1 || type == 2 || type == 5 || type == 8) { - value = getValue(item.name, _stats.get()); - if (key == "total_time") { - value = value / 10000000; - } - - if ((type == 2 || type == 5 || type == 8) && _old_stats != nullptr && _absolute == false) { - double old = getValue(item.name, _old_stats.get()); - if (key == "total_time") { - old = old / 10000000; - } - value = _time_diff ? (value - old) / _time_diff : 0; - } - } else if (type == 3 || type == 4) { - double numerator = 0; - double denominator = 0; - getStat(item.numerator, numerator); - getStat(item.denominator, denominator); - if (denominator == 0) { - value = 0; - } else { - value = numerator / denominator; - } - if (type == 4) { - value *= 100; - } - } else if (type == 6 || type == 7) { - // add rate - double first; - double second; - getStat(item.numerator, first, 2); - getStat(item.denominator, second, 2); - value = first + second; - if (type == 7) { - value *= 8; - } - } else if (type == 9) { - // add - double first; - double second; - getStat(item.numerator, first); - getStat(item.denominator, second); - value = first + second; - } - - if (type == 8) { - double denominator; - getStat(item.denominator, denominator, 2); - if (denominator == 0) { - value = 0; - } else { - value = value / denominator * 1000; - } - } - - if (type == 5) { - double denominator = 0; - getStat("client_req", denominator); - if (denominator == 0) { - value = 0; - } else { - value = value / denominator * 100; - } - } - } - - bool - toggleAbsolute() - { - if (_absolute == true) { - _absolute = false; - } else { - _absolute = true; - } - - return _absolute; - } - - void - parseResponse(const string &response) - { - // move past global - size_t pos = response.find(constant::global); - pos += sizeof(constant::global) - 1; - - // find parts of the line - while (true) { - size_t start = response.find(constant::start, pos); - size_t separator = response.find(constant::separator, pos); - size_t end = response.find(constant::end, pos); - - if (start == string::npos || separator == string::npos || end == string::npos) { - return; - } - - // cout << constant::start << " " << start << endl; - // cout << constant::separator << " " << separator << endl; - // cout << constant::end << " " << end << endl; - - string key = response.substr(start + 1, separator - start - 1); - string value = - response.substr(separator + sizeof(constant::separator) - 1, end - separator - sizeof(constant::separator) + 1); - - (*_stats)[key] = value; - // cout << "key " << key << " " << "value " << value << endl; - pos = end + sizeof(constant::end) - 1; - // cout << "pos: " << pos << endl; - } - } - - const string & - getHost() const - { - return _host; - } - - ~Stats() {} - -private: - std::pair - make_pair(std::string s, LookupItem i) - { - return std::make_pair(s, i); - } - - /// Invoke the remote server and fill the responses into the stats map. - std::string - fetch_and_fill_stats(shared::rpc::RecordLookupRequest const &request, std::map *stats) noexcept - { - namespace rpc = shared::rpc; - - if (stats == nullptr) { - return "Invalid stats parameter, it shouldn't be null."; - } - try { - rpc::RPCClient rpcClient; - - // invoke the rpc. - auto const &rpcResponse = rpcClient.invoke<>(request, std::chrono::milliseconds(1000), 10); - - if (!rpcResponse.is_error()) { - auto const &records = rpcResponse.result.as(); - - // we check if we got some specific record error, if any we report it. - if (records.errorList.size()) { - std::stringstream ss; - - for (auto const &err : records.errorList) { - ss << err; - ss << "----\n"; - } - return ss.str(); - } else { - // No records error, so we are good to fill the list - for (auto &&recordInfo : records.recordList) { - (*stats)[recordInfo.name] = recordInfo.currentValue; - } - } - } else { - // something didn't work inside the RPC server. - std::stringstream ss; - ss << rpcResponse.error.as(); - return ss.str(); - } - } catch (std::exception const &ex) { - return {ex.what()}; - } - return {}; // no error - } - - std::unique_ptr> _stats; - std::unique_ptr> _old_stats; - map lookup_table; - string _host; - double _old_time; - double _now; - double _time_diff; - struct timeval _time; - bool _absolute; -}; diff --git a/src/traffic_top/traffic_top.cc b/src/traffic_top/traffic_top.cc index e101123eb93..36f4783e935 100644 --- a/src/traffic_top/traffic_top.cc +++ b/src/traffic_top/traffic_top.cc @@ -2,6 +2,16 @@ Main file for the traffic_top application. + traffic_top is a real-time monitoring tool for Apache Traffic Server (ATS). + It displays statistics in a curses-based terminal UI. + + Features: + - Real-time display of cache hits, requests, connections, bandwidth + - Multiple pages for different stat categories (responses, cache, SSL, etc.) + - Graph visualization of key metrics over time + - Batch mode for scripting with JSON/text output + - Responsive layout adapting to terminal size (80, 120, 160+ columns) + @section license License Licensed to the Apache Software Foundation (ASF) under one @@ -21,477 +31,465 @@ limitations under the License. */ -#include "tscore/ink_config.h" -#include -#include -#include -#include -#include -#include #include +#include +#include #include -#include - -// At least on solaris, the default ncurses defines macros such as -// clear() that break stdlibc++. -#define NOMACROS 1 -#define NCURSES_NOMACROS 1 - -#if defined HAVE_NCURSESW_CURSES_H -#include -#elif defined HAVE_NCURSESW_H -#include -#elif defined HAVE_NCURSES_CURSES_H -#include -#elif defined HAVE_NCURSES_H -#include -#elif defined HAVE_CURSES_H -#include -#else -#error "SysV or X/Open-compatible Curses header file required" -#endif - -#include "stats.h" +#include #include "tscore/Layout.h" #include "tscore/ink_args.h" #include "tscore/Version.h" #include "tscore/runroot.h" -using namespace std; +#include "Stats.h" +#include "Display.h" +#include "Output.h" -string response; +using namespace traffic_top; -namespace colorPair +namespace +{ +// Timeout constants (in milliseconds) +constexpr int FIRST_DISPLAY_TIMEOUT_MS = 1000; // Initial display timeout for responsiveness +constexpr int CONNECT_RETRY_TIMEOUT_MS = 500; // Timeout between connection retry attempts +constexpr int MAX_CONNECTION_RETRIES = 10; // Max retries before falling back to normal timeout +constexpr int MS_PER_SECOND = 1000; // Milliseconds per second for timeout conversion + +// Command-line options +int g_sleep_time = 5; // Seconds between updates +int g_count = 0; // Number of iterations (0 = infinite) +int g_batch_mode = 0; // Batch mode flag +int g_ascii_mode = 0; // ASCII mode flag (no Unicode) +int g_json_format = 0; // JSON output format +char g_output_file[1024]; // Output file path + +// ------------------------------------------------------------------------- +// Signal handling +// ------------------------------------------------------------------------- +// We use sig_atomic_t for thread-safe signal flags that can be safely +// accessed from both signal handlers and the main loop. +// +// g_shutdown: Set by SIGINT/SIGTERM to trigger clean exit +// g_window_resized: Set by SIGWINCH to trigger terminal size refresh +// ------------------------------------------------------------------------- +volatile sig_atomic_t g_shutdown = 0; +volatile sig_atomic_t g_window_resized = 0; + +/** + * Signal handler for SIGINT (Ctrl+C) and SIGTERM. + * Sets the shutdown flag to trigger a clean exit from the main loop. + */ +void +signal_handler(int) { -const short red = 1; -const short yellow = 2; -const short green = 3; -const short blue = 4; -// const short black = 5; -const short grey = 6; -const short cyan = 7; -const short border = 8; -}; // namespace colorPair - -//---------------------------------------------------------------------------- -static void -prettyPrint(const int x, const int y, const double number, const int type) + g_shutdown = 1; +} + +/** + * Signal handler for SIGWINCH (window resize). + * Sets a flag that the main loop checks to refresh terminal dimensions. + */ +void +resize_handler(int) { - char buffer[32]; - char exp = ' '; - double my_number = number; - short color; - if (number > 1000000000000LL) { - my_number = number / 1000000000000LL; - exp = 'T'; - color = colorPair::red; - } else if (number > 1000000000) { - my_number = number / 1000000000; - exp = 'G'; - color = colorPair::red; - } else if (number > 1000000) { - my_number = number / 1000000; - exp = 'M'; - color = colorPair::yellow; - } else if (number > 1000) { - my_number = number / 1000; - exp = 'K'; - color = colorPair::cyan; - } else if (my_number <= .09) { - color = colorPair::grey; - } else { - color = colorPair::green; + g_window_resized = 1; +} + +/** + * Register signal handlers for clean shutdown and window resize. + * + * SIGINT/SIGTERM: Trigger clean shutdown (restore terminal, exit gracefully) + * SIGWINCH: Trigger terminal size refresh for responsive layout + */ +void +setup_signals() +{ + // Handler for clean shutdown on Ctrl+C or kill + struct sigaction sa; + sa.sa_handler = signal_handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sigaction(SIGINT, &sa, nullptr); + sigaction(SIGTERM, &sa, nullptr); + + // Handler for terminal window resize + // SA_RESTART ensures system calls aren't interrupted by this signal + struct sigaction sa_resize; + sa_resize.sa_handler = resize_handler; + sigemptyset(&sa_resize.sa_mask); + sa_resize.sa_flags = SA_RESTART; + sigaction(SIGWINCH, &sa_resize, nullptr); +} + +/** + * Run in interactive curses mode. + * + * This is the main event loop for the interactive TUI. It: + * 1. Initializes the display with ncurses for input handling + * 2. Fetches stats from ATS via RPC on each iteration + * 3. Renders the current page based on terminal size + * 4. Handles keyboard input for navigation and mode switching + * + * The loop uses a timeout-based approach: + * - Quick timeout (500ms) during initial connection attempts + * - Normal timeout (sleep_time) once connected + * + * Display modes: + * - Absolute: Shows raw counter values (useful at startup before rates can be calculated) + * - Rate: Shows per-second rates (automatically enabled once we have two data points) + * + * @param stats Reference to the Stats object for fetching ATS metrics + * @param sleep_time Seconds between stat refreshes (user-configurable) + * @param ascii_mode If true, use ASCII characters instead of Unicode box-drawing + * @return 0 on success, 1 on error + */ +int +run_interactive(Stats &stats, int sleep_time, bool ascii_mode) +{ + Display display; + display.setAsciiMode(ascii_mode); + + if (!display.initialize()) { + fprintf(stderr, "Failed to initialize display\n"); + return 1; } - if (type == 4 || type == 5) { - if (number > 90) { - color = colorPair::red; - } else if (number > 80) { - color = colorPair::yellow; - } else if (number > 50) { - color = colorPair::blue; - } else if (my_number <= .09) { - color = colorPair::grey; + // State variables for the main loop + Page current_page = Page::Main; // Currently displayed page + bool connected = false; // Whether we have a successful RPC connection + int anim_frame = 0; // Animation frame for "connecting" spinner + bool first_display = true; // True until first successful render + int connect_retry = 0; // Number of connection retry attempts + bool user_toggled_mode = false; // True if user manually pressed 'a' to toggle mode + bool running = true; // Main loop control flag (false = exit) + + // Try initial connection - start with absolute values since we can't calculate rates yet + if (stats.getStats()) { + connected = true; + } + + while (running && !g_shutdown) { + // Handle window resize - terminal size is re-read in render() via ioctl() + if (g_window_resized) { + g_window_resized = 0; + } + + // Auto-switch from absolute to rate mode once we can calculate rates + // (unless user has manually toggled the mode) + if (!user_toggled_mode && stats.isAbsolute() && stats.canCalculateRates()) { + stats.setAbsolute(false); + } + + // Render current page + display.render(stats, current_page, stats.isAbsolute()); + + // Draw status bar + std::string host_display = stats.getHost(); + if (!connected) { + const char *anim = "|/-\\"; + host_display = std::string("connecting ") + anim[anim_frame % 4]; + ++anim_frame; + } + display.drawStatusBar(host_display, current_page, stats.isAbsolute(), connected); + fflush(stdout); + + // Use short timeout when first starting or still connecting + // This allows quick display updates and responsive connection retry + int current_timeout; + if (first_display && connected) { + // First successful display - short timeout for responsiveness + current_timeout = FIRST_DISPLAY_TIMEOUT_MS; + first_display = false; + } else if (!connected && connect_retry < MAX_CONNECTION_RETRIES) { + // Still trying to connect - retry quickly + current_timeout = CONNECT_RETRY_TIMEOUT_MS; + ++connect_retry; } else { - color = colorPair::green; + // Normal operation - use configured sleep time + current_timeout = sleep_time * MS_PER_SECOND; + } + + // getInput() blocks for up to current_timeout milliseconds, then returns -1 + // This allows the UI to update even if no key is pressed + int ch = display.getInput(current_timeout); + + // ------------------------------------------------------------------------- + // Keyboard input handling + // ------------------------------------------------------------------------- + // Navigation keys: + // 1-8 - Jump directly to page N + // Left/m - Previous page (wraps around) + // Right/r - Next page (wraps around) + // h/? - Show help page + // b/ESC - Return from help to main + // + // Mode keys: + // a - Toggle absolute/rate display mode + // q - Quit the application + // ------------------------------------------------------------------------- + switch (ch) { + // Quit application + case 'q': + case 'Q': + running = false; + break; + + // Show help page + case 'h': + case 'H': + case '?': + current_page = Page::Help; + break; + + // Direct page navigation (1-8) + case '1': + current_page = Page::Main; + break; + case '2': + current_page = Page::Response; + break; + case '3': + current_page = Page::Connection; + break; + case '4': + current_page = Page::Cache; + break; + case '5': + current_page = Page::SSL; + break; + case '6': + current_page = Page::Errors; + break; + case '7': + case 'p': + case 'P': + current_page = Page::Performance; + break; + case '8': + case 'g': + case 'G': + current_page = Page::Graphs; + break; + + // Toggle between absolute values and per-second rates + case 'a': + case 'A': + stats.toggleAbsolute(); + user_toggled_mode = true; // Disable auto-switch once user takes control + break; + + // Navigate to previous page (with wraparound) + case Display::KEY_LEFT: + case 'm': + case 'M': + if (current_page != Page::Help) { + int p = static_cast(current_page); + if (p > 0) { + current_page = static_cast(p - 1); + } else { + // Wrap to last page + current_page = static_cast(Display::getPageCount() - 1); + } + } + break; + + // Navigate to next page (with wraparound) + case Display::KEY_RIGHT: + case 'r': + case 'R': + if (current_page != Page::Help) { + int p = static_cast(current_page); + if (p < Display::getPageCount() - 1) { + current_page = static_cast(p + 1); + } else { + // Wrap to first page + current_page = Page::Main; + } + } + break; + + // Return from help page + case 'b': + case 'B': + case 0x7f: // Backspace (ASCII DEL) + case 0x08: // Backspace (ASCII BS) + case 27: // ESC key + if (current_page == Page::Help) { + current_page = Page::Main; + } + break; + + default: + // Any other key exits help page (convenience feature) + if (current_page == Page::Help && ch != Display::KEY_NONE) { + current_page = Page::Main; + } + break; + } + + // Refresh stats + bool was_connected = connected; + connected = stats.getStats(); + + // Reset retry counter when we successfully connect + if (connected && !was_connected) { + connect_retry = 0; } - snprintf(buffer, sizeof(buffer), "%6.1f%%%%", my_number); - } else { - snprintf(buffer, sizeof(buffer), "%6.1f%c", my_number, exp); } - attron(COLOR_PAIR(color)); - attron(A_BOLD); - mvprintw(y, x, "%s", buffer); - attroff(COLOR_PAIR(color)); - attroff(A_BOLD); + + display.shutdown(); + return 0; } -//---------------------------------------------------------------------------- -static void -makeTable(const int x, const int y, const list &items, Stats &stats) +/** + * Run in batch mode (non-interactive). + * + * Batch mode outputs statistics in a machine-readable format (JSON or text) + * suitable for scripting, logging, or piping to other tools. Unlike interactive + * mode, it doesn't use curses and writes directly to stdout or a file. + * + * Output formats: + * - Text: Tab-separated values with column headers (vmstat-style) + * - JSON: One JSON object per line with timestamp, host, and stat values + * + * @param stats Reference to the Stats object for fetching ATS metrics + * @param sleep_time Seconds to wait between iterations + * @param count Number of iterations (-1 for infinite, 0 defaults to 1) + * @param format Output format (Text or JSON) + * @param output_path File path to write output (empty string = stdout) + * @return 0 on success, 1 on error + */ +int +run_batch(Stats &stats, int sleep_time, int count, OutputFormat format, const char *output_path) { - int my_y = y; + // Open output file if specified, otherwise use stdout + FILE *output = stdout; + + if (output_path[0] != '\0') { + output = fopen(output_path, "w"); + if (!output) { + fprintf(stderr, "Error: Cannot open output file '%s': %s\n", output_path, strerror(errno)); + return 1; + } + } - for (const auto &item : items) { - string prettyName; - double value = 0; - int type; + Output out(format, output); - stats.getStat(item, value, prettyName, type); - mvprintw(my_y, x, "%s", prettyName.c_str()); - prettyPrint(x + 10, my_y++, value, type); + // In batch mode, default to single iteration if count not specified + // This makes `traffic_top -b` useful for one-shot queries + if (count == 0) { + count = 1; } -} -//---------------------------------------------------------------------------- -size_t -write_data(void *ptr, size_t size, size_t nmemb, void * /* stream */) -{ - response.append(static_cast(ptr), size * nmemb); - return size * nmemb; -} + // Main batch loop - runs until count reached or signal received + int iterations = 0; + while (!g_shutdown && (count < 0 || iterations < count)) { + // Fetch stats from ATS via RPC + if (!stats.getStats()) { + out.printError(stats.getLastError()); + if (output != stdout) { + fclose(output); + } + return 1; + } -//---------------------------------------------------------------------------- -static void -response_code_page(Stats &stats) -{ - attron(COLOR_PAIR(colorPair::border)); - attron(A_BOLD); - mvprintw(0, 0, " RESPONSE CODES "); - attroff(COLOR_PAIR(colorPair::border)); - attroff(A_BOLD); - - list response1; - response1.push_back("100"); - response1.push_back("101"); - response1.push_back("1xx"); - response1.push_back("200"); - response1.push_back("201"); - response1.push_back("202"); - response1.push_back("203"); - response1.push_back("204"); - response1.push_back("205"); - response1.push_back("206"); - response1.push_back("2xx"); - response1.push_back("300"); - response1.push_back("301"); - response1.push_back("302"); - response1.push_back("303"); - response1.push_back("304"); - response1.push_back("305"); - response1.push_back("307"); - response1.push_back("3xx"); - makeTable(0, 1, response1, stats); - - list response2; - response2.push_back("400"); - response2.push_back("401"); - response2.push_back("402"); - response2.push_back("403"); - response2.push_back("404"); - response2.push_back("405"); - response2.push_back("406"); - response2.push_back("407"); - response2.push_back("408"); - response2.push_back("409"); - response2.push_back("410"); - response2.push_back("411"); - response2.push_back("412"); - response2.push_back("413"); - response2.push_back("414"); - response2.push_back("415"); - response2.push_back("416"); - response2.push_back("4xx"); - makeTable(21, 1, response2, stats); - - list response3; - response3.push_back("500"); - response3.push_back("501"); - response3.push_back("502"); - response3.push_back("503"); - response3.push_back("504"); - response3.push_back("505"); - response3.push_back("5xx"); - makeTable(42, 1, response3, stats); -} + // Output the stats in the requested format + out.printStats(stats); + ++iterations; -//---------------------------------------------------------------------------- -static void -help(const string &host, const string &version) -{ - timeout(1000); - - while (true) { - clear(); - time_t now = time(nullptr); - struct tm nowtm; - char timeBuf[32]; - localtime_r(&now, &nowtm); - strftime(timeBuf, sizeof(timeBuf), "%H:%M:%S", &nowtm); - - // clear(); - attron(A_BOLD); - mvprintw(0, 0, "Overview:"); - attroff(A_BOLD); - mvprintw( - 1, 0, - "traffic_top is a top like program for Apache Traffic Server (ATS). " - "There is a lot of statistical information gathered by ATS. " - "This program tries to show some of the more important stats and gives a good overview of what the proxy server is doing. " - "Hopefully this can be used as a tool for diagnosing the proxy server if there are problems."); - - attron(A_BOLD); - mvprintw(7, 0, "Definitions:"); - attroff(A_BOLD); - mvprintw(8, 0, "Fresh => Requests that were served by fresh entries in cache"); - mvprintw(9, 0, "Revalidate => Requests that contacted the origin to verify if still valid"); - mvprintw(10, 0, "Cold => Requests that were not in cache at all"); - mvprintw(11, 0, "Changed => Requests that required entries in cache to be updated"); - mvprintw(12, 0, "Changed => Requests that can't be cached for some reason"); - mvprintw(12, 0, "No Cache => Requests that the client sent Cache-Control: no-cache header"); - - attron(COLOR_PAIR(colorPair::border)); - attron(A_BOLD); - mvprintw(23, 0, "%s - %.12s - %.12s (b)ack ", timeBuf, version.c_str(), host.c_str()); - attroff(COLOR_PAIR(colorPair::border)); - attroff(A_BOLD); - refresh(); - int x = getch(); - if (x == 'b') { - break; + // Sleep between iterations (but not after the last one) + if (count < 0 || iterations < count) { + sleep(sleep_time); } } -} -//---------------------------------------------------------------------------- -void -main_stats_page(Stats &stats) -{ - attron(COLOR_PAIR(colorPair::border)); - attron(A_BOLD); - mvprintw(0, 0, " CACHE INFORMATION "); - mvprintw(0, 40, " CLIENT REQUEST & RESPONSE "); - mvprintw(16, 0, " CLIENT "); - mvprintw(16, 40, " ORIGIN SERVER "); - - for (int i = 0; i <= 22; ++i) { - mvprintw(i, 39, " "); + // Clean up output file if we opened one + if (output != stdout) { + fclose(output); } - attroff(COLOR_PAIR(colorPair::border)); - attroff(A_BOLD); - - list cache1; - cache1.push_back("disk_used"); - cache1.push_back("disk_total"); - cache1.push_back("ram_used"); - cache1.push_back("ram_total"); - cache1.push_back("lookups"); - cache1.push_back("cache_writes"); - cache1.push_back("cache_updates"); - cache1.push_back("cache_deletes"); - cache1.push_back("read_active"); - cache1.push_back("write_active"); - cache1.push_back("update_active"); - cache1.push_back("entries"); - cache1.push_back("avg_size"); - cache1.push_back("dns_lookups"); - cache1.push_back("dns_hits"); - makeTable(0, 1, cache1, stats); - - list cache2; - cache2.push_back("ram_ratio"); - cache2.push_back("fresh"); - cache2.push_back("reval"); - cache2.push_back("cold"); - cache2.push_back("changed"); - cache2.push_back("not"); - cache2.push_back("no"); - cache2.push_back("fresh_time"); - cache2.push_back("reval_time"); - cache2.push_back("cold_time"); - cache2.push_back("changed_time"); - cache2.push_back("not_time"); - cache2.push_back("no_time"); - cache2.push_back("dns_ratio"); - cache2.push_back("dns_entry"); - makeTable(21, 1, cache2, stats); - - list response1; - response1.push_back("get"); - response1.push_back("head"); - response1.push_back("post"); - response1.push_back("2xx"); - response1.push_back("3xx"); - response1.push_back("4xx"); - response1.push_back("5xx"); - response1.push_back("conn_fail"); - response1.push_back("other_err"); - response1.push_back("abort"); - makeTable(41, 1, response1, stats); - - list response2; - response2.push_back("200"); - response2.push_back("206"); - response2.push_back("301"); - response2.push_back("302"); - response2.push_back("304"); - response2.push_back("404"); - response2.push_back("502"); - makeTable(62, 1, response2, stats); - - list client1; - client1.push_back("client_req"); - client1.push_back("client_req_conn"); - client1.push_back("client_conn"); - client1.push_back("client_curr_conn"); - client1.push_back("client_actv_conn"); - client1.push_back("client_dyn_ka"); - makeTable(0, 17, client1, stats); - - list client2; - client2.push_back("client_head"); - client2.push_back("client_body"); - client2.push_back("client_avg_size"); - client2.push_back("client_net"); - client2.push_back("client_req_time"); - makeTable(21, 17, client2, stats); - - list server1; - server1.push_back("server_req"); - server1.push_back("server_req_conn"); - server1.push_back("server_conn"); - server1.push_back("server_curr_conn"); - makeTable(41, 17, server1, stats); - - list server2; - server2.push_back("server_head"); - server2.push_back("server_body"); - server2.push_back("server_avg_size"); - server2.push_back("server_net"); - makeTable(62, 17, server2, stats); -} -enum class HostStatus { UP, DOWN }; -char reconnecting_animation[4] = {'|', '/', '-', '\\'}; + return 0; +} -//---------------------------------------------------------------------------- +} // anonymous namespace + +/** + * Main entry point for traffic_top. + * + * Parses command-line arguments and launches either: + * - Interactive mode: curses-based TUI with real-time stats display + * - Batch mode: machine-readable output (JSON or text) for scripting + * + * Example usage: + * traffic_top # Interactive mode with default settings + * traffic_top -s 1 # Update every 1 second + * traffic_top -b -j # Single JSON output to stdout + * traffic_top -b -c 10 -o out.txt # 10 text outputs to file + * traffic_top -a # Use ASCII instead of Unicode + */ int main([[maybe_unused]] int argc, const char **argv) { - static const char USAGE[] = "Usage: traffic_top [-s seconds]"; - - int sleep_time = 6; // In seconds - bool absolute = false; - auto &version = AppVersionInfo::setup_version("traffic_top"); - + static const char USAGE[] = "Usage: traffic_top [options]\n" + "\n" + "Interactive mode (default):\n" + " Display real-time ATS statistics in a curses interface.\n" + " Use number keys (1-8) to switch pages, 'p' for performance, 'g' for graphs, 'q' to quit.\n" + "\n" + "Batch mode (-b):\n" + " Output statistics to stdout/file for scripting.\n"; + + // Initialize output file path to empty string + g_output_file[0] = '\0'; + + // Setup version info for --version output + auto &version = AppVersionInfo::setup_version("traffic_top"); + + // Define command-line arguments + // Format: {name, short_opt, description, type, variable, default, callback} + // Types: "I" = int, "F" = flag (bool), "S1023" = string up to 1023 chars const ArgumentDescription argument_descriptions[] = { - {"sleep", 's', "Sets the delay between updates (in seconds)", "I", &sleep_time, nullptr, nullptr}, + {"sleep", 's', "Seconds between updates (default: 5)", "I", &g_sleep_time, nullptr, nullptr}, + {"count", 'c', "Number of iterations (default: 1 in batch, infinite in interactive)", "I", &g_count, nullptr, nullptr}, + {"batch", 'b', "Batch mode (non-interactive output)", "F", &g_batch_mode, nullptr, nullptr}, + {"output", 'o', "Output file for batch mode (default: stdout)", "S1023", g_output_file, nullptr, nullptr}, + {"json", 'j', "Output in JSON format (batch mode)", "F", &g_json_format, nullptr, nullptr}, + {"ascii", 'a', "Use ASCII characters instead of Unicode", "F", &g_ascii_mode, nullptr, nullptr}, HELP_ARGUMENT_DESCRIPTION(), VERSION_ARGUMENT_DESCRIPTION(), RUNROOT_ARGUMENT_DESCRIPTION(), }; + // Parse command-line arguments (exits on --help or --version) process_args(&version, argument_descriptions, countof(argument_descriptions), argv, USAGE); + // Initialize ATS runroot and layout for finding RPC socket runroot_handler(argv); Layout::create(); - if (n_file_arguments == 1) { - usage(argument_descriptions, countof(argument_descriptions), USAGE); - } else if (n_file_arguments > 1) { - usage(argument_descriptions, countof(argument_descriptions), USAGE); + // Validate arguments + if (g_sleep_time < 1) { + fprintf(stderr, "Error: Sleep time must be at least 1 second\n"); + return 1; } - HostStatus host_status{HostStatus::DOWN}; - Stats stats; - if (stats.getStats()) { - host_status = HostStatus::UP; - } - - const string &host = stats.getHost(); - - initscr(); - curs_set(0); - - start_color(); /* Start color functionality */ + // Setup signal handlers for clean shutdown and window resize + setup_signals(); - init_pair(colorPair::red, COLOR_RED, COLOR_BLACK); - init_pair(colorPair::yellow, COLOR_YELLOW, COLOR_BLACK); - init_pair(colorPair::grey, COLOR_BLACK, COLOR_BLACK); - init_pair(colorPair::green, COLOR_GREEN, COLOR_BLACK); - init_pair(colorPair::blue, COLOR_BLUE, COLOR_BLACK); - init_pair(colorPair::cyan, COLOR_CYAN, COLOR_BLACK); - init_pair(colorPair::border, COLOR_WHITE, COLOR_BLUE); - // mvchgat(0, 0, -1, A_BLINK, 1, nullptr); - - enum Page { - MAIN_PAGE, - RESPONSE_PAGE, - }; - Page page = MAIN_PAGE; - string page_alt = "(r)esponse"; - - int animation_index{0}; - while (true) { - attron(COLOR_PAIR(colorPair::border)); - attron(A_BOLD); - - string version; - time_t now = time(nullptr); - struct tm nowtm; - char timeBuf[32]; - localtime_r(&now, &nowtm); - strftime(timeBuf, sizeof(timeBuf), "%H:%M:%S", &nowtm); - stats.getStat("version", version); - - std::string hh; - if (host_status == HostStatus::DOWN) { - hh.append("connecting "); - hh.append(1, reconnecting_animation[animation_index % 4]); - ++animation_index; - } else { - hh = host; - } - - mvprintw(23, 0, "%-20.20s %30s (q)uit (h)elp (%c)bsolute ", hh.c_str(), page_alt.c_str(), absolute ? 'A' : 'a'); - attroff(COLOR_PAIR(colorPair::border)); - attroff(A_BOLD); - - if (page == MAIN_PAGE) { - main_stats_page(stats); - } else if (page == RESPONSE_PAGE) { - response_code_page(stats); - } + // Create the stats collector (initializes lookup table and validates config) + Stats stats; - curs_set(0); - refresh(); - timeout(sleep_time * 1000); - - int x = getch(); - switch (x) { - case 'h': - help(host, version); - break; - case 'q': - goto quit; - case 'm': - page = MAIN_PAGE; - page_alt = "(r)esponse"; - break; - case 'r': - page = RESPONSE_PAGE; - page_alt = "(m)ain"; - break; - case 'a': - absolute = stats.toggleAbsolute(); - } - host_status = !stats.getStats() ? HostStatus::DOWN : HostStatus::UP; - clear(); + // Run in the appropriate mode + int result; + if (g_batch_mode) { + // Batch mode: output to stdout/file for scripting + OutputFormat format = g_json_format ? OutputFormat::Json : OutputFormat::Text; + result = run_batch(stats, g_sleep_time, g_count, format, g_output_file); + } else { + // Interactive mode: curses-based TUI + result = run_interactive(stats, g_sleep_time, g_ascii_mode != 0); } -quit: - endwin(); - - return 0; + return result; } diff --git a/tests/gold_tests/traffic_top/traffic_top_batch.test.py b/tests/gold_tests/traffic_top/traffic_top_batch.test.py new file mode 100644 index 00000000000..4b20bcb1f2c --- /dev/null +++ b/tests/gold_tests/traffic_top/traffic_top_batch.test.py @@ -0,0 +1,107 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Test traffic_top batch mode output. +""" + +import os + +Test.Summary = ''' +Test traffic_top batch mode with JSON and text output. +''' + +Test.ContinueOnFail = True + +# Get traffic_top path +# Test.TestDirectory is the directory containing this test file +# Navigate up from gold_tests/traffic_top to find the source root +test_dir = Test.TestDirectory +source_root = os.path.dirname(os.path.dirname(os.path.dirname(test_dir))) + +# Look for build directories with traffic_top +build_dirs = ['build-dev-asan', 'build-default', 'build', 'build-autest'] +traffic_top_path = None + +for build_dir in build_dirs: + candidate = os.path.join(source_root, build_dir, 'src', 'traffic_top', 'traffic_top') + if os.path.exists(candidate): + traffic_top_path = candidate + break + # Also check bin/ directory for symlink + candidate = os.path.join(source_root, build_dir, 'bin', 'traffic_top') + if os.path.exists(candidate): + traffic_top_path = candidate + break + +# Fallback to BINDIR if no build directory found +if traffic_top_path is None: + traffic_top_path = os.path.join(Test.Variables.BINDIR, 'traffic_top') + + +class TrafficTopHelper: + """Helper class for traffic_top tests.""" + + def __init__(self, test): + self.test = test + self.ts = test.MakeATSProcess("ts") + self.test_number = 0 + + def add_test(self, name): + """Add a new test run.""" + tr = self.test.AddTestRun(name) + if self.test_number == 0: + tr.Processes.Default.StartBefore(self.ts) + self.test_number += 1 + tr.Processes.Default.Env = self.ts.Env + tr.DelayStart = 2 + tr.StillRunningAfter = self.ts + return tr + + +# Create the helper +helper = TrafficTopHelper(Test) + +# Test 1: JSON output format - check for JSON structure markers +tr = helper.add_test("traffic_top JSON output") +tr.Processes.Default.Command = f"{traffic_top_path} -b -j -c 1" +tr.Processes.Default.ReturnCode = 0 +# JSON output should contain timestamp and host fields +tr.Processes.Default.Streams.stdout = Testers.ContainsExpression('"timestamp"', "JSON should contain timestamp field") + +# Test 2: JSON output contains host field +tr2 = helper.add_test("traffic_top JSON contains host field") +tr2.Processes.Default.Command = f"{traffic_top_path} -b -j -c 1" +tr2.Processes.Default.ReturnCode = 0 +tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression('"host"', "JSON should contain host field") + +# Test 3: Text output format +tr3 = helper.add_test("traffic_top text output") +tr3.Processes.Default.Command = f"{traffic_top_path} -b -c 1" +tr3.Processes.Default.ReturnCode = 0 +# Text output should have header and data lines +tr3.Processes.Default.Streams.stdout = Testers.ContainsExpression("TIMESTAMP", "Text output should contain TIMESTAMP header") + +# Test 4: Help output (argparse returns 64 for --help) +tr4 = helper.add_test("traffic_top help") +tr4.Processes.Default.Command = f"{traffic_top_path} --help" +tr4.Processes.Default.ReturnCode = 64 +tr4.Processes.Default.Streams.stderr = Testers.ContainsExpression("batch", "Help should mention batch mode") + +# Test 5: Version output +tr5 = helper.add_test("traffic_top version") +tr5.Processes.Default.Command = f"{traffic_top_path} --version" +tr5.Processes.Default.ReturnCode = 0 +tr5.Processes.Default.Streams.stdout = Testers.ContainsExpression("traffic_top", "Version should contain program name") diff --git a/tools/traffic_grapher/uv.lock b/tools/traffic_grapher/uv.lock new file mode 100644 index 00000000000..527fa4cb82a --- /dev/null +++ b/tools/traffic_grapher/uv.lock @@ -0,0 +1,1321 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] + +[[package]] +name = "contourpy" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/f6/31a8f28b4a2a4fa0e01085e542f3081ab0588eff8e589d39d775172c9792/contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4", size = 13464370, upload-time = "2024-08-27T21:00:03.328Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/e0/be8dcc796cfdd96708933e0e2da99ba4bb8f9b2caa9d560a50f3f09a65f3/contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7", size = 265366, upload-time = "2024-08-27T20:50:09.947Z" }, + { url = "https://files.pythonhosted.org/packages/50/d6/c953b400219443535d412fcbbc42e7a5e823291236bc0bb88936e3cc9317/contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42", size = 249226, upload-time = "2024-08-27T20:50:16.1Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b4/6fffdf213ffccc28483c524b9dad46bb78332851133b36ad354b856ddc7c/contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7", size = 308460, upload-time = "2024-08-27T20:50:22.536Z" }, + { url = "https://files.pythonhosted.org/packages/cf/6c/118fc917b4050f0afe07179a6dcbe4f3f4ec69b94f36c9e128c4af480fb8/contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab", size = 347623, upload-time = "2024-08-27T20:50:28.806Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a4/30ff110a81bfe3abf7b9673284d21ddce8cc1278f6f77393c91199da4c90/contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589", size = 317761, upload-time = "2024-08-27T20:50:35.126Z" }, + { url = "https://files.pythonhosted.org/packages/99/e6/d11966962b1aa515f5586d3907ad019f4b812c04e4546cc19ebf62b5178e/contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41", size = 322015, upload-time = "2024-08-27T20:50:40.318Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e3/182383743751d22b7b59c3c753277b6aee3637049197624f333dac5b4c80/contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d", size = 1262672, upload-time = "2024-08-27T20:50:55.643Z" }, + { url = "https://files.pythonhosted.org/packages/78/53/974400c815b2e605f252c8fb9297e2204347d1755a5374354ee77b1ea259/contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223", size = 1321688, upload-time = "2024-08-27T20:51:11.293Z" }, + { url = "https://files.pythonhosted.org/packages/52/29/99f849faed5593b2926a68a31882af98afbeac39c7fdf7de491d9c85ec6a/contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f", size = 171145, upload-time = "2024-08-27T20:51:15.2Z" }, + { url = "https://files.pythonhosted.org/packages/a9/97/3f89bba79ff6ff2b07a3cbc40aa693c360d5efa90d66e914f0ff03b95ec7/contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b", size = 216019, upload-time = "2024-08-27T20:51:19.365Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1f/9375917786cb39270b0ee6634536c0e22abf225825602688990d8f5c6c19/contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad", size = 266356, upload-time = "2024-08-27T20:51:24.146Z" }, + { url = "https://files.pythonhosted.org/packages/05/46/9256dd162ea52790c127cb58cfc3b9e3413a6e3478917d1f811d420772ec/contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49", size = 250915, upload-time = "2024-08-27T20:51:28.683Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5d/3056c167fa4486900dfbd7e26a2fdc2338dc58eee36d490a0ed3ddda5ded/contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66", size = 310443, upload-time = "2024-08-27T20:51:33.675Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c2/1a612e475492e07f11c8e267ea5ec1ce0d89971be496c195e27afa97e14a/contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081", size = 348548, upload-time = "2024-08-27T20:51:39.322Z" }, + { url = "https://files.pythonhosted.org/packages/45/cf/2c2fc6bb5874158277b4faf136847f0689e1b1a1f640a36d76d52e78907c/contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1", size = 319118, upload-time = "2024-08-27T20:51:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/03/33/003065374f38894cdf1040cef474ad0546368eea7e3a51d48b8a423961f8/contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d", size = 323162, upload-time = "2024-08-27T20:51:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/42/80/e637326e85e4105a802e42959f56cff2cd39a6b5ef68d5d9aee3ea5f0e4c/contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c", size = 1265396, upload-time = "2024-08-27T20:52:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3b/8cbd6416ca1bbc0202b50f9c13b2e0b922b64be888f9d9ee88e6cfabfb51/contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb", size = 1324297, upload-time = "2024-08-27T20:52:21.843Z" }, + { url = "https://files.pythonhosted.org/packages/4d/2c/021a7afaa52fe891f25535506cc861c30c3c4e5a1c1ce94215e04b293e72/contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c", size = 171808, upload-time = "2024-08-27T20:52:25.163Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/804f02ff30a7fae21f98198828d0857439ec4c91a96e20cf2d6c49372966/contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67", size = 217181, upload-time = "2024-08-27T20:52:29.13Z" }, + { url = "https://files.pythonhosted.org/packages/c9/92/8e0bbfe6b70c0e2d3d81272b58c98ac69ff1a4329f18c73bd64824d8b12e/contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f", size = 267838, upload-time = "2024-08-27T20:52:33.911Z" }, + { url = "https://files.pythonhosted.org/packages/e3/04/33351c5d5108460a8ce6d512307690b023f0cfcad5899499f5c83b9d63b1/contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6", size = 251549, upload-time = "2024-08-27T20:52:39.179Z" }, + { url = "https://files.pythonhosted.org/packages/51/3d/aa0fe6ae67e3ef9f178389e4caaaa68daf2f9024092aa3c6032e3d174670/contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639", size = 303177, upload-time = "2024-08-27T20:52:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/56/c3/c85a7e3e0cab635575d3b657f9535443a6f5d20fac1a1911eaa4bbe1aceb/contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c", size = 341735, upload-time = "2024-08-27T20:52:51.05Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/20f7a211a7be966a53f474bc90b1a8202e9844b3f1ef85f3ae45a77151ee/contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06", size = 314679, upload-time = "2024-08-27T20:52:58.473Z" }, + { url = "https://files.pythonhosted.org/packages/6e/be/524e377567defac0e21a46e2a529652d165fed130a0d8a863219303cee18/contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09", size = 320549, upload-time = "2024-08-27T20:53:06.593Z" }, + { url = "https://files.pythonhosted.org/packages/0f/96/fdb2552a172942d888915f3a6663812e9bc3d359d53dafd4289a0fb462f0/contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd", size = 1263068, upload-time = "2024-08-27T20:53:23.442Z" }, + { url = "https://files.pythonhosted.org/packages/2a/25/632eab595e3140adfa92f1322bf8915f68c932bac468e89eae9974cf1c00/contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35", size = 1322833, upload-time = "2024-08-27T20:53:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/69738782e315a1d26d29d71a550dbbe3eb6c653b028b150f70c1a5f4f229/contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb", size = 172681, upload-time = "2024-08-27T20:53:43.05Z" }, + { url = "https://files.pythonhosted.org/packages/0c/89/9830ba00d88e43d15e53d64931e66b8792b46eb25e2050a88fec4a0df3d5/contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b", size = 218283, upload-time = "2024-08-27T20:53:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/53/a1/d20415febfb2267af2d7f06338e82171824d08614084714fb2c1dac9901f/contourpy-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3e1c7fa44aaae40a2247e2e8e0627f4bea3dd257014764aa644f319a5f8600e3", size = 267879, upload-time = "2024-08-27T20:53:51.597Z" }, + { url = "https://files.pythonhosted.org/packages/aa/45/5a28a3570ff6218d8bdfc291a272a20d2648104815f01f0177d103d985e1/contourpy-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:364174c2a76057feef647c802652f00953b575723062560498dc7930fc9b1cb7", size = 251573, upload-time = "2024-08-27T20:53:55.659Z" }, + { url = "https://files.pythonhosted.org/packages/39/1c/d3f51540108e3affa84f095c8b04f0aa833bb797bc8baa218a952a98117d/contourpy-1.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32b238b3b3b649e09ce9aaf51f0c261d38644bdfa35cbaf7b263457850957a84", size = 303184, upload-time = "2024-08-27T20:54:00.225Z" }, + { url = "https://files.pythonhosted.org/packages/00/56/1348a44fb6c3a558c1a3a0cd23d329d604c99d81bf5a4b58c6b71aab328f/contourpy-1.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d51fca85f9f7ad0b65b4b9fe800406d0d77017d7270d31ec3fb1cc07358fdea0", size = 340262, upload-time = "2024-08-27T20:54:05.234Z" }, + { url = "https://files.pythonhosted.org/packages/2b/23/00d665ba67e1bb666152131da07e0f24c95c3632d7722caa97fb61470eca/contourpy-1.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:732896af21716b29ab3e988d4ce14bc5133733b85956316fb0c56355f398099b", size = 313806, upload-time = "2024-08-27T20:54:09.889Z" }, + { url = "https://files.pythonhosted.org/packages/5a/42/3cf40f7040bb8362aea19af9a5fb7b32ce420f645dd1590edcee2c657cd5/contourpy-1.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d73f659398a0904e125280836ae6f88ba9b178b2fed6884f3b1f95b989d2c8da", size = 319710, upload-time = "2024-08-27T20:54:14.536Z" }, + { url = "https://files.pythonhosted.org/packages/05/32/f3bfa3fc083b25e1a7ae09197f897476ee68e7386e10404bdf9aac7391f0/contourpy-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c6c7c2408b7048082932cf4e641fa3b8ca848259212f51c8c59c45aa7ac18f14", size = 1264107, upload-time = "2024-08-27T20:54:29.735Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1e/1019d34473a736664f2439542b890b2dc4c6245f5c0d8cdfc0ccc2cab80c/contourpy-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f317576606de89da6b7e0861cf6061f6146ead3528acabff9236458a6ba467f8", size = 1322458, upload-time = "2024-08-27T20:54:45.507Z" }, + { url = "https://files.pythonhosted.org/packages/22/85/4f8bfd83972cf8909a4d36d16b177f7b8bdd942178ea4bf877d4a380a91c/contourpy-1.3.0-cp313-cp313-win32.whl", hash = "sha256:31cd3a85dbdf1fc002280c65caa7e2b5f65e4a973fcdf70dd2fdcb9868069294", size = 172643, upload-time = "2024-08-27T20:55:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4a/fb3c83c1baba64ba90443626c228ca14f19a87c51975d3b1de308dd2cf08/contourpy-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4553c421929ec95fb07b3aaca0fae668b2eb5a5203d1217ca7c34c063c53d087", size = 218301, upload-time = "2024-08-27T20:55:56.509Z" }, + { url = "https://files.pythonhosted.org/packages/76/65/702f4064f397821fea0cb493f7d3bc95a5d703e20954dce7d6d39bacf378/contourpy-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:345af746d7766821d05d72cb8f3845dfd08dd137101a2cb9b24de277d716def8", size = 278972, upload-time = "2024-08-27T20:54:50.347Z" }, + { url = "https://files.pythonhosted.org/packages/80/85/21f5bba56dba75c10a45ec00ad3b8190dbac7fd9a8a8c46c6116c933e9cf/contourpy-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3bb3808858a9dc68f6f03d319acd5f1b8a337e6cdda197f02f4b8ff67ad2057b", size = 263375, upload-time = "2024-08-27T20:54:54.909Z" }, + { url = "https://files.pythonhosted.org/packages/0a/64/084c86ab71d43149f91ab3a4054ccf18565f0a8af36abfa92b1467813ed6/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:420d39daa61aab1221567b42eecb01112908b2cab7f1b4106a52caaec8d36973", size = 307188, upload-time = "2024-08-27T20:55:00.184Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/d61a4c288dc42da0084b8d9dc2aa219a850767165d7d9a9c364ff530b509/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d63ee447261e963af02642ffcb864e5a2ee4cbfd78080657a9880b8b1868e18", size = 345644, upload-time = "2024-08-27T20:55:05.673Z" }, + { url = "https://files.pythonhosted.org/packages/ca/aa/00d2313d35ec03f188e8f0786c2fc61f589306e02fdc158233697546fd58/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:167d6c890815e1dac9536dca00828b445d5d0df4d6a8c6adb4a7ec3166812fa8", size = 317141, upload-time = "2024-08-27T20:55:11.047Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6a/b5242c8cb32d87f6abf4f5e3044ca397cb1a76712e3fa2424772e3ff495f/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:710a26b3dc80c0e4febf04555de66f5fd17e9cf7170a7b08000601a10570bda6", size = 323469, upload-time = "2024-08-27T20:55:15.914Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a6/73e929d43028a9079aca4bde107494864d54f0d72d9db508a51ff0878593/contourpy-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:75ee7cb1a14c617f34a51d11fa7524173e56551646828353c4af859c56b766e2", size = 1260894, upload-time = "2024-08-27T20:55:31.553Z" }, + { url = "https://files.pythonhosted.org/packages/2b/1e/1e726ba66eddf21c940821df8cf1a7d15cb165f0682d62161eaa5e93dae1/contourpy-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:33c92cdae89ec5135d036e7218e69b0bb2851206077251f04a6c4e0e21f03927", size = 1314829, upload-time = "2024-08-27T20:55:47.837Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/b9f72758adb6ef7397327ceb8b9c39c75711affb220e4f53c745ea1d5a9a/contourpy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8", size = 265518, upload-time = "2024-08-27T20:56:01.333Z" }, + { url = "https://files.pythonhosted.org/packages/ec/22/19f5b948367ab5260fb41d842c7a78dae645603881ea6bc39738bcfcabf6/contourpy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c", size = 249350, upload-time = "2024-08-27T20:56:05.432Z" }, + { url = "https://files.pythonhosted.org/packages/26/76/0c7d43263dd00ae21a91a24381b7e813d286a3294d95d179ef3a7b9fb1d7/contourpy-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca", size = 309167, upload-time = "2024-08-27T20:56:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/96/3b/cadff6773e89f2a5a492c1a8068e21d3fccaf1a1c1df7d65e7c8e3ef60ba/contourpy-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f", size = 348279, upload-time = "2024-08-27T20:56:15.41Z" }, + { url = "https://files.pythonhosted.org/packages/e1/86/158cc43aa549d2081a955ab11c6bdccc7a22caacc2af93186d26f5f48746/contourpy-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc", size = 318519, upload-time = "2024-08-27T20:56:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/05/11/57335544a3027e9b96a05948c32e566328e3a2f84b7b99a325b7a06d2b06/contourpy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2", size = 321922, upload-time = "2024-08-27T20:56:26.983Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e3/02114f96543f4a1b694333b92a6dcd4f8eebbefcc3a5f3bbb1316634178f/contourpy-1.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e", size = 1258017, upload-time = "2024-08-27T20:56:42.246Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3b/bfe4c81c6d5881c1c643dde6620be0b42bf8aab155976dd644595cfab95c/contourpy-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800", size = 1316773, upload-time = "2024-08-27T20:56:58.58Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/c52d2970784383cafb0bd918b6fb036d98d96bbf0bc1befb5d1e31a07a70/contourpy-1.3.0-cp39-cp39-win32.whl", hash = "sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5", size = 171353, upload-time = "2024-08-27T20:57:02.718Z" }, + { url = "https://files.pythonhosted.org/packages/53/23/db9f69676308e094d3c45f20cc52e12d10d64f027541c995d89c11ad5c75/contourpy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843", size = 211817, upload-time = "2024-08-27T20:57:06.328Z" }, + { url = "https://files.pythonhosted.org/packages/d1/09/60e486dc2b64c94ed33e58dcfb6f808192c03dfc5574c016218b9b7680dc/contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c", size = 261886, upload-time = "2024-08-27T20:57:10.863Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/b57f9f7174fcd439a7789fb47d764974ab646fa34d1790551de386457a8e/contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779", size = 311008, upload-time = "2024-08-27T20:57:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/74/fc/5040d42623a1845d4f17a418e590fd7a79ae8cb2bad2b2f83de63c3bdca4/contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4", size = 215690, upload-time = "2024-08-27T20:57:19.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/dc3dcd77ac7460ab7e9d2b01a618cb31406902e50e605a8d6091f0a8f7cc/contourpy-1.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0", size = 261894, upload-time = "2024-08-27T20:57:23.873Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/531642a01cfec39d1682e46b5457b07cf805e3c3c584ec27e2a6223f8f6c/contourpy-1.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102", size = 311099, upload-time = "2024-08-27T20:57:28.58Z" }, + { url = "https://files.pythonhosted.org/packages/38/1e/94bda024d629f254143a134eead69e21c836429a2a6ce82209a00ddcb79a/contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb", size = 215838, upload-time = "2024-08-27T20:57:32.913Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +dependencies = [ + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "fonttools" +version = "4.60.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/c4/db6a7b5eb0656534c3aa2596c2c5e18830d74f1b9aa5aa8a7dff63a0b11d/fonttools-4.60.2.tar.gz", hash = "sha256:d29552e6b155ebfc685b0aecf8d429cb76c14ab734c22ef5d3dea6fdf800c92c", size = 3562254, upload-time = "2025-12-09T13:38:11.835Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/de/9e10a99fb3070accb8884886a41a4ce54e49bf2fa4fc63f48a6cf2061713/fonttools-4.60.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e36fadcf7e8ca6e34d490eef86ed638d6fd9c55d2f514b05687622cfc4a7050", size = 2850403, upload-time = "2025-12-09T13:35:53.14Z" }, + { url = "https://files.pythonhosted.org/packages/e4/40/d5b369d1073b134f600a94a287e13b5bdea2191ba6347d813fa3da00e94a/fonttools-4.60.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e500fc9c04bee749ceabfc20cb4903f6981c2139050d85720ea7ada61b75d5c", size = 2398629, upload-time = "2025-12-09T13:35:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b5/123819369aaf99d1e4dc49f1de1925d4edc7379114d15a56a7dd2e9d56e6/fonttools-4.60.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22efea5e784e1d1cd8d7b856c198e360a979383ebc6dea4604743b56da1cbc34", size = 4893471, upload-time = "2025-12-09T13:35:58.927Z" }, + { url = "https://files.pythonhosted.org/packages/24/29/f8f8acccb9716b899be4be45e9ce770d6aa76327573863e68448183091b0/fonttools-4.60.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:677aa92d84d335e4d301d8ba04afca6f575316bc647b6782cb0921943fcb6343", size = 4854686, upload-time = "2025-12-09T13:36:01.767Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0d/f3f51d7519f44f2dd5c9a60d7cd41185ebcee4348f073e515a3a93af15ff/fonttools-4.60.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:edd49d3defbf35476e78b61ff737ff5efea811acff68d44233a95a5a48252334", size = 4871233, upload-time = "2025-12-09T13:36:06.094Z" }, + { url = "https://files.pythonhosted.org/packages/cc/3f/4d4fd47d3bc40ab4d76718555185f8adffb5602ea572eac4bbf200c47d22/fonttools-4.60.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:126839492b69cecc5baf2bddcde60caab2ffafd867bbae2a88463fce6078ca3a", size = 4988936, upload-time = "2025-12-09T13:36:08.42Z" }, + { url = "https://files.pythonhosted.org/packages/01/6f/83bbdefa43f2c3ae206fd8c4b9a481f3c913eef871b1ce9a453069239e39/fonttools-4.60.2-cp310-cp310-win32.whl", hash = "sha256:ffcab6f5537136046ca902ed2491ab081ba271b07591b916289b7c27ff845f96", size = 2278044, upload-time = "2025-12-09T13:36:10.641Z" }, + { url = "https://files.pythonhosted.org/packages/d4/04/7d9a137e919d6c9ef26704b7f7b2580d9cfc5139597588227aacebc0e3b7/fonttools-4.60.2-cp310-cp310-win_amd64.whl", hash = "sha256:9c68b287c7ffcd29dd83b5f961004b2a54a862a88825d52ea219c6220309ba45", size = 2326522, upload-time = "2025-12-09T13:36:12.981Z" }, + { url = "https://files.pythonhosted.org/packages/e0/80/b7693d37c02417e162cc83cdd0b19a4f58be82c638b5d4ce4de2dae050c4/fonttools-4.60.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a2aed0a7931401b3875265717a24c726f87ecfedbb7b3426c2ca4d2812e281ae", size = 2847809, upload-time = "2025-12-09T13:36:14.884Z" }, + { url = "https://files.pythonhosted.org/packages/f9/9a/9c2c13bf8a6496ac21607d704e74e9cc68ebf23892cf924c9a8b5c7566b9/fonttools-4.60.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dea6868e9d2b816c9076cfea77754686f3c19149873bdbc5acde437631c15df1", size = 2397302, upload-time = "2025-12-09T13:36:17.151Z" }, + { url = "https://files.pythonhosted.org/packages/56/f6/ce38ff6b2d2d58f6fd981d32f3942365bfa30eadf2b47d93b2d48bf6097f/fonttools-4.60.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fa27f34950aa1fe0f0b1abe25eed04770a3b3b34ad94e5ace82cc341589678a", size = 5054418, upload-time = "2025-12-09T13:36:19.062Z" }, + { url = "https://files.pythonhosted.org/packages/88/06/5353bea128ff39e857c31de3dd605725b4add956badae0b31bc9a50d4c8e/fonttools-4.60.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:13a53d479d187b09bfaa4a35ffcbc334fc494ff355f0a587386099cb66674f1e", size = 5031652, upload-time = "2025-12-09T13:36:21.206Z" }, + { url = "https://files.pythonhosted.org/packages/71/05/ebca836437f6ebd57edd6428e7eff584e683ff0556ddb17d62e3b731f46c/fonttools-4.60.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fac5e921d3bd0ca3bb8517dced2784f0742bc8ca28579a68b139f04ea323a779", size = 5030321, upload-time = "2025-12-09T13:36:23.515Z" }, + { url = "https://files.pythonhosted.org/packages/57/f9/eb9d2a2ce30c99f840c1cc3940729a970923cf39d770caf88909d98d516b/fonttools-4.60.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:648f4f9186fd7f1f3cd57dbf00d67a583720d5011feca67a5e88b3a491952cfb", size = 5154255, upload-time = "2025-12-09T13:36:25.879Z" }, + { url = "https://files.pythonhosted.org/packages/08/a2/088b6ceba8272a9abb629d3c08f9c1e35e5ce42db0ccfe0c1f9f03e60d1d/fonttools-4.60.2-cp311-cp311-win32.whl", hash = "sha256:3274e15fad871bead5453d5ce02658f6d0c7bc7e7021e2a5b8b04e2f9e40da1a", size = 2276300, upload-time = "2025-12-09T13:36:27.772Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/8e4c3d908cc5dade7bb1316ce48589f6a24460c1056fd4b8db51f1fa309a/fonttools-4.60.2-cp311-cp311-win_amd64.whl", hash = "sha256:91d058d5a483a1525b367803abb69de0923fbd45e1f82ebd000f5c8aa65bc78e", size = 2327574, upload-time = "2025-12-09T13:36:30.89Z" }, + { url = "https://files.pythonhosted.org/packages/c0/30/530c9eddcd1c39219dc0aaede2b5a4c8ab80e0bb88d1b3ffc12944c4aac3/fonttools-4.60.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e0164b7609d2b5c5dd4e044b8085b7bd7ca7363ef8c269a4ab5b5d4885a426b2", size = 2847196, upload-time = "2025-12-09T13:36:33.262Z" }, + { url = "https://files.pythonhosted.org/packages/19/2f/4077a482836d5bbe3bc9dac1c004d02ee227cf04ed62b0a2dfc41d4f0dfd/fonttools-4.60.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1dd3d9574fc595c1e97faccae0f264dc88784ddf7fbf54c939528378bacc0033", size = 2395842, upload-time = "2025-12-09T13:36:35.47Z" }, + { url = "https://files.pythonhosted.org/packages/dd/05/aae5bb99c5398f8ed4a8b784f023fd9dd3568f0bd5d5b21e35b282550f11/fonttools-4.60.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98d0719f1b11c2817307d2da2e94296a3b2a3503f8d6252a101dca3ee663b917", size = 4949713, upload-time = "2025-12-09T13:36:37.874Z" }, + { url = "https://files.pythonhosted.org/packages/b4/37/49067349fc78ff0efbf09fadefe80ddf41473ca8f8a25400e3770da38328/fonttools-4.60.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d3ea26957dd07209f207b4fff64c702efe5496de153a54d3b91007ec28904dd", size = 4999907, upload-time = "2025-12-09T13:36:39.853Z" }, + { url = "https://files.pythonhosted.org/packages/16/31/d0f11c758bd0db36b664c92a0f9dfdcc2d7313749aa7d6629805c6946f21/fonttools-4.60.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ee301273b0850f3a515299f212898f37421f42ff9adfc341702582ca5073c13", size = 4939717, upload-time = "2025-12-09T13:36:43.075Z" }, + { url = "https://files.pythonhosted.org/packages/d9/bc/1cff0d69522e561bf1b99bee7c3911c08c25e919584827c3454a64651ce9/fonttools-4.60.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6eb4694cc3b9c03b7c01d65a9cf35b577f21aa6abdbeeb08d3114b842a58153", size = 5089205, upload-time = "2025-12-09T13:36:45.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e6/fb174f0069b7122e19828c551298bfd34fdf9480535d2a6ac2ed37afacd3/fonttools-4.60.2-cp312-cp312-win32.whl", hash = "sha256:57f07b616c69c244cc1a5a51072eeef07dddda5ebef9ca5c6e9cf6d59ae65b70", size = 2264674, upload-time = "2025-12-09T13:36:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/75/57/6552ffd6b582d3e6a9f01780c5275e6dfff1e70ca146101733aa1c12a129/fonttools-4.60.2-cp312-cp312-win_amd64.whl", hash = "sha256:310035802392f1fe5a7cf43d76f6ff4a24c919e4c72c0352e7b8176e2584b8a0", size = 2314701, upload-time = "2025-12-09T13:36:51.09Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e4/8381d0ca6b6c6c484660b03517ec5b5b81feeefca3808726dece36c652a9/fonttools-4.60.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2bb5fd231e56ccd7403212636dcccffc96c5ae0d6f9e4721fa0a32cb2e3ca432", size = 2842063, upload-time = "2025-12-09T13:36:53.468Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2c/4367117ee8ff4f4374787a1222da0bd413d80cf3522111f727a7b8f80d1d/fonttools-4.60.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:536b5fab7b6fec78ccf59b5c59489189d9d0a8b0d3a77ed1858be59afb096696", size = 2393792, upload-time = "2025-12-09T13:36:55.742Z" }, + { url = "https://files.pythonhosted.org/packages/49/b7/a76b6dffa193869e54e32ca2f9abb0d0e66784bc8a24e6f86eb093015481/fonttools-4.60.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b9288fc38252ac86a9570f19313ecbc9ff678982e0f27c757a85f1f284d3400", size = 4924020, upload-time = "2025-12-09T13:36:58.229Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4e/0078200e2259f0061c86a74075f507d64c43dd2ab38971956a5c0012d344/fonttools-4.60.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93fcb420791d839ef592eada2b69997c445d0ce9c969b5190f2e16828ec10607", size = 4980070, upload-time = "2025-12-09T13:37:00.311Z" }, + { url = "https://files.pythonhosted.org/packages/85/1f/d87c85a11cb84852c975251581862681e4a0c1c3bd456c648792203f311b/fonttools-4.60.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7916a381b094db4052ac284255186aebf74c5440248b78860cb41e300036f598", size = 4921411, upload-time = "2025-12-09T13:37:02.345Z" }, + { url = "https://files.pythonhosted.org/packages/75/c0/7efad650f5ed8e317c2633133ef3c64917e7adf2e4e2940c798f5d57ec6e/fonttools-4.60.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:58c8c393d5e16b15662cfc2d988491940458aa87894c662154f50c7b49440bef", size = 5063465, upload-time = "2025-12-09T13:37:04.836Z" }, + { url = "https://files.pythonhosted.org/packages/18/a8/750518c4f8cdd79393b386bc81226047ade80239e58c6c9f5dbe1fdd8ea1/fonttools-4.60.2-cp313-cp313-win32.whl", hash = "sha256:19c6e0afd8b02008caa0aa08ab896dfce5d0bcb510c49b2c499541d5cb95a963", size = 2263443, upload-time = "2025-12-09T13:37:06.762Z" }, + { url = "https://files.pythonhosted.org/packages/b8/22/026c60376f165981f80a0e90bd98a79ae3334e9d89a3d046c4d2e265c724/fonttools-4.60.2-cp313-cp313-win_amd64.whl", hash = "sha256:6a500dc59e11b2338c2dba1f8cf11a4ae8be35ec24af8b2628b8759a61457b76", size = 2313800, upload-time = "2025-12-09T13:37:08.713Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ab/7cf1f5204e1366ddf9dc5cdc2789b571feb9eebcee0e3463c3f457df5f52/fonttools-4.60.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9387c532acbe323bbf2a920f132bce3c408a609d5f9dcfc6532fbc7e37f8ccbb", size = 2841690, upload-time = "2025-12-09T13:37:10.696Z" }, + { url = "https://files.pythonhosted.org/packages/00/3c/0bf83c6f863cc8b934952567fa2bf737cfcec8fc4ffb59b3f93820095f89/fonttools-4.60.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6f1c824185b5b8fb681297f315f26ae55abb0d560c2579242feea8236b1cfef", size = 2392191, upload-time = "2025-12-09T13:37:12.954Z" }, + { url = "https://files.pythonhosted.org/packages/00/f0/40090d148b8907fbea12e9bdf1ff149f30cdf1769e3b2c3e0dbf5106b88d/fonttools-4.60.2-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:55a3129d1e4030b1a30260f1b32fe76781b585fb2111d04a988e141c09eb6403", size = 4873503, upload-time = "2025-12-09T13:37:15.142Z" }, + { url = "https://files.pythonhosted.org/packages/dc/e0/d8b13f99e58b8c293781288ba62fe634f1f0697c9c4c0ae104d3215f3a10/fonttools-4.60.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b196e63753abc33b3b97a6fd6de4b7c4fef5552c0a5ba5e562be214d1e9668e0", size = 4968493, upload-time = "2025-12-09T13:37:18.272Z" }, + { url = "https://files.pythonhosted.org/packages/46/c5/960764d12c92bc225f02401d3067048cb7b282293d9e48e39fe2b0ec38a9/fonttools-4.60.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:de76c8d740fb55745f3b154f0470c56db92ae3be27af8ad6c2e88f1458260c9a", size = 4920015, upload-time = "2025-12-09T13:37:20.334Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ab/839d8caf253d1eef3653ef4d34427d0326d17a53efaec9eb04056b670fff/fonttools-4.60.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ba6303225c95998c9fda2d410aa792c3d2c1390a09df58d194b03e17583fa25", size = 5031165, upload-time = "2025-12-09T13:37:23.57Z" }, + { url = "https://files.pythonhosted.org/packages/de/bf/3bc862796a6841cbe0725bb5512d272239b809dba631a4b0301df885e62d/fonttools-4.60.2-cp314-cp314-win32.whl", hash = "sha256:0a89728ce10d7c816fedaa5380c06d2793e7a8a634d7ce16810e536c22047384", size = 2267526, upload-time = "2025-12-09T13:37:25.821Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a1/c1909cacf00c76dc37b4743451561fbaaf7db4172c22a6d9394081d114c3/fonttools-4.60.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa8446e6ab8bd778b82cb1077058a2addba86f30de27ab9cc18ed32b34bc8667", size = 2319096, upload-time = "2025-12-09T13:37:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/29/b3/f66e71433f08e3a931b2b31a665aeed17fcc5e6911fc73529c70a232e421/fonttools-4.60.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4063bc81ac5a4137642865cb63dd270e37b3cd1f55a07c0d6e41d072699ccca2", size = 2925167, upload-time = "2025-12-09T13:37:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/2e/13/eeb491ff743594bbd0bee6e49422c03a59fe9c49002d3cc60eeb77414285/fonttools-4.60.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebfdb66fa69732ed604ab8e2a0431e6deff35e933a11d73418cbc7823d03b8e1", size = 2430923, upload-time = "2025-12-09T13:37:32.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/db609f785e460796e53c4dbc3874a5f4948477f27beceb5e2d24b2537666/fonttools-4.60.2-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50b10b3b1a72d1d54c61b0e59239e1a94c0958f4a06a1febf97ce75388dd91a4", size = 4877729, upload-time = "2025-12-09T13:37:35.858Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d6/85e4484dd4bfb03fee7bd370d65888cccbd3dee2681ee48c869dd5ccb23f/fonttools-4.60.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:beae16891a13b4a2ddec9b39b4de76092a3025e4d1c82362e3042b62295d5e4d", size = 5096003, upload-time = "2025-12-09T13:37:37.862Z" }, + { url = "https://files.pythonhosted.org/packages/30/49/1a98e44b71030b83d2046f981373b80571868259d98e6dae7bc20099dac6/fonttools-4.60.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:522f017fdb3766fd5d2d321774ef351cc6ce88ad4e6ac9efe643e4a2b9d528db", size = 4974410, upload-time = "2025-12-09T13:37:40.166Z" }, + { url = "https://files.pythonhosted.org/packages/42/07/d6f775d950ee8a841012472c7303f8819423d8cc3b4530915de7265ebfa2/fonttools-4.60.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82cceceaf9c09a965a75b84a4b240dd3768e596ffb65ef53852681606fe7c9ba", size = 5002036, upload-time = "2025-12-09T13:37:42.639Z" }, + { url = "https://files.pythonhosted.org/packages/73/f6/ba6458f83ce1a9f8c3b17bd8f7b8a2205a126aac1055796b7e7cfebbd38f/fonttools-4.60.2-cp314-cp314t-win32.whl", hash = "sha256:bbfbc918a75437fe7e6d64d1b1e1f713237df1cf00f3a36dedae910b2ba01cee", size = 2330985, upload-time = "2025-12-09T13:37:45.157Z" }, + { url = "https://files.pythonhosted.org/packages/91/24/fea0ba4d3a32d4ed1103a1098bfd99dc78b5fe3bb97202920744a37b73dc/fonttools-4.60.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0e5cd9b0830f6550d58c84f3ab151a9892b50c4f9d538c5603c0ce6fff2eb3f1", size = 2396226, upload-time = "2025-12-09T13:37:47.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/ae/a6d9446cb258d3fe87e311c2d7bacf8e8da3e5809fbdc3a8306db4f6b14e/fonttools-4.60.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a3c75b8b42f7f93906bdba9eb1197bb76aecbe9a0a7cf6feec75f7605b5e8008", size = 2857184, upload-time = "2025-12-09T13:37:49.96Z" }, + { url = "https://files.pythonhosted.org/packages/3a/f3/1b41d0b6a8b908aa07f652111155dd653ebbf0b3385e66562556c5206685/fonttools-4.60.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0f86c8c37bc0ec0b9c141d5e90c717ff614e93c187f06d80f18c7057097f71bc", size = 2401877, upload-time = "2025-12-09T13:37:52.307Z" }, + { url = "https://files.pythonhosted.org/packages/71/57/048fd781680c38b05c5463657d0d95d5f2391a51972176e175c01de29d42/fonttools-4.60.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe905403fe59683b0e9a45f234af2866834376b8821f34633b1c76fb731b6311", size = 4878073, upload-time = "2025-12-09T13:37:56.477Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/363364f052a893cebd3d449588b21244a9d873620fda03ad92702d2e1bc7/fonttools-4.60.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38ce703b60a906e421e12d9e3a7f064883f5e61bb23e8961f4be33cfe578500b", size = 4835385, upload-time = "2025-12-09T13:37:58.882Z" }, + { url = "https://files.pythonhosted.org/packages/1c/38/e392bb930b2436287e6021672345db26441bf1f85f1e98f8b9784334e41d/fonttools-4.60.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9e810c06f3e79185cecf120e58b343ea5a89b54dd695fd644446bcf8c026da5e", size = 4853084, upload-time = "2025-12-09T13:38:01.578Z" }, + { url = "https://files.pythonhosted.org/packages/65/60/0d77faeaecf7a3276a8a6dc49e2274357e6b3ed6a1774e2fdb2a7f142db0/fonttools-4.60.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:38faec8cc1d12122599814d15a402183f5123fb7608dac956121e7c6742aebc5", size = 4971144, upload-time = "2025-12-09T13:38:03.748Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c7/6d3ac3afbcd598631bce24c3ecb919e7d0644a82fea8ddc4454312fc0be6/fonttools-4.60.2-cp39-cp39-win32.whl", hash = "sha256:80a45cf7bf659acb7b36578f300231873daba67bd3ca8cce181c73f861f14a37", size = 1499411, upload-time = "2025-12-09T13:38:05.586Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/9dedf6420e23f9fa630bb97941839dddd2e1e57d1b2b85a902378dbe0bd2/fonttools-4.60.2-cp39-cp39-win_amd64.whl", hash = "sha256:c355d5972071938e1b1e0f5a1df001f68ecf1a62f34a3407dc8e0beccf052501", size = 1547943, upload-time = "2025-12-09T13:38:07.604Z" }, + { url = "https://files.pythonhosted.org/packages/79/6c/10280af05b44fafd1dff69422805061fa1af29270bc52dce031ac69540bf/fonttools-4.60.2-py3-none-any.whl", hash = "sha256:73cf92eeda67cf6ff10c8af56fc8f4f07c1647d989a979be9e388a49be26552a", size = 1144610, upload-time = "2025-12-09T13:38:09.5Z" }, +] + +[[package]] +name = "fonttools" +version = "4.62.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/96/686339e0fda8142b7ebed39af53f4a5694602a729662f42a6209e3be91d0/fonttools-4.62.0.tar.gz", hash = "sha256:0dc477c12b8076b4eb9af2e440421b0433ffa9e1dcb39e0640a6c94665ed1098", size = 3579521, upload-time = "2026-03-09T16:50:06.217Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/e0/9db48ec7f6b95bae7b20667ded54f18dba8e759ef66232c8683822ae26fc/fonttools-4.62.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:62b6a3d0028e458e9b59501cf7124a84cd69681c433570e4861aff4fb54a236c", size = 2873527, upload-time = "2026-03-09T16:48:12.416Z" }, + { url = "https://files.pythonhosted.org/packages/dd/45/86eccfdc922cb9fafc63189a9793fa9f6dd60e68a07be42e454ef2c0deae/fonttools-4.62.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:966557078b55e697f65300b18025c54e872d7908d1899b7314d7c16e64868cb2", size = 2417427, upload-time = "2026-03-09T16:48:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/d3/98/f547a1fceeae81a9a5c6461bde2badac8bf50bda7122a8012b32b1e65396/fonttools-4.62.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cf34861145b516cddd19b07ae6f4a61ea1c6326031b960ec9ddce8ee815e888", size = 4934993, upload-time = "2026-03-09T16:48:18.186Z" }, + { url = "https://files.pythonhosted.org/packages/5c/57/a23a051fcff998fdfabdd33c6721b5bad499da08b586d3676993410071f0/fonttools-4.62.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e2ff573de2775508c8a366351fb901c4ced5dc6cf2d87dd15c973bedcdd5216", size = 4892154, upload-time = "2026-03-09T16:48:20.736Z" }, + { url = "https://files.pythonhosted.org/packages/e2/62/e27644b433dc6db1d47bc6028a27d772eec5cc8338e24a9a1fce5d7120aa/fonttools-4.62.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:55b189a1b3033860a38e4e5bd0626c5aa25c7ce9caee7bc784a8caec7a675401", size = 4911635, upload-time = "2026-03-09T16:48:23.174Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e2/1bf141911a5616bacfe9cf237c80ccd69d0d92482c38c0f7f6a55d063ad9/fonttools-4.62.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:825f98cd14907c74a4d0a3f7db8570886ffce9c6369fed1385020febf919abf6", size = 5031492, upload-time = "2026-03-09T16:48:25.095Z" }, + { url = "https://files.pythonhosted.org/packages/2f/59/790c292f4347ecfa77d9c7e0d1d91e04ab227f6e4a337ed4fe37ca388048/fonttools-4.62.0-cp310-cp310-win32.whl", hash = "sha256:c858030560f92a054444c6e46745227bfd3bb4e55383c80d79462cd47289e4b5", size = 1507656, upload-time = "2026-03-09T16:48:26.973Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ee/08c0b7f8bac6e44638de6fe9a3e710a623932f60eccd58912c4d4743516d/fonttools-4.62.0-cp310-cp310-win_amd64.whl", hash = "sha256:9bf75eb69330e34ad2a096fac67887102c8537991eb6cac1507fc835bbb70e0a", size = 1556540, upload-time = "2026-03-09T16:48:30.359Z" }, + { url = "https://files.pythonhosted.org/packages/e4/33/63d79ca41020dd460b51f1e0f58ad1ff0a36b7bcbdf8f3971d52836581e9/fonttools-4.62.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:196cafef9aeec5258425bd31a4e9a414b2ee0d1557bca184d7923d3d3bcd90f9", size = 2870816, upload-time = "2026-03-09T16:48:32.39Z" }, + { url = "https://files.pythonhosted.org/packages/c0/7a/9aeec114bc9fc00d757a41f092f7107863d372e684a5b5724c043654477c/fonttools-4.62.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:153afc3012ff8761b1733e8fbe5d98623409774c44ffd88fbcb780e240c11d13", size = 2416127, upload-time = "2026-03-09T16:48:34.627Z" }, + { url = "https://files.pythonhosted.org/packages/5a/71/12cfd8ae0478b7158ffa8850786781f67e73c00fd897ef9d053415c5f88b/fonttools-4.62.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13b663fb197334de84db790353d59da2a7288fd14e9be329f5debc63ec0500a5", size = 5100678, upload-time = "2026-03-09T16:48:36.454Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d7/8e4845993ee233c2023d11babe9b3dae7d30333da1d792eeccebcb77baab/fonttools-4.62.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:591220d5333264b1df0d3285adbdfe2af4f6a45bbf9ca2b485f97c9f577c49ff", size = 5070859, upload-time = "2026-03-09T16:48:38.786Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a0/287ae04cd883a52e7bb1d92dfc4997dcffb54173761c751106845fa9e316/fonttools-4.62.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:579f35c121528a50c96bf6fcb6a393e81e7f896d4326bf40e379f1c971603db9", size = 5076689, upload-time = "2026-03-09T16:48:41.886Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4e/a2377ad26c36fcd3e671a1c316ea5ed83107de1588e2d897a98349363bc7/fonttools-4.62.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:44956b003151d5a289eba6c71fe590d63509267c37e26de1766ba15d9c589582", size = 5202053, upload-time = "2026-03-09T16:48:43.867Z" }, + { url = "https://files.pythonhosted.org/packages/44/2e/ad0472e69b02f83dc88983a9910d122178461606404be5b4838af6d1744a/fonttools-4.62.0-cp311-cp311-win32.whl", hash = "sha256:42c7848fa8836ab92c23b1617c407a905642521ff2d7897fe2bf8381530172f1", size = 2292852, upload-time = "2026-03-09T16:48:46.962Z" }, + { url = "https://files.pythonhosted.org/packages/77/ce/f5a4c42c117f8113ce04048053c128d17426751a508f26398110c993a074/fonttools-4.62.0-cp311-cp311-win_amd64.whl", hash = "sha256:4da779e8f342a32856075ddb193b2a024ad900bc04ecb744014c32409ae871ed", size = 2344367, upload-time = "2026-03-09T16:48:48.818Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9d/7ad1ffc080619f67d0b1e0fa6a0578f0be077404f13fd8e448d1616a94a3/fonttools-4.62.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:22bde4dc12a9e09b5ced77f3b5053d96cf10c4976c6ac0dee293418ef289d221", size = 2870004, upload-time = "2026-03-09T16:48:50.837Z" }, + { url = "https://files.pythonhosted.org/packages/4d/8b/ba59069a490f61b737e064c3129453dbd28ee38e81d56af0d04d7e6b4de4/fonttools-4.62.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7199c73b326bad892f1cb53ffdd002128bfd58a89b8f662204fbf1daf8d62e85", size = 2414662, upload-time = "2026-03-09T16:48:53.295Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8c/c52a4310de58deeac7e9ea800892aec09b00bb3eb0c53265b31ec02be115/fonttools-4.62.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d732938633681d6e2324e601b79e93f7f72395ec8681f9cdae5a8c08bc167e72", size = 5032975, upload-time = "2026-03-09T16:48:55.718Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a1/d16318232964d786907b9b3613b8409f74cf0be2da400854509d3a864e43/fonttools-4.62.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:31a804c16d76038cc4e3826e07678efb0a02dc4f15396ea8e07088adbfb2578e", size = 4988544, upload-time = "2026-03-09T16:48:57.715Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8d/7e745ca3e65852adc5e52a83dc213fe1b07d61cb5b394970fcd4b1199d1e/fonttools-4.62.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:090e74ac86e68c20150e665ef8e7e0c20cb9f8b395302c9419fa2e4d332c3b51", size = 4971296, upload-time = "2026-03-09T16:48:59.678Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d4/b717a4874175146029ca1517e85474b1af80c9d9a306fc3161e71485eea5/fonttools-4.62.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8f086120e8be9e99ca1288aa5ce519833f93fe0ec6ebad2380c1dee18781f0b5", size = 5122503, upload-time = "2026-03-09T16:49:02.464Z" }, + { url = "https://files.pythonhosted.org/packages/cb/4b/92cfcba4bf8373f51c49c5ae4b512ead6fbda7d61a0e8c35a369d0db40a0/fonttools-4.62.0-cp312-cp312-win32.whl", hash = "sha256:37a73e5e38fd05c637daede6ffed5f3496096be7df6e4a3198d32af038f87527", size = 2281060, upload-time = "2026-03-09T16:49:04.385Z" }, + { url = "https://files.pythonhosted.org/packages/cd/06/cc96468781a4dc8ae2f14f16f32b32f69bde18cb9384aad27ccc7adf76f7/fonttools-4.62.0-cp312-cp312-win_amd64.whl", hash = "sha256:658ab837c878c4d2a652fcbb319547ea41693890e6434cf619e66f79387af3b8", size = 2331193, upload-time = "2026-03-09T16:49:06.598Z" }, + { url = "https://files.pythonhosted.org/packages/82/c7/985c1670aa6d82ef270f04cde11394c168f2002700353bd2bde405e59b8f/fonttools-4.62.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:274c8b8a87e439faf565d3bcd3f9f9e31bca7740755776a4a90a4bfeaa722efa", size = 2864929, upload-time = "2026-03-09T16:49:09.331Z" }, + { url = "https://files.pythonhosted.org/packages/c1/dc/c409c8ceec0d3119e9ab0b7b1a2e3c76d1f4d66e4a9db5c59e6b7652e7df/fonttools-4.62.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93e27131a5a0ae82aaadcffe309b1bae195f6711689722af026862bede05c07c", size = 2412586, upload-time = "2026-03-09T16:49:11.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ac/8e300dbf7b4d135287c261ffd92ede02d9f48f0d2db14665fbc8b059588a/fonttools-4.62.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83c6524c5b93bad9c2939d88e619fedc62e913c19e673f25d5ab74e7a5d074e5", size = 5013708, upload-time = "2026-03-09T16:49:14.063Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bc/60d93477b653eeb1ddf5f9ec34be689b79234d82dbdded269ac0252715b8/fonttools-4.62.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:106aec9226f9498fc5345125ff7200842c01eda273ae038f5049b0916907acee", size = 4964355, upload-time = "2026-03-09T16:49:16.515Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/6dc62bcc3c3598c28a3ecb77e69018869c3e109bd83031d4973c059d318b/fonttools-4.62.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15d86b96c79013320f13bc1b15f94789edb376c0a2d22fb6088f33637e8dfcbc", size = 4953472, upload-time = "2026-03-09T16:49:18.494Z" }, + { url = "https://files.pythonhosted.org/packages/82/b3/3af7592d9b254b7b7fec018135f8776bfa0d1ad335476c2791b1334dc5e4/fonttools-4.62.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f16c07e5250d5d71d0f990a59460bc5620c3cc456121f2cfb5b60475699905f", size = 5094701, upload-time = "2026-03-09T16:49:21.67Z" }, + { url = "https://files.pythonhosted.org/packages/31/3d/976645583ab567d3ee75ff87b33aa1330fa2baeeeae5fc46210b4274dd45/fonttools-4.62.0-cp313-cp313-win32.whl", hash = "sha256:d31558890f3fa00d4f937d12708f90c7c142c803c23eaeb395a71f987a77ebe3", size = 2279710, upload-time = "2026-03-09T16:49:23.812Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/e25245a30457595740041dba9d0ea8ec1b2517f2f1a6a741f15eba1a4edc/fonttools-4.62.0-cp313-cp313-win_amd64.whl", hash = "sha256:6826a5aa53fb6def8a66bf423939745f415546c4e92478a7c531b8b6282b6c3b", size = 2330291, upload-time = "2026-03-09T16:49:26.237Z" }, + { url = "https://files.pythonhosted.org/packages/1a/64/61f69298aa6e7c363dcf00dd6371a654676900abe27d1effd1a74b43e5d0/fonttools-4.62.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:4fa5a9c716e2f75ef34b5a5c2ca0ee4848d795daa7e6792bf30fd4abf8993449", size = 2864222, upload-time = "2026-03-09T16:49:28.285Z" }, + { url = "https://files.pythonhosted.org/packages/c6/57/6b08756fe4455336b1fe160ab3c11fccc90768ccb6ee03fb0b45851aace4/fonttools-4.62.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:625f5cbeb0b8f4e42343eaeb4bc2786718ddd84760a2f5e55fdd3db049047c00", size = 2410674, upload-time = "2026-03-09T16:49:30.504Z" }, + { url = "https://files.pythonhosted.org/packages/6f/86/db65b63bb1b824b63e602e9be21b18741ddc99bcf5a7850f9181159ae107/fonttools-4.62.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6247e58b96b982709cd569a91a2ba935d406dccf17b6aa615afaed37ac3856aa", size = 4999387, upload-time = "2026-03-09T16:49:32.593Z" }, + { url = "https://files.pythonhosted.org/packages/86/c8/c6669e42d2f4efd60d38a3252cebbb28851f968890efb2b9b15f9d1092b0/fonttools-4.62.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:840632ea9c1eab7b7f01c369e408c0721c287dfd7500ab937398430689852fd1", size = 4912506, upload-time = "2026-03-09T16:49:34.927Z" }, + { url = "https://files.pythonhosted.org/packages/2e/49/0ae552aa098edd0ec548413fbf818f52ceb70535016215094a5ce9bf8f70/fonttools-4.62.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:28a9ea2a7467a816d1bec22658b0cce4443ac60abac3e293bdee78beb74588f3", size = 4951202, upload-time = "2026-03-09T16:49:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/71/65/ae38fc8a4cea6f162d74cf11f58e9aeef1baa7d0e3d1376dabd336c129e5/fonttools-4.62.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ae611294f768d413949fd12693a8cba0e6332fbc1e07aba60121be35eac68d0", size = 5060758, upload-time = "2026-03-09T16:49:39.464Z" }, + { url = "https://files.pythonhosted.org/packages/db/3d/bb797496f35c60544cd5af71ffa5aad62df14ef7286908d204cb5c5096fe/fonttools-4.62.0-cp314-cp314-win32.whl", hash = "sha256:273acb61f316d07570a80ed5ff0a14a23700eedbec0ad968b949abaa4d3f6bb5", size = 2283496, upload-time = "2026-03-09T16:49:42.448Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9f/91081ffe5881253177c175749cce5841f5ec6e931f5d52f4a817207b7429/fonttools-4.62.0-cp314-cp314-win_amd64.whl", hash = "sha256:a5f974006d14f735c6c878fc4b117ad031dc93638ddcc450ca69f8fd64d5e104", size = 2335426, upload-time = "2026-03-09T16:49:44.228Z" }, + { url = "https://files.pythonhosted.org/packages/f8/65/f47f9b3db1ec156a1f222f1089ba076b2cc9ee1d024a8b0a60c54258517e/fonttools-4.62.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0361a7d41d86937f1f752717c19f719d0fde064d3011038f9f19bdf5fc2f5c95", size = 2947079, upload-time = "2026-03-09T16:49:46.471Z" }, + { url = "https://files.pythonhosted.org/packages/52/73/bc62e5058a0c22cf02b1e0169ef0c3ca6c3247216d719f95bead3c05a991/fonttools-4.62.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4108c12773b3c97aa592311557c405d5b4fc03db2b969ed928fcf68e7b3c887", size = 2448802, upload-time = "2026-03-09T16:49:48.328Z" }, + { url = "https://files.pythonhosted.org/packages/2b/df/bfaa0e845884935355670e6e68f137185ab87295f8bc838db575e4a66064/fonttools-4.62.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b448075f32708e8fb377fe7687f769a5f51a027172c591ba9a58693631b077a8", size = 5137378, upload-time = "2026-03-09T16:49:50.223Z" }, + { url = "https://files.pythonhosted.org/packages/32/32/04f616979a18b48b52e634988b93d847b6346260faf85ecccaf7e2e9057f/fonttools-4.62.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5f1fa8cc9f1a56a3e33ee6b954d6d9235e6b9d11eb7a6c9dfe2c2f829dc24db", size = 4920714, upload-time = "2026-03-09T16:49:53.172Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2e/274e16689c1dfee5c68302cd7c444213cfddd23cf4620374419625037ec6/fonttools-4.62.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f8c8ea812f82db1e884b9cdb663080453e28f0f9a1f5027a5adb59c4cc8d38d1", size = 5016012, upload-time = "2026-03-09T16:49:55.762Z" }, + { url = "https://files.pythonhosted.org/packages/7f/0c/b08117270626e7117ac2f89d732fdd4386ec37d2ab3a944462d29e6f89a1/fonttools-4.62.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:03c6068adfdc67c565d217e92386b1cdd951abd4240d65180cec62fa74ba31b2", size = 5042766, upload-time = "2026-03-09T16:49:57.726Z" }, + { url = "https://files.pythonhosted.org/packages/11/83/a48b73e54efa272ee65315a6331b30a9b3a98733310bc11402606809c50e/fonttools-4.62.0-cp314-cp314t-win32.whl", hash = "sha256:d28d5baacb0017d384df14722a63abe6e0230d8ce642b1615a27d78ffe3bc983", size = 2347785, upload-time = "2026-03-09T16:49:59.698Z" }, + { url = "https://files.pythonhosted.org/packages/f8/27/c67eab6dc3525bdc39586511b1b3d7161e972dacc0f17476dbaf932e708b/fonttools-4.62.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3f9e20c4618f1e04190c802acae6dc337cb6db9fa61e492fd97cd5c5a9ff6d07", size = 2413914, upload-time = "2026-03-09T16:50:02.251Z" }, + { url = "https://files.pythonhosted.org/packages/9c/57/c2487c281dde03abb2dec244fd67059b8d118bd30a653cbf69e94084cb23/fonttools-4.62.0-py3-none-any.whl", hash = "sha256:75064f19a10c50c74b336aa5ebe7b1f89fd0fb5255807bfd4b0c6317098f4af3", size = 1152427, upload-time = "2026-03-09T16:50:04.074Z" }, +] + +[[package]] +name = "importlib-resources" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/85/4d/2255e1c76304cbd60b48cee302b66d1dde4468dc5b1160e4b7cb43778f2a/kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", size = 97286, upload-time = "2024-09-04T09:39:44.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/14/fc943dd65268a96347472b4fbe5dcc2f6f55034516f80576cd0dd3a8930f/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6", size = 122440, upload-time = "2024-09-04T09:03:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/1e/46/e68fed66236b69dd02fcdb506218c05ac0e39745d696d22709498896875d/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17", size = 65758, upload-time = "2024-09-04T09:03:46.582Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fa/65de49c85838681fc9cb05de2a68067a683717321e01ddafb5b8024286f0/kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9", size = 64311, upload-time = "2024-09-04T09:03:47.973Z" }, + { url = "https://files.pythonhosted.org/packages/42/9c/cc8d90f6ef550f65443bad5872ffa68f3dee36de4974768628bea7c14979/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9", size = 1637109, upload-time = "2024-09-04T09:03:49.281Z" }, + { url = "https://files.pythonhosted.org/packages/55/91/0a57ce324caf2ff5403edab71c508dd8f648094b18cfbb4c8cc0fde4a6ac/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c", size = 1617814, upload-time = "2024-09-04T09:03:51.444Z" }, + { url = "https://files.pythonhosted.org/packages/12/5d/c36140313f2510e20207708adf36ae4919416d697ee0236b0ddfb6fd1050/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599", size = 1400881, upload-time = "2024-09-04T09:03:53.357Z" }, + { url = "https://files.pythonhosted.org/packages/56/d0/786e524f9ed648324a466ca8df86298780ef2b29c25313d9a4f16992d3cf/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05", size = 1512972, upload-time = "2024-09-04T09:03:55.082Z" }, + { url = "https://files.pythonhosted.org/packages/67/5a/77851f2f201e6141d63c10a0708e996a1363efaf9e1609ad0441b343763b/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407", size = 1444787, upload-time = "2024-09-04T09:03:56.588Z" }, + { url = "https://files.pythonhosted.org/packages/06/5f/1f5eaab84355885e224a6fc8d73089e8713dc7e91c121f00b9a1c58a2195/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278", size = 2199212, upload-time = "2024-09-04T09:03:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/b5/28/9152a3bfe976a0ae21d445415defc9d1cd8614b2910b7614b30b27a47270/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5", size = 2346399, upload-time = "2024-09-04T09:04:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/f6/453d1904c52ac3b400f4d5e240ac5fec25263716723e44be65f4d7149d13/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad", size = 2308688, upload-time = "2024-09-04T09:04:02.216Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/d4968499441b9ae187e81745e3277a8b4d7c60840a52dc9d535a7909fac3/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895", size = 2445493, upload-time = "2024-09-04T09:04:04.571Z" }, + { url = "https://files.pythonhosted.org/packages/07/c9/032267192e7828520dacb64dfdb1d74f292765f179e467c1cba97687f17d/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3", size = 2262191, upload-time = "2024-09-04T09:04:05.969Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ad/db0aedb638a58b2951da46ddaeecf204be8b4f5454df020d850c7fa8dca8/kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc", size = 46644, upload-time = "2024-09-04T09:04:07.408Z" }, + { url = "https://files.pythonhosted.org/packages/12/ca/d0f7b7ffbb0be1e7c2258b53554efec1fd652921f10d7d85045aff93ab61/kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c", size = 55877, upload-time = "2024-09-04T09:04:08.869Z" }, + { url = "https://files.pythonhosted.org/packages/97/6c/cfcc128672f47a3e3c0d918ecb67830600078b025bfc32d858f2e2d5c6a4/kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a", size = 48347, upload-time = "2024-09-04T09:04:10.106Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77429fa0a58f941d6e1c58da9efe08597d2e86bf2b2cce6626834f49d07b/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54", size = 122442, upload-time = "2024-09-04T09:04:11.432Z" }, + { url = "https://files.pythonhosted.org/packages/e5/20/8c75caed8f2462d63c7fd65e16c832b8f76cda331ac9e615e914ee80bac9/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95", size = 65762, upload-time = "2024-09-04T09:04:12.468Z" }, + { url = "https://files.pythonhosted.org/packages/f4/98/fe010f15dc7230f45bc4cf367b012d651367fd203caaa992fd1f5963560e/kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935", size = 64319, upload-time = "2024-09-04T09:04:13.635Z" }, + { url = "https://files.pythonhosted.org/packages/8b/1b/b5d618f4e58c0675654c1e5051bcf42c776703edb21c02b8c74135541f60/kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb", size = 1334260, upload-time = "2024-09-04T09:04:14.878Z" }, + { url = "https://files.pythonhosted.org/packages/b8/01/946852b13057a162a8c32c4c8d2e9ed79f0bb5d86569a40c0b5fb103e373/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02", size = 1426589, upload-time = "2024-09-04T09:04:16.514Z" }, + { url = "https://files.pythonhosted.org/packages/70/d1/c9f96df26b459e15cf8a965304e6e6f4eb291e0f7a9460b4ad97b047561e/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51", size = 1541080, upload-time = "2024-09-04T09:04:18.322Z" }, + { url = "https://files.pythonhosted.org/packages/d3/73/2686990eb8b02d05f3de759d6a23a4ee7d491e659007dd4c075fede4b5d0/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052", size = 1470049, upload-time = "2024-09-04T09:04:20.266Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4b/2db7af3ed3af7c35f388d5f53c28e155cd402a55432d800c543dc6deb731/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18", size = 1426376, upload-time = "2024-09-04T09:04:22.419Z" }, + { url = "https://files.pythonhosted.org/packages/05/83/2857317d04ea46dc5d115f0df7e676997bbd968ced8e2bd6f7f19cfc8d7f/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545", size = 2222231, upload-time = "2024-09-04T09:04:24.526Z" }, + { url = "https://files.pythonhosted.org/packages/0d/b5/866f86f5897cd4ab6d25d22e403404766a123f138bd6a02ecb2cdde52c18/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b", size = 2368634, upload-time = "2024-09-04T09:04:25.899Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ee/73de8385403faba55f782a41260210528fe3273d0cddcf6d51648202d6d0/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36", size = 2329024, upload-time = "2024-09-04T09:04:28.523Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/cd101d8cd2cdfaa42dc06c433df17c8303d31129c9fdd16c0ea37672af91/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3", size = 2468484, upload-time = "2024-09-04T09:04:30.547Z" }, + { url = "https://files.pythonhosted.org/packages/e1/72/84f09d45a10bc57a40bb58b81b99d8f22b58b2040c912b7eb97ebf625bf2/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523", size = 2284078, upload-time = "2024-09-04T09:04:33.218Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d4/71828f32b956612dc36efd7be1788980cb1e66bfb3706e6dec9acad9b4f9/kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d", size = 46645, upload-time = "2024-09-04T09:04:34.371Z" }, + { url = "https://files.pythonhosted.org/packages/a1/65/d43e9a20aabcf2e798ad1aff6c143ae3a42cf506754bcb6a7ed8259c8425/kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b", size = 56022, upload-time = "2024-09-04T09:04:35.786Z" }, + { url = "https://files.pythonhosted.org/packages/35/b3/9f75a2e06f1b4ca00b2b192bc2b739334127d27f1d0625627ff8479302ba/kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376", size = 48536, upload-time = "2024-09-04T09:04:37.525Z" }, + { url = "https://files.pythonhosted.org/packages/97/9c/0a11c714cf8b6ef91001c8212c4ef207f772dd84540104952c45c1f0a249/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2", size = 121808, upload-time = "2024-09-04T09:04:38.637Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d8/0fe8c5f5d35878ddd135f44f2af0e4e1d379e1c7b0716f97cdcb88d4fd27/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a", size = 65531, upload-time = "2024-09-04T09:04:39.694Z" }, + { url = "https://files.pythonhosted.org/packages/80/c5/57fa58276dfdfa612241d640a64ca2f76adc6ffcebdbd135b4ef60095098/kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee", size = 63894, upload-time = "2024-09-04T09:04:41.6Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e9/26d3edd4c4ad1c5b891d8747a4f81b1b0aba9fb9721de6600a4adc09773b/kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640", size = 1369296, upload-time = "2024-09-04T09:04:42.886Z" }, + { url = "https://files.pythonhosted.org/packages/b6/67/3f4850b5e6cffb75ec40577ddf54f7b82b15269cc5097ff2e968ee32ea7d/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f", size = 1461450, upload-time = "2024-09-04T09:04:46.284Z" }, + { url = "https://files.pythonhosted.org/packages/52/be/86cbb9c9a315e98a8dc6b1d23c43cffd91d97d49318854f9c37b0e41cd68/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483", size = 1579168, upload-time = "2024-09-04T09:04:47.91Z" }, + { url = "https://files.pythonhosted.org/packages/0f/00/65061acf64bd5fd34c1f4ae53f20b43b0a017a541f242a60b135b9d1e301/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258", size = 1507308, upload-time = "2024-09-04T09:04:49.465Z" }, + { url = "https://files.pythonhosted.org/packages/21/e4/c0b6746fd2eb62fe702118b3ca0cb384ce95e1261cfada58ff693aeec08a/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e", size = 1464186, upload-time = "2024-09-04T09:04:50.949Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0f/529d0a9fffb4d514f2782c829b0b4b371f7f441d61aa55f1de1c614c4ef3/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107", size = 2247877, upload-time = "2024-09-04T09:04:52.388Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e1/66603ad779258843036d45adcbe1af0d1a889a07af4635f8b4ec7dccda35/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948", size = 2404204, upload-time = "2024-09-04T09:04:54.385Z" }, + { url = "https://files.pythonhosted.org/packages/8d/61/de5fb1ca7ad1f9ab7970e340a5b833d735df24689047de6ae71ab9d8d0e7/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038", size = 2352461, upload-time = "2024-09-04T09:04:56.307Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d2/0edc00a852e369827f7e05fd008275f550353f1f9bcd55db9363d779fc63/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383", size = 2501358, upload-time = "2024-09-04T09:04:57.922Z" }, + { url = "https://files.pythonhosted.org/packages/84/15/adc15a483506aec6986c01fb7f237c3aec4d9ed4ac10b756e98a76835933/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520", size = 2314119, upload-time = "2024-09-04T09:04:59.332Z" }, + { url = "https://files.pythonhosted.org/packages/36/08/3a5bb2c53c89660863a5aa1ee236912269f2af8762af04a2e11df851d7b2/kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b", size = 46367, upload-time = "2024-09-04T09:05:00.804Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/c05f0a6d825c643779fc3c70876bff1ac221f0e31e6f701f0e9578690d70/kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb", size = 55884, upload-time = "2024-09-04T09:05:01.924Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f9/3828d8f21b6de4279f0667fb50a9f5215e6fe57d5ec0d61905914f5b6099/kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a", size = 48528, upload-time = "2024-09-04T09:05:02.983Z" }, + { url = "https://files.pythonhosted.org/packages/c4/06/7da99b04259b0f18b557a4effd1b9c901a747f7fdd84cf834ccf520cb0b2/kiwisolver-1.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e", size = 121913, upload-time = "2024-09-04T09:05:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/97/f5/b8a370d1aa593c17882af0a6f6755aaecd643640c0ed72dcfd2eafc388b9/kiwisolver-1.4.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6", size = 65627, upload-time = "2024-09-04T09:05:05.119Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fc/6c0374f7503522539e2d4d1b497f5ebad3f8ed07ab51aed2af988dd0fb65/kiwisolver-1.4.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750", size = 63888, upload-time = "2024-09-04T09:05:06.191Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3e/0b7172793d0f41cae5c923492da89a2ffcd1adf764c16159ca047463ebd3/kiwisolver-1.4.7-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d", size = 1369145, upload-time = "2024-09-04T09:05:07.919Z" }, + { url = "https://files.pythonhosted.org/packages/77/92/47d050d6f6aced2d634258123f2688fbfef8ded3c5baf2c79d94d91f1f58/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379", size = 1461448, upload-time = "2024-09-04T09:05:10.01Z" }, + { url = "https://files.pythonhosted.org/packages/9c/1b/8f80b18e20b3b294546a1adb41701e79ae21915f4175f311a90d042301cf/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c", size = 1578750, upload-time = "2024-09-04T09:05:11.598Z" }, + { url = "https://files.pythonhosted.org/packages/a4/fe/fe8e72f3be0a844f257cadd72689c0848c6d5c51bc1d60429e2d14ad776e/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34", size = 1507175, upload-time = "2024-09-04T09:05:13.22Z" }, + { url = "https://files.pythonhosted.org/packages/39/fa/cdc0b6105d90eadc3bee525fecc9179e2b41e1ce0293caaf49cb631a6aaf/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1", size = 1463963, upload-time = "2024-09-04T09:05:15.925Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5c/0c03c4e542720c6177d4f408e56d1c8315899db72d46261a4e15b8b33a41/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f", size = 2248220, upload-time = "2024-09-04T09:05:17.434Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ee/55ef86d5a574f4e767df7da3a3a7ff4954c996e12d4fbe9c408170cd7dcc/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b", size = 2404463, upload-time = "2024-09-04T09:05:18.997Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6d/73ad36170b4bff4825dc588acf4f3e6319cb97cd1fb3eb04d9faa6b6f212/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27", size = 2352842, upload-time = "2024-09-04T09:05:21.299Z" }, + { url = "https://files.pythonhosted.org/packages/0b/16/fa531ff9199d3b6473bb4d0f47416cdb08d556c03b8bc1cccf04e756b56d/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a", size = 2501635, upload-time = "2024-09-04T09:05:23.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/7e/aa9422e78419db0cbe75fb86d8e72b433818f2e62e2e394992d23d23a583/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee", size = 2314556, upload-time = "2024-09-04T09:05:25.907Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/15f7f556df0a6e5b3772a1e076a9d9f6c538ce5f05bd590eca8106508e06/kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07", size = 46364, upload-time = "2024-09-04T09:05:27.184Z" }, + { url = "https://files.pythonhosted.org/packages/0b/db/32e897e43a330eee8e4770bfd2737a9584b23e33587a0812b8e20aac38f7/kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76", size = 55887, upload-time = "2024-09-04T09:05:28.372Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a4/df2bdca5270ca85fd25253049eb6708d4127be2ed0e5c2650217450b59e9/kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650", size = 48530, upload-time = "2024-09-04T09:05:30.225Z" }, + { url = "https://files.pythonhosted.org/packages/11/88/37ea0ea64512997b13d69772db8dcdc3bfca5442cda3a5e4bb943652ee3e/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd", size = 122449, upload-time = "2024-09-04T09:05:55.311Z" }, + { url = "https://files.pythonhosted.org/packages/4e/45/5a5c46078362cb3882dcacad687c503089263c017ca1241e0483857791eb/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583", size = 65757, upload-time = "2024-09-04T09:05:56.906Z" }, + { url = "https://files.pythonhosted.org/packages/8a/be/a6ae58978772f685d48dd2e84460937761c53c4bbd84e42b0336473d9775/kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417", size = 64312, upload-time = "2024-09-04T09:05:58.384Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/18ef6f452d311e1e1eb180c9bf5589187fa1f042db877e6fe443ef10099c/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904", size = 1626966, upload-time = "2024-09-04T09:05:59.855Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/40655f6c3fa11ce740e8a964fa8e4c0479c87d6a7944b95af799c7a55dfe/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a", size = 1607044, upload-time = "2024-09-04T09:06:02.16Z" }, + { url = "https://files.pythonhosted.org/packages/fd/93/af67dbcfb9b3323bbd2c2db1385a7139d8f77630e4a37bb945b57188eb2d/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8", size = 1391879, upload-time = "2024-09-04T09:06:03.908Z" }, + { url = "https://files.pythonhosted.org/packages/40/6f/d60770ef98e77b365d96061d090c0cd9e23418121c55fff188fa4bdf0b54/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2", size = 1504751, upload-time = "2024-09-04T09:06:05.58Z" }, + { url = "https://files.pythonhosted.org/packages/fa/3a/5f38667d313e983c432f3fcd86932177519ed8790c724e07d77d1de0188a/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88", size = 1436990, upload-time = "2024-09-04T09:06:08.126Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/1520301a47326e6a6043b502647e42892be33b3f051e9791cc8bb43f1a32/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde", size = 2191122, upload-time = "2024-09-04T09:06:10.345Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c4/eb52da300c166239a2233f1f9c4a1b767dfab98fae27681bfb7ea4873cb6/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c", size = 2338126, upload-time = "2024-09-04T09:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/1a/cb/42b92fd5eadd708dd9107c089e817945500685f3437ce1fd387efebc6d6e/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2", size = 2298313, upload-time = "2024-09-04T09:06:14.562Z" }, + { url = "https://files.pythonhosted.org/packages/4f/eb/be25aa791fe5fc75a8b1e0c965e00f942496bc04635c9aae8035f6b76dcd/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb", size = 2437784, upload-time = "2024-09-04T09:06:16.767Z" }, + { url = "https://files.pythonhosted.org/packages/c5/22/30a66be7f3368d76ff95689e1c2e28d382383952964ab15330a15d8bfd03/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327", size = 2253988, upload-time = "2024-09-04T09:06:18.705Z" }, + { url = "https://files.pythonhosted.org/packages/35/d3/5f2ecb94b5211c8a04f218a76133cc8d6d153b0f9cd0b45fad79907f0689/kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644", size = 46980, upload-time = "2024-09-04T09:06:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/ef/17/cd10d020578764ea91740204edc6b3236ed8106228a46f568d716b11feb2/kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4", size = 55847, upload-time = "2024-09-04T09:06:21.407Z" }, + { url = "https://files.pythonhosted.org/packages/91/84/32232502020bd78d1d12be7afde15811c64a95ed1f606c10456db4e4c3ac/kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f", size = 48494, upload-time = "2024-09-04T09:06:22.648Z" }, + { url = "https://files.pythonhosted.org/packages/ac/59/741b79775d67ab67ced9bb38552da688c0305c16e7ee24bba7a2be253fb7/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643", size = 59491, upload-time = "2024-09-04T09:06:24.188Z" }, + { url = "https://files.pythonhosted.org/packages/58/cc/fb239294c29a5656e99e3527f7369b174dd9cc7c3ef2dea7cb3c54a8737b/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706", size = 57648, upload-time = "2024-09-04T09:06:25.559Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ef/2f009ac1f7aab9f81efb2d837301d255279d618d27b6015780115ac64bdd/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6", size = 84257, upload-time = "2024-09-04T09:06:27.038Z" }, + { url = "https://files.pythonhosted.org/packages/81/e1/c64f50987f85b68b1c52b464bb5bf73e71570c0f7782d626d1eb283ad620/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2", size = 80906, upload-time = "2024-09-04T09:06:28.48Z" }, + { url = "https://files.pythonhosted.org/packages/fd/71/1687c5c0a0be2cee39a5c9c389e546f9c6e215e46b691d00d9f646892083/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4", size = 79951, upload-time = "2024-09-04T09:06:29.966Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8b/d7497df4a1cae9367adf21665dd1f896c2a7aeb8769ad77b662c5e2bcce7/kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a", size = 55715, upload-time = "2024-09-04T09:06:31.489Z" }, + { url = "https://files.pythonhosted.org/packages/d5/df/ce37d9b26f07ab90880923c94d12a6ff4d27447096b4c849bfc4339ccfdf/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39", size = 58666, upload-time = "2024-09-04T09:06:43.756Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d3/e4b04f43bc629ac8e186b77b2b1a251cdfa5b7610fa189dc0db622672ce6/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e", size = 57088, upload-time = "2024-09-04T09:06:45.406Z" }, + { url = "https://files.pythonhosted.org/packages/30/1c/752df58e2d339e670a535514d2db4fe8c842ce459776b8080fbe08ebb98e/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608", size = 84321, upload-time = "2024-09-04T09:06:47.557Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f8/fe6484e847bc6e238ec9f9828089fb2c0bb53f2f5f3a79351fde5b565e4f/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674", size = 80776, upload-time = "2024-09-04T09:06:49.235Z" }, + { url = "https://files.pythonhosted.org/packages/9b/57/d7163c0379f250ef763aba85330a19feefb5ce6cb541ade853aaba881524/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225", size = 79984, upload-time = "2024-09-04T09:06:51.336Z" }, + { url = "https://files.pythonhosted.org/packages/8c/95/4a103776c265d13b3d2cd24fb0494d4e04ea435a8ef97e1b2c026d43250b/kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0", size = 55811, upload-time = "2024-09-04T09:06:53.078Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/f8/06549565caa026e540b7e7bab5c5a90eb7ca986015f4c48dace243cd24d9/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374", size = 122802, upload-time = "2026-03-09T13:12:37.515Z" }, + { url = "https://files.pythonhosted.org/packages/84/eb/8476a0818850c563ff343ea7c9c05dcdcbd689a38e01aa31657df01f91fa/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd", size = 66216, upload-time = "2026-03-09T13:12:38.812Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/f9c8a6b4c21aed4198566e45923512986d6cef530e7263b3a5f823546561/kiwisolver-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476", size = 63917, upload-time = "2026-03-09T13:12:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0e/ba4ae25d03722f64de8b2c13e80d82ab537a06b30fc7065183c6439357e3/kiwisolver-1.5.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22", size = 1628776, upload-time = "2026-03-09T13:12:41.976Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e4/3f43a011bc8a0860d1c96f84d32fa87439d3feedf66e672fef03bf5e8bac/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b", size = 1228164, upload-time = "2026-03-09T13:12:44.002Z" }, + { url = "https://files.pythonhosted.org/packages/4b/34/3a901559a1e0c218404f9a61a93be82d45cb8f44453ba43088644980f033/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e", size = 1246656, upload-time = "2026-03-09T13:12:45.557Z" }, + { url = "https://files.pythonhosted.org/packages/87/9e/f78c466ea20527822b95ad38f141f2de1dcd7f23fb8716b002b0d91bbe59/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb", size = 1295562, upload-time = "2026-03-09T13:12:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/0a/66/fd0e4a612e3a286c24e6d6f3a5428d11258ed1909bc530ba3b59807fd980/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537", size = 2178473, upload-time = "2026-03-09T13:12:50.254Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8e/6cac929e0049539e5ee25c1ee937556f379ba5204840d03008363ced662d/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4", size = 2274035, upload-time = "2026-03-09T13:12:51.785Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d3/9d0c18f1b52ea8074b792452cf17f1f5a56bd0302a85191f405cfbf9da16/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c", size = 2443217, upload-time = "2026-03-09T13:12:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/45/2a/6e19368803a038b2a90857bf4ee9e3c7b667216d045866bf22d3439fd75e/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede", size = 2249196, upload-time = "2026-03-09T13:12:55.057Z" }, + { url = "https://files.pythonhosted.org/packages/75/2b/3f641dfcbe72e222175d626bacf2f72c3b34312afec949dd1c50afa400f5/kiwisolver-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2", size = 73389, upload-time = "2026-03-09T13:12:56.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/88/299b137b9e0025d8982e03d2d52c123b0a2b159e84b0ef1501ef446339cf/kiwisolver-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875", size = 64782, upload-time = "2026-03-09T13:12:57.609Z" }, + { url = "https://files.pythonhosted.org/packages/12/dd/a495a9c104be1c476f0386e714252caf2b7eca883915422a64c50b88c6f5/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c", size = 122798, upload-time = "2026-03-09T13:12:58.963Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/37b4047a2af0cf5ef6d8b4b26e91829ae6fc6a2d1f74524bcb0e7cd28a32/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb", size = 66216, upload-time = "2026-03-09T13:13:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac", size = 63911, upload-time = "2026-03-09T13:13:01.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27", size = 1438209, upload-time = "2026-03-09T13:13:03.385Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/76621246f5165e5372f02f5e6f3f48ea336a8f9e96e43997d45b240ed8cd/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398", size = 1248888, upload-time = "2026-03-09T13:13:05.231Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c1/31559ec6fb39a5b48035ce29bb63ade628f321785f38c384dee3e2c08bc1/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db", size = 1266304, upload-time = "2026-03-09T13:13:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ef/1cb8276f2d29cc6a41e0a042f27946ca347d3a4a75acf85d0a16aa6dcc82/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc", size = 1319650, upload-time = "2026-03-09T13:13:08.607Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e4/5ba3cecd7ce6236ae4a80f67e5d5531287337d0e1f076ca87a5abe4cd5d0/kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679", size = 970949, upload-time = "2026-03-09T13:13:10.299Z" }, + { url = "https://files.pythonhosted.org/packages/5a/69/dc61f7ae9a2f071f26004ced87f078235b5507ab6e5acd78f40365655034/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309", size = 2199125, upload-time = "2026-03-09T13:13:11.841Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7b/abbe0f1b5afa85f8d084b73e90e5f801c0939eba16ac2e49af7c61a6c28d/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2", size = 2293783, upload-time = "2026-03-09T13:13:14.399Z" }, + { url = "https://files.pythonhosted.org/packages/8a/80/5908ae149d96d81580d604c7f8aefd0e98f4fd728cf172f477e9f2a81744/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c", size = 1960726, upload-time = "2026-03-09T13:13:16.047Z" }, + { url = "https://files.pythonhosted.org/packages/84/08/a78cb776f8c085b7143142ce479859cfec086bd09ee638a317040b6ef420/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08", size = 2464738, upload-time = "2026-03-09T13:13:17.897Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e1/65584da5356ed6cb12c63791a10b208860ac40a83de165cb6a6751a686e3/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4", size = 2270718, upload-time = "2026-03-09T13:13:19.421Z" }, + { url = "https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b", size = 73480, upload-time = "2026-03-09T13:13:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0e/2ee5debc4f77a625778fec5501ff3e8036fe361b7ee28ae402a485bb9694/kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac", size = 64930, upload-time = "2026-03-09T13:13:21.997Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, + { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, + { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, + { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, + { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" }, + { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" }, + { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, + { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, + { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, + { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, + { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, + { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, + { url = "https://files.pythonhosted.org/packages/17/6f/6fd4f690a40c2582fa34b97d2678f718acf3706b91d270c65ecb455d0a06/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4", size = 59606, upload-time = "2026-03-09T13:15:40.81Z" }, + { url = "https://files.pythonhosted.org/packages/82/a0/2355d5e3b338f13ce63f361abb181e3b6ea5fffdb73f739b3e80efa76159/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca", size = 57537, upload-time = "2026-03-09T13:15:42.071Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b9/1d50e610ecadebe205b71d6728fd224ce0e0ca6aba7b9cbe1da049203ac5/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f", size = 79888, upload-time = "2026-03-09T13:15:43.317Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ee/b85ffcd75afed0357d74f0e6fc02a4507da441165de1ca4760b9f496390d/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed", size = 77584, upload-time = "2026-03-09T13:15:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/6b/dd/644d0dde6010a8583b4cd66dd41c5f83f5325464d15c4f490b3340ab73b4/kiwisolver-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc", size = 73390, upload-time = "2026-03-09T13:15:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/e9/eb/5fcbbbf9a0e2c3a35effb88831a483345326bbc3a030a3b5b69aee647f84/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232", size = 59532, upload-time = "2026-03-09T13:15:47.047Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9b/e17104555bb4db148fd52327feea1e96be4b88e8e008b029002c281a21ab/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a", size = 57420, upload-time = "2026-03-09T13:15:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/48/44/2b5b95b7aa39fb2d8d9d956e0f3d5d45aef2ae1d942d4c3ffac2f9cfed1a/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737", size = 79892, upload-time = "2026-03-09T13:15:49.694Z" }, + { url = "https://files.pythonhosted.org/packages/52/7d/7157f9bba6b455cfb4632ed411e199fc8b8977642c2b12082e1bd9e6d173/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16", size = 77603, upload-time = "2026-03-09T13:15:50.945Z" }, + { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.9.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "contourpy", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "cycler", marker = "python_full_version < '3.10'" }, + { name = "fonttools", version = "4.60.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "importlib-resources", marker = "python_full_version < '3.10'" }, + { name = "kiwisolver", version = "1.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyparsing", marker = "python_full_version < '3.10'" }, + { name = "python-dateutil", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/17/1747b4154034befd0ed33b52538f5eb7752d05bb51c5e2a31470c3bc7d52/matplotlib-3.9.4.tar.gz", hash = "sha256:1e00e8be7393cbdc6fedfa8a6fba02cf3e83814b285db1c60b906a023ba41bc3", size = 36106529, upload-time = "2024-12-13T05:56:34.184Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/94/27d2e2c30d54b56c7b764acc1874a909e34d1965a427fc7092bb6a588b63/matplotlib-3.9.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fdd7abfb706dfa8d307af64a87f1a862879ec3cd8d0ec8637458f0885b9c50", size = 7885089, upload-time = "2024-12-13T05:54:24.224Z" }, + { url = "https://files.pythonhosted.org/packages/c6/25/828273307e40a68eb8e9df832b6b2aaad075864fdc1de4b1b81e40b09e48/matplotlib-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d89bc4e85e40a71d1477780366c27fb7c6494d293e1617788986f74e2a03d7ff", size = 7770600, upload-time = "2024-12-13T05:54:27.214Z" }, + { url = "https://files.pythonhosted.org/packages/f2/65/f841a422ec994da5123368d76b126acf4fc02ea7459b6e37c4891b555b83/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddf9f3c26aae695c5daafbf6b94e4c1a30d6cd617ba594bbbded3b33a1fcfa26", size = 8200138, upload-time = "2024-12-13T05:54:29.497Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/272aca07a38804d93b6050813de41ca7ab0e29ba7a9dd098e12037c919a9/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18ebcf248030173b59a868fda1fe42397253f6698995b55e81e1f57431d85e50", size = 8312711, upload-time = "2024-12-13T05:54:34.396Z" }, + { url = "https://files.pythonhosted.org/packages/98/37/f13e23b233c526b7e27ad61be0a771894a079e0f7494a10d8d81557e0e9a/matplotlib-3.9.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974896ec43c672ec23f3f8c648981e8bc880ee163146e0312a9b8def2fac66f5", size = 9090622, upload-time = "2024-12-13T05:54:36.808Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8c/b1f5bd2bd70e60f93b1b54c4d5ba7a992312021d0ddddf572f9a1a6d9348/matplotlib-3.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:4598c394ae9711cec135639374e70871fa36b56afae17bdf032a345be552a88d", size = 7828211, upload-time = "2024-12-13T05:54:40.596Z" }, + { url = "https://files.pythonhosted.org/packages/74/4b/65be7959a8fa118a3929b49a842de5b78bb55475236fcf64f3e308ff74a0/matplotlib-3.9.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4dd29641d9fb8bc4492420c5480398dd40a09afd73aebe4eb9d0071a05fbe0c", size = 7894430, upload-time = "2024-12-13T05:54:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/e9/18/80f70d91896e0a517b4a051c3fd540daa131630fd75e02e250365353b253/matplotlib-3.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30e5b22e8bcfb95442bf7d48b0d7f3bdf4a450cbf68986ea45fca3d11ae9d099", size = 7780045, upload-time = "2024-12-13T05:54:46.414Z" }, + { url = "https://files.pythonhosted.org/packages/a2/73/ccb381026e3238c5c25c3609ba4157b2d1a617ec98d65a8b4ee4e1e74d02/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bb0030d1d447fd56dcc23b4c64a26e44e898f0416276cac1ebc25522e0ac249", size = 8209906, upload-time = "2024-12-13T05:54:49.459Z" }, + { url = "https://files.pythonhosted.org/packages/ab/33/1648da77b74741c89f5ea95cbf42a291b4b364f2660b316318811404ed97/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aca90ed222ac3565d2752b83dbb27627480d27662671e4d39da72e97f657a423", size = 8322873, upload-time = "2024-12-13T05:54:53.066Z" }, + { url = "https://files.pythonhosted.org/packages/57/d3/8447ba78bc6593c9044c372d1609f8ea10fb1e071e7a9e0747bea74fc16c/matplotlib-3.9.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a181b2aa2906c608fcae72f977a4a2d76e385578939891b91c2550c39ecf361e", size = 9099566, upload-time = "2024-12-13T05:54:55.522Z" }, + { url = "https://files.pythonhosted.org/packages/23/e1/4f0e237bf349c02ff9d1b6e7109f1a17f745263809b9714a8576dc17752b/matplotlib-3.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:1f6882828231eca17f501c4dcd98a05abb3f03d157fbc0769c6911fe08b6cfd3", size = 7838065, upload-time = "2024-12-13T05:54:58.337Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2b/c918bf6c19d6445d1cefe3d2e42cb740fb997e14ab19d4daeb6a7ab8a157/matplotlib-3.9.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dfc48d67e6661378a21c2983200a654b72b5c5cdbd5d2cf6e5e1ece860f0cc70", size = 7891131, upload-time = "2024-12-13T05:55:02.837Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e5/b4e8fc601ca302afeeabf45f30e706a445c7979a180e3a978b78b2b681a4/matplotlib-3.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47aef0fab8332d02d68e786eba8113ffd6f862182ea2999379dec9e237b7e483", size = 7776365, upload-time = "2024-12-13T05:55:05.158Z" }, + { url = "https://files.pythonhosted.org/packages/99/06/b991886c506506476e5d83625c5970c656a491b9f80161458fed94597808/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fba1f52c6b7dc764097f52fd9ab627b90db452c9feb653a59945de16752e965f", size = 8200707, upload-time = "2024-12-13T05:55:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e2/556b627498cb27e61026f2d1ba86a78ad1b836fef0996bef5440e8bc9559/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:173ac3748acaac21afcc3fa1633924609ba1b87749006bc25051c52c422a5d00", size = 8313761, upload-time = "2024-12-13T05:55:12.95Z" }, + { url = "https://files.pythonhosted.org/packages/58/ff/165af33ec766ff818306ea88e91f9f60d2a6ed543be1eb122a98acbf3b0d/matplotlib-3.9.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320edea0cadc07007765e33f878b13b3738ffa9745c5f707705692df70ffe0e0", size = 9095284, upload-time = "2024-12-13T05:55:16.199Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8b/3d0c7a002db3b1ed702731c2a9a06d78d035f1f2fb0fb936a8e43cc1e9f4/matplotlib-3.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a4a4cfc82330b27042a7169533da7991e8789d180dd5b3daeaee57d75cd5a03b", size = 7841160, upload-time = "2024-12-13T05:55:19.991Z" }, + { url = "https://files.pythonhosted.org/packages/49/b1/999f89a7556d101b23a2f0b54f1b6e140d73f56804da1398f2f0bc0924bc/matplotlib-3.9.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37eeffeeca3c940985b80f5b9a7b95ea35671e0e7405001f249848d2b62351b6", size = 7891499, upload-time = "2024-12-13T05:55:22.142Z" }, + { url = "https://files.pythonhosted.org/packages/87/7b/06a32b13a684977653396a1bfcd34d4e7539c5d55c8cbfaa8ae04d47e4a9/matplotlib-3.9.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3e7465ac859ee4abcb0d836137cd8414e7bb7ad330d905abced457217d4f0f45", size = 7776802, upload-time = "2024-12-13T05:55:25.947Z" }, + { url = "https://files.pythonhosted.org/packages/65/87/ac498451aff739e515891bbb92e566f3c7ef31891aaa878402a71f9b0910/matplotlib-3.9.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4c12302c34afa0cf061bea23b331e747e5e554b0fa595c96e01c7b75bc3b858", size = 8200802, upload-time = "2024-12-13T05:55:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6b/9eb761c00e1cb838f6c92e5f25dcda3f56a87a52f6cb8fdfa561e6cf6a13/matplotlib-3.9.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b8c97917f21b75e72108b97707ba3d48f171541a74aa2a56df7a40626bafc64", size = 8313880, upload-time = "2024-12-13T05:55:30.965Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a2/c8eaa600e2085eec7e38cbbcc58a30fc78f8224939d31d3152bdafc01fd1/matplotlib-3.9.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0229803bd7e19271b03cb09f27db76c918c467aa4ce2ae168171bc67c3f508df", size = 9094637, upload-time = "2024-12-13T05:55:33.701Z" }, + { url = "https://files.pythonhosted.org/packages/71/1f/c6e1daea55b7bfeb3d84c6cb1abc449f6a02b181e7e2a5e4db34c3afb793/matplotlib-3.9.4-cp313-cp313-win_amd64.whl", hash = "sha256:7c0d8ef442ebf56ff5e206f8083d08252ee738e04f3dc88ea882853a05488799", size = 7841311, upload-time = "2024-12-13T05:55:36.737Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3a/2757d3f7d388b14dd48f5a83bea65b6d69f000e86b8f28f74d86e0d375bd/matplotlib-3.9.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a04c3b00066a688834356d196136349cb32f5e1003c55ac419e91585168b88fb", size = 7919989, upload-time = "2024-12-13T05:55:39.024Z" }, + { url = "https://files.pythonhosted.org/packages/24/28/f5077c79a4f521589a37fe1062d6a6ea3534e068213f7357e7cfffc2e17a/matplotlib-3.9.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04c519587f6c210626741a1e9a68eefc05966ede24205db8982841826af5871a", size = 7809417, upload-time = "2024-12-13T05:55:42.412Z" }, + { url = "https://files.pythonhosted.org/packages/36/c8/c523fd2963156692916a8eb7d4069084cf729359f7955cf09075deddfeaf/matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308afbf1a228b8b525fcd5cec17f246bbbb63b175a3ef6eb7b4d33287ca0cf0c", size = 8226258, upload-time = "2024-12-13T05:55:47.259Z" }, + { url = "https://files.pythonhosted.org/packages/f6/88/499bf4b8fa9349b6f5c0cf4cead0ebe5da9d67769129f1b5651e5ac51fbc/matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb3b02246ddcffd3ce98e88fed5b238bc5faff10dbbaa42090ea13241d15764", size = 8335849, upload-time = "2024-12-13T05:55:49.763Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9f/20a4156b9726188646a030774ee337d5ff695a965be45ce4dbcb9312c170/matplotlib-3.9.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8a75287e9cb9eee48cb79ec1d806f75b29c0fde978cb7223a1f4c5848d696041", size = 9102152, upload-time = "2024-12-13T05:55:51.997Z" }, + { url = "https://files.pythonhosted.org/packages/10/11/237f9c3a4e8d810b1759b67ff2da7c32c04f9c80aa475e7beb36ed43a8fb/matplotlib-3.9.4-cp313-cp313t-win_amd64.whl", hash = "sha256:488deb7af140f0ba86da003e66e10d55ff915e152c78b4b66d231638400b1965", size = 7896987, upload-time = "2024-12-13T05:55:55.941Z" }, + { url = "https://files.pythonhosted.org/packages/56/eb/501b465c9fef28f158e414ea3a417913dc2ac748564c7ed41535f23445b4/matplotlib-3.9.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3c3724d89a387ddf78ff88d2a30ca78ac2b4c89cf37f2db4bd453c34799e933c", size = 7885919, upload-time = "2024-12-13T05:55:59.66Z" }, + { url = "https://files.pythonhosted.org/packages/da/36/236fbd868b6c91309a5206bd90c3f881f4f44b2d997cd1d6239ef652f878/matplotlib-3.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d5f0a8430ffe23d7e32cfd86445864ccad141797f7d25b7c41759a5b5d17cfd7", size = 7771486, upload-time = "2024-12-13T05:56:04.264Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4b/105caf2d54d5ed11d9f4335398f5103001a03515f2126c936a752ccf1461/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bb0141a21aef3b64b633dc4d16cbd5fc538b727e4958be82a0e1c92a234160e", size = 8201838, upload-time = "2024-12-13T05:56:06.792Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a7/bb01188fb4013d34d274caf44a2f8091255b0497438e8b6c0a7c1710c692/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57aa235109e9eed52e2c2949db17da185383fa71083c00c6c143a60e07e0888c", size = 8314492, upload-time = "2024-12-13T05:56:09.964Z" }, + { url = "https://files.pythonhosted.org/packages/33/19/02e1a37f7141fc605b193e927d0a9cdf9dc124a20b9e68793f4ffea19695/matplotlib-3.9.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b18c600061477ccfdd1e6fd050c33d8be82431700f3452b297a56d9ed7037abb", size = 9092500, upload-time = "2024-12-13T05:56:13.55Z" }, + { url = "https://files.pythonhosted.org/packages/57/68/c2feb4667adbf882ffa4b3e0ac9967f848980d9f8b5bebd86644aa67ce6a/matplotlib-3.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:ef5f2d1b67d2d2145ff75e10f8c008bfbf71d45137c4b648c87193e7dd053eac", size = 7822962, upload-time = "2024-12-13T05:56:16.358Z" }, + { url = "https://files.pythonhosted.org/packages/0c/22/2ef6a364cd3f565442b0b055e0599744f1e4314ec7326cdaaa48a4d864d7/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:44e0ed786d769d85bc787b0606a53f2d8d2d1d3c8a2608237365e9121c1a338c", size = 7877995, upload-time = "2024-12-13T05:56:18.805Z" }, + { url = "https://files.pythonhosted.org/packages/87/b8/2737456e566e9f4d94ae76b8aa0d953d9acb847714f9a7ad80184474f5be/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:09debb9ce941eb23ecdbe7eab972b1c3e0276dcf01688073faff7b0f61d6c6ca", size = 7769300, upload-time = "2024-12-13T05:56:21.315Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1f/e709c6ec7b5321e6568769baa288c7178e60a93a9da9e682b39450da0e29/matplotlib-3.9.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc53cf157a657bfd03afab14774d54ba73aa84d42cfe2480c91bd94873952db", size = 8313423, upload-time = "2024-12-13T05:56:26.719Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b6/5a1f868782cd13f053a679984e222007ecff654a9bfbac6b27a65f4eeb05/matplotlib-3.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ad45da51be7ad02387801fd154ef74d942f49fe3fcd26a64c94842ba7ec0d865", size = 7854624, upload-time = "2024-12-13T05:56:29.359Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "cycler", marker = "python_full_version >= '3.10'" }, + { name = "fonttools", version = "4.62.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "kiwisolver", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pillow", version = "12.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyparsing", marker = "python_full_version >= '3.10'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/be/a30bd917018ad220c400169fba298f2bb7003c8ccbc0c3e24ae2aacad1e8/matplotlib-3.10.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7", size = 8239828, upload-time = "2025-12-10T22:55:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/58/27/ca01e043c4841078e82cf6e80a6993dfecd315c3d79f5f3153afbb8e1ec6/matplotlib-3.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656", size = 8128050, upload-time = "2025-12-10T22:55:04.997Z" }, + { url = "https://files.pythonhosted.org/packages/cb/aa/7ab67f2b729ae6a91bcf9dcac0affb95fb8c56f7fd2b2af894ae0b0cf6fa/matplotlib-3.10.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df", size = 8700452, upload-time = "2025-12-10T22:55:07.47Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/2d5817b0acee3c49b7e7ccfbf5b273f284957cc8e270adf36375db353190/matplotlib-3.10.8-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17", size = 9534928, upload-time = "2025-12-10T22:55:10.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5b/8e66653e9f7c39cb2e5cab25fce4810daffa2bff02cbf5f3077cea9e942c/matplotlib-3.10.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933", size = 9586377, upload-time = "2025-12-10T22:55:12.362Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/fd0bbadf837f81edb0d208ba8f8cb552874c3b16e27cb91a31977d90875d/matplotlib-3.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a", size = 8128127, upload-time = "2025-12-10T22:55:14.436Z" }, + { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, + { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, + { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, + { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, + { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, + { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, + { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/f5/43/31d59500bb950b0d188e149a2e552040528c13d6e3d6e84d0cccac593dcd/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8", size = 8237252, upload-time = "2025-12-10T22:56:39.529Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2c/615c09984f3c5f907f51c886538ad785cf72e0e11a3225de2c0f9442aecc/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7", size = 8124693, upload-time = "2025-12-10T22:56:41.758Z" }, + { url = "https://files.pythonhosted.org/packages/91/e1/2757277a1c56041e1fc104b51a0f7b9a4afc8eb737865d63cababe30bc61/matplotlib-3.10.8-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3", size = 8702205, upload-time = "2025-12-10T22:56:43.415Z" }, + { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, + { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, +] + +[[package]] +name = "numpy" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015, upload-time = "2024-08-26T20:19:40.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece", size = 21165245, upload-time = "2024-08-26T20:04:14.625Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04", size = 13738540, upload-time = "2024-08-26T20:04:36.784Z" }, + { url = "https://files.pythonhosted.org/packages/ec/31/cc46e13bf07644efc7a4bf68df2df5fb2a1a88d0cd0da9ddc84dc0033e51/numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66", size = 5300623, upload-time = "2024-08-26T20:04:46.491Z" }, + { url = "https://files.pythonhosted.org/packages/6e/16/7bfcebf27bb4f9d7ec67332ffebee4d1bf085c84246552d52dbb548600e7/numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b", size = 6901774, upload-time = "2024-08-26T20:04:58.173Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a3/561c531c0e8bf082c5bef509d00d56f82e0ea7e1e3e3a7fc8fa78742a6e5/numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd", size = 13907081, upload-time = "2024-08-26T20:05:19.098Z" }, + { url = "https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318", size = 19523451, upload-time = "2024-08-26T20:05:47.479Z" }, + { url = "https://files.pythonhosted.org/packages/25/7f/0b209498009ad6453e4efc2c65bcdf0ae08a182b2b7877d7ab38a92dc542/numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8", size = 19927572, upload-time = "2024-08-26T20:06:17.137Z" }, + { url = "https://files.pythonhosted.org/packages/3e/df/2619393b1e1b565cd2d4c4403bdd979621e2c4dea1f8532754b2598ed63b/numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326", size = 14400722, upload-time = "2024-08-26T20:06:39.16Z" }, + { url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170, upload-time = "2024-08-26T20:06:50.361Z" }, + { url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558, upload-time = "2024-08-26T20:07:13.881Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137, upload-time = "2024-08-26T20:07:45.345Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552, upload-time = "2024-08-26T20:08:06.666Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957, upload-time = "2024-08-26T20:08:15.83Z" }, + { url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573, upload-time = "2024-08-26T20:08:27.185Z" }, + { url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330, upload-time = "2024-08-26T20:08:48.058Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895, upload-time = "2024-08-26T20:09:16.536Z" }, + { url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253, upload-time = "2024-08-26T20:09:46.263Z" }, + { url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074, upload-time = "2024-08-26T20:10:08.483Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640, upload-time = "2024-08-26T20:10:19.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230, upload-time = "2024-08-26T20:10:43.413Z" }, + { url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803, upload-time = "2024-08-26T20:11:13.916Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835, upload-time = "2024-08-26T20:11:34.779Z" }, + { url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499, upload-time = "2024-08-26T20:11:43.902Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497, upload-time = "2024-08-26T20:11:55.09Z" }, + { url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158, upload-time = "2024-08-26T20:12:14.95Z" }, + { url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173, upload-time = "2024-08-26T20:12:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174, upload-time = "2024-08-26T20:13:13.634Z" }, + { url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701, upload-time = "2024-08-26T20:13:34.851Z" }, + { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313, upload-time = "2024-08-26T20:13:45.653Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179, upload-time = "2024-08-26T20:14:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/43/c1/41c8f6df3162b0c6ffd4437d729115704bd43363de0090c7f913cfbc2d89/numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c", size = 21169942, upload-time = "2024-08-26T20:14:40.108Z" }, + { url = "https://files.pythonhosted.org/packages/39/bc/fd298f308dcd232b56a4031fd6ddf11c43f9917fbc937e53762f7b5a3bb1/numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd", size = 13711512, upload-time = "2024-08-26T20:15:00.985Z" }, + { url = "https://files.pythonhosted.org/packages/96/ff/06d1aa3eeb1c614eda245c1ba4fb88c483bee6520d361641331872ac4b82/numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b", size = 5306976, upload-time = "2024-08-26T20:15:10.876Z" }, + { url = "https://files.pythonhosted.org/packages/2d/98/121996dcfb10a6087a05e54453e28e58694a7db62c5a5a29cee14c6e047b/numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729", size = 6906494, upload-time = "2024-08-26T20:15:22.055Z" }, + { url = "https://files.pythonhosted.org/packages/15/31/9dffc70da6b9bbf7968f6551967fc21156207366272c2a40b4ed6008dc9b/numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1", size = 13912596, upload-time = "2024-08-26T20:15:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/b9/14/78635daab4b07c0930c919d451b8bf8c164774e6a3413aed04a6d95758ce/numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd", size = 19526099, upload-time = "2024-08-26T20:16:11.048Z" }, + { url = "https://files.pythonhosted.org/packages/26/4c/0eeca4614003077f68bfe7aac8b7496f04221865b3a5e7cb230c9d055afd/numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d", size = 19932823, upload-time = "2024-08-26T20:16:40.171Z" }, + { url = "https://files.pythonhosted.org/packages/f1/46/ea25b98b13dccaebddf1a803f8c748680d972e00507cd9bc6dcdb5aa2ac1/numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d", size = 14404424, upload-time = "2024-08-26T20:17:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809, upload-time = "2024-08-26T20:17:13.553Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314, upload-time = "2024-08-26T20:17:36.72Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3b/df5a870ac6a3be3a86856ce195ef42eec7ae50d2a202be1f5a4b3b340e14/numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8", size = 21025288, upload-time = "2024-08-26T20:18:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/2c/97/51af92f18d6f6f2d9ad8b482a99fb74e142d71372da5d834b3a2747a446e/numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4", size = 6762793, upload-time = "2024-08-26T20:18:19.125Z" }, + { url = "https://files.pythonhosted.org/packages/12/46/de1fbd0c1b5ccaa7f9a005b66761533e2f6a3e560096682683a223631fe9/numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c", size = 19334885, upload-time = "2024-08-26T20:18:47.237Z" }, + { url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784, upload-time = "2024-08-26T20:19:11.19Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/51/5093a2df15c4dc19da3f79d1021e891f5dcf1d9d1db6ba38891d5590f3fe/numpy-2.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33b3bf58ee84b172c067f56aeadc7ee9ab6de69c5e800ab5b10295d54c581adb", size = 16957183, upload-time = "2026-03-09T07:55:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/b5/7c/c061f3de0630941073d2598dc271ac2f6cbcf5c83c74a5870fea07488333/numpy-2.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ba7b51e71c05aa1f9bc3641463cd82308eab40ce0d5c7e1fd4038cbf9938147", size = 14968734, upload-time = "2026-03-09T07:56:00.494Z" }, + { url = "https://files.pythonhosted.org/packages/ef/27/d26c85cbcd86b26e4f125b0668e7a7c0542d19dd7d23ee12e87b550e95b5/numpy-2.4.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1988292870c7cb9d0ebb4cc96b4d447513a9644801de54606dc7aabf2b7d920", size = 5475288, upload-time = "2026-03-09T07:56:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/2b/09/3c4abbc1dcd8010bf1a611d174c7aa689fc505585ec806111b4406f6f1b1/numpy-2.4.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:23b46bb6d8ecb68b58c09944483c135ae5f0e9b8d8858ece5e4ead783771d2a9", size = 6805253, upload-time = "2026-03-09T07:56:04.53Z" }, + { url = "https://files.pythonhosted.org/packages/21/bc/e7aa3f6817e40c3f517d407742337cbb8e6fc4b83ce0b55ab780c829243b/numpy-2.4.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a016db5c5dba78fa8fe9f5d80d6708f9c42ab087a739803c0ac83a43d686a470", size = 15969479, upload-time = "2026-03-09T07:56:06.638Z" }, + { url = "https://files.pythonhosted.org/packages/78/51/9f5d7a41f0b51649ddf2f2320595e15e122a40610b233d51928dd6c92353/numpy-2.4.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:715de7f82e192e8cae5a507a347d97ad17598f8e026152ca97233e3666daaa71", size = 16901035, upload-time = "2026-03-09T07:56:09.405Z" }, + { url = "https://files.pythonhosted.org/packages/64/6e/b221dd847d7181bc5ee4857bfb026182ef69499f9305eb1371cbb1aea626/numpy-2.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ddb7919366ee468342b91dea2352824c25b55814a987847b6c52003a7c97f15", size = 17325657, upload-time = "2026-03-09T07:56:12.067Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b8/8f3fd2da596e1063964b758b5e3c970aed1949a05200d7e3d46a9d46d643/numpy-2.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a315e5234d88067f2d97e1f2ef670a7569df445d55400f1e33d117418d008d52", size = 18635512, upload-time = "2026-03-09T07:56:14.629Z" }, + { url = "https://files.pythonhosted.org/packages/5c/24/2993b775c37e39d2f8ab4125b44337ab0b2ba106c100980b7c274a22bee7/numpy-2.4.3-cp311-cp311-win32.whl", hash = "sha256:2b3f8d2c4589b1a2028d2a770b0fc4d1f332fb5e01521f4de3199a896d158ddd", size = 6238100, upload-time = "2026-03-09T07:56:17.243Z" }, + { url = "https://files.pythonhosted.org/packages/76/1d/edccf27adedb754db7c4511d5eac8b83f004ae948fe2d3509e8b78097d4c/numpy-2.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:77e76d932c49a75617c6d13464e41203cd410956614d0a0e999b25e9e8d27eec", size = 12609816, upload-time = "2026-03-09T07:56:19.089Z" }, + { url = "https://files.pythonhosted.org/packages/92/82/190b99153480076c8dce85f4cfe7d53ea84444145ffa54cb58dcd460d66b/numpy-2.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:eb610595dd91560905c132c709412b512135a60f1851ccbd2c959e136431ff67", size = 10485757, upload-time = "2026-03-09T07:56:21.753Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ed/6388632536f9788cea23a3a1b629f25b43eaacd7d7377e5d6bc7b9deb69b/numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef", size = 16669628, upload-time = "2026-03-09T07:56:24.252Z" }, + { url = "https://files.pythonhosted.org/packages/74/1b/ee2abfc68e1ce728b2958b6ba831d65c62e1b13ce3017c13943f8f9b5b2e/numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e", size = 14696872, upload-time = "2026-03-09T07:56:26.991Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d1/780400e915ff5638166f11ca9dc2c5815189f3d7cf6f8759a1685e586413/numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4", size = 5203489, upload-time = "2026-03-09T07:56:29.414Z" }, + { url = "https://files.pythonhosted.org/packages/0b/bb/baffa907e9da4cc34a6e556d6d90e032f6d7a75ea47968ea92b4858826c4/numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18", size = 6550814, upload-time = "2026-03-09T07:56:32.225Z" }, + { url = "https://files.pythonhosted.org/packages/7b/12/8c9f0c6c95f76aeb20fc4a699c33e9f827fa0d0f857747c73bb7b17af945/numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5", size = 15666601, upload-time = "2026-03-09T07:56:34.461Z" }, + { url = "https://files.pythonhosted.org/packages/bd/79/cc665495e4d57d0aa6fbcc0aa57aa82671dfc78fbf95fe733ed86d98f52a/numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97", size = 16621358, upload-time = "2026-03-09T07:56:36.852Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/b4ecb7224af1065c3539f5ecfff879d090de09608ad1008f02c05c770cb3/numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c", size = 17016135, upload-time = "2026-03-09T07:56:39.337Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b1/6a88e888052eed951afed7a142dcdf3b149a030ca59b4c71eef085858e43/numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc", size = 18345816, upload-time = "2026-03-09T07:56:42.31Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132, upload-time = "2026-03-09T07:56:44.851Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144, upload-time = "2026-03-09T07:56:47.057Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364, upload-time = "2026-03-09T07:56:49.849Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" }, + { url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" }, + { url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" }, + { url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" }, + { url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" }, + { url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" }, + { url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" }, + { url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" }, + { url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" }, + { url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" }, + { url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" }, + { url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" }, + { url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" }, + { url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" }, + { url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" }, + { url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" }, + { url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" }, + { url = "https://files.pythonhosted.org/packages/64/e4/4dab9fb43c83719c29241c535d9e07be73bea4bc0c6686c5816d8e1b6689/numpy-2.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028", size = 16834892, upload-time = "2026-03-09T07:58:35.334Z" }, + { url = "https://files.pythonhosted.org/packages/c9/29/f8b6d4af90fed3dfda84ebc0df06c9833d38880c79ce954e5b661758aa31/numpy-2.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8", size = 14893070, upload-time = "2026-03-09T07:58:37.7Z" }, + { url = "https://files.pythonhosted.org/packages/9a/04/a19b3c91dbec0a49269407f15d5753673a09832daed40c45e8150e6fa558/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152", size = 5399609, upload-time = "2026-03-09T07:58:39.853Z" }, + { url = "https://files.pythonhosted.org/packages/79/34/4d73603f5420eab89ea8a67097b31364bf7c30f811d4dd84b1659c7476d9/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:bc71942c789ef415a37f0d4eab90341425a00d538cd0642445d30b41023d3395", size = 6714355, upload-time = "2026-03-09T07:58:42.365Z" }, + { url = "https://files.pythonhosted.org/packages/58/ad/1100d7229bb248394939a12a8074d485b655e8ed44207d328fdd7fcebc7b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e58765ad74dcebd3ef0208a5078fba32dc8ec3578fe84a604432950cd043d79", size = 15800434, upload-time = "2026-03-09T07:58:44.837Z" }, + { url = "https://files.pythonhosted.org/packages/0c/fd/16d710c085d28ba4feaf29ac60c936c9d662e390344f94a6beaa2ac9899b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e236dbda4e1d319d681afcbb136c0c4a8e0f1a5c58ceec2adebb547357fe857", size = 16729409, upload-time = "2026-03-09T07:58:47.972Z" }, + { url = "https://files.pythonhosted.org/packages/57/a7/b35835e278c18b85206834b3aa3abe68e77a98769c59233d1f6300284781/numpy-2.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5", size = 12504685, upload-time = "2026-03-09T07:58:50.525Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pillow" +version = "11.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554, upload-time = "2025-07-01T09:13:39.342Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548, upload-time = "2025-07-01T09:13:41.835Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742, upload-time = "2025-07-03T13:09:47.439Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087, upload-time = "2025-07-03T13:09:51.796Z" }, + { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350, upload-time = "2025-07-01T09:13:43.865Z" }, + { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840, upload-time = "2025-07-01T09:13:46.161Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005, upload-time = "2025-07-01T09:13:47.829Z" }, + { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372, upload-time = "2025-07-01T09:13:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090, upload-time = "2025-07-01T09:13:53.915Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988, upload-time = "2025-07-01T09:13:55.699Z" }, + { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899, upload-time = "2025-07-01T09:13:57.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, + { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, + { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, + { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, + { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, + { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, + { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, + { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, + { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, + { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, + { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, + { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, + { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, + { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, + { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" }, + { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" }, + { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" }, + { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" }, + { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, + { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, + { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8e/9c089f01677d1264ab8648352dcb7773f37da6ad002542760c80107da816/pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f", size = 5316478, upload-time = "2025-07-01T09:15:52.209Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a9/5749930caf674695867eb56a581e78eb5f524b7583ff10b01b6e5048acb3/pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081", size = 4686522, upload-time = "2025-07-01T09:15:54.162Z" }, + { url = "https://files.pythonhosted.org/packages/43/46/0b85b763eb292b691030795f9f6bb6fcaf8948c39413c81696a01c3577f7/pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4", size = 5853376, upload-time = "2025-07-03T13:11:01.066Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/1a230ec0067243cbd60bc2dad5dc3ab46a8a41e21c15f5c9b52b26873069/pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc", size = 7626020, upload-time = "2025-07-03T13:11:06.479Z" }, + { url = "https://files.pythonhosted.org/packages/63/dd/f296c27ffba447bfad76c6a0c44c1ea97a90cb9472b9304c94a732e8dbfb/pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06", size = 5956732, upload-time = "2025-07-01T09:15:56.111Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a0/98a3630f0b57f77bae67716562513d3032ae70414fcaf02750279c389a9e/pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a", size = 6624404, upload-time = "2025-07-01T09:15:58.245Z" }, + { url = "https://files.pythonhosted.org/packages/de/e6/83dfba5646a290edd9a21964da07674409e410579c341fc5b8f7abd81620/pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978", size = 6067760, upload-time = "2025-07-01T09:16:00.003Z" }, + { url = "https://files.pythonhosted.org/packages/bc/41/15ab268fe6ee9a2bc7391e2bbb20a98d3974304ab1a406a992dcb297a370/pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d", size = 6700534, upload-time = "2025-07-01T09:16:02.29Z" }, + { url = "https://files.pythonhosted.org/packages/64/79/6d4f638b288300bed727ff29f2a3cb63db054b33518a95f27724915e3fbc/pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71", size = 6277091, upload-time = "2025-07-01T09:16:04.4Z" }, + { url = "https://files.pythonhosted.org/packages/46/05/4106422f45a05716fd34ed21763f8ec182e8ea00af6e9cb05b93a247361a/pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada", size = 6986091, upload-time = "2025-07-01T09:16:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/287fd55c2c12761d0591549d48885187579b7c257bef0c6660755b0b59ae/pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb", size = 2422632, upload-time = "2025-07-01T09:16:08.142Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556, upload-time = "2025-07-01T09:16:09.961Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625, upload-time = "2025-07-01T09:16:11.913Z" }, + { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207, upload-time = "2025-07-03T13:11:10.201Z" }, + { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939, upload-time = "2025-07-03T13:11:15.68Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166, upload-time = "2025-07-01T09:16:13.74Z" }, + { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482, upload-time = "2025-07-01T09:16:16.107Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596, upload-time = "2025-07-01T09:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, + { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, + { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, +] + +[[package]] +name = "pillow" +version = "12.1.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/30/5bd3d794762481f8c8ae9c80e7b76ecea73b916959eb587521358ef0b2f9/pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0", size = 5304099, upload-time = "2026-02-11T04:20:06.13Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c1/aab9e8f3eeb4490180e357955e15c2ef74b31f64790ff356c06fb6cf6d84/pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713", size = 4657880, upload-time = "2026-02-11T04:20:09.291Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0a/9879e30d56815ad529d3985aeff5af4964202425c27261a6ada10f7cbf53/pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b", size = 6222587, upload-time = "2026-02-11T04:20:10.82Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5f/a1b72ff7139e4f89014e8d451442c74a774d5c43cd938fb0a9f878576b37/pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b", size = 8027678, upload-time = "2026-02-11T04:20:12.455Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c2/c7cb187dac79a3d22c3ebeae727abee01e077c8c7d930791dc592f335153/pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4", size = 6335777, upload-time = "2026-02-11T04:20:14.441Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7b/f9b09a7804ec7336effb96c26d37c29d27225783dc1501b7d62dcef6ae25/pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4", size = 7027140, upload-time = "2026-02-11T04:20:16.387Z" }, + { url = "https://files.pythonhosted.org/packages/98/b2/2fa3c391550bd421b10849d1a2144c44abcd966daadd2f7c12e19ea988c4/pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e", size = 6449855, upload-time = "2026-02-11T04:20:18.554Z" }, + { url = "https://files.pythonhosted.org/packages/96/ff/9caf4b5b950c669263c39e96c78c0d74a342c71c4f43fd031bb5cb7ceac9/pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff", size = 7151329, upload-time = "2026-02-11T04:20:20.646Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f8/4b24841f582704da675ca535935bccb32b00a6da1226820845fac4a71136/pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40", size = 6325574, upload-time = "2026-02-11T04:20:22.43Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/9f6b01c0881d7036063aa6612ef04c0e2cad96be21325a1e92d0203f8e91/pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23", size = 7032347, upload-time = "2026-02-11T04:20:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/79/13/c7922edded3dcdaf10c59297540b72785620abc0538872c819915746757d/pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9", size = 2453457, upload-time = "2026-02-11T04:20:25.392Z" }, + { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" }, + { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" }, + { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" }, + { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" }, + { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" }, + { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, + { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, + { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, + { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, + { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, + { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, + { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, + { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, + { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, + { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, + { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, + { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" }, + { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" }, + { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" }, + { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, + { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "traffic-grapher" +version = "1.0.0" +source = { virtual = "." } +dependencies = [ + { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "matplotlib", version = "3.10.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyyaml" }, +] + +[package.metadata] +requires-dist = [ + { name = "matplotlib", specifier = ">=3.7" }, + { name = "pyyaml", specifier = ">=6.0" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]