From 276241447c3699f56704d286d6e343adcf019867 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Feb 2026 21:46:47 -0500 Subject: [PATCH 01/86] fix: add deprecation warning for setup() --- lua/cp/init.lua | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lua/cp/init.lua b/lua/cp/init.lua index fac3044..0bb5582 100644 --- a/lua/cp/init.lua +++ b/lua/cp/init.lua @@ -34,4 +34,13 @@ function M.is_initialized() return initialized end +---@deprecated Use `vim.g.cp_config` instead +function M.setup(user_config) + vim.deprecate('require("cp").setup()', 'vim.g.cp_config', 'v0.1.0', 'cp.nvim', false) + + if user_config then + vim.g.cp_config = vim.tbl_deep_extend('force', vim.g.cp_config or {}, user_config) + end +end + return M From a54a06f9396213725a06aab58e023ec6d8e20f37 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Fri, 6 Feb 2026 15:12:47 -0500 Subject: [PATCH 02/86] refactor: rename `vim.g.cp_config` to `vim.g.cp` --- doc/cp.nvim.txt | 8 ++++---- lua/cp/init.lua | 12 ++++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/doc/cp.nvim.txt b/doc/cp.nvim.txt index d6d1d73..17fd18c 100644 --- a/doc/cp.nvim.txt +++ b/doc/cp.nvim.txt @@ -205,9 +205,9 @@ Debug Builds ~ ============================================================================== CONFIGURATION *cp-config* -Configuration is done via `vim.g.cp_config`. Set this before using the plugin: +Configuration is done via `vim.g.cp`. Set this before using the plugin: >lua - vim.g.cp_config = { + vim.g.cp = { languages = { cpp = { extension = 'cc', @@ -274,7 +274,7 @@ the default; per-platform overrides can tweak 'extension' or 'commands'. For example, to run CodeForces contests with Python by default: >lua - vim.g.cp_config = { + vim.g.cp = { platforms = { codeforces = { default_language = 'python', @@ -285,7 +285,7 @@ For example, to run CodeForces contests with Python by default: Any language is supported provided the proper configuration. For example, to run CSES problems with Rust using the single schema: >lua - vim.g.cp_config = { + vim.g.cp = { languages = { rust = { extension = 'rs', diff --git a/lua/cp/init.lua b/lua/cp/init.lua index 0bb5582..1837901 100644 --- a/lua/cp/init.lua +++ b/lua/cp/init.lua @@ -17,7 +17,11 @@ local function ensure_initialized() if initialized then return end - local user_config = vim.g.cp_config or {} + if vim.g.cp_config then + vim.deprecate('vim.g.cp_config', 'vim.g.cp', 'v0.7.6', 'cp.nvim', false) + vim.g.cp = vim.g.cp or vim.g.cp_config + end + local user_config = vim.g.cp or {} local config = config_module.setup(user_config) config_module.set_current_config(config) initialized = true @@ -34,12 +38,12 @@ function M.is_initialized() return initialized end ----@deprecated Use `vim.g.cp_config` instead +---@deprecated Use `vim.g.cp` instead function M.setup(user_config) - vim.deprecate('require("cp").setup()', 'vim.g.cp_config', 'v0.1.0', 'cp.nvim', false) + vim.deprecate('require("cp").setup()', 'vim.g.cp', 'v0.1.0', 'cp.nvim', false) if user_config then - vim.g.cp_config = vim.tbl_deep_extend('force', vim.g.cp_config or {}, user_config) + vim.g.cp = vim.tbl_deep_extend('force', vim.g.cp or {}, user_config) end end From d23b4e59d15018472a3eb6c397f7e74cf2a6eaa9 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Fri, 6 Feb 2026 16:29:46 -0500 Subject: [PATCH 03/86] fix(setup): set language state before setup_code hook on first open Problem: when opening a contest for the first time (metadata not cached), the setup_code hook fired before state.set_language() was called, causing state.get_language() to return nil inside the hook. Solution: call state.set_language(lang) before the hook in the provisional-buffer branch of setup_contest(). The value is already computed at that point and is identical to what setup_problem() sets later, so the early write is idempotent. --- lua/cp/setup.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/cp/setup.lua b/lua/cp/setup.lua index fce1a0c..e3bb38d 100644 --- a/lua/cp/setup.lua +++ b/lua/cp/setup.lua @@ -160,6 +160,8 @@ function M.setup_contest(platform, contest_id, problem_id, language) vim.bo[bufnr].buftype = '' vim.bo[bufnr].swapfile = false + state.set_language(lang) + if cfg.hooks and cfg.hooks.setup_code and not vim.b[bufnr].cp_setup_done then local ok = pcall(cfg.hooks.setup_code, state) if ok then From de2bc0753228be518d0cf0a6d5614c281d5b3144 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Fri, 6 Feb 2026 16:40:39 -0500 Subject: [PATCH 04/86] refactor: remove vim.g.cp_config compatibility shim Problem: the deprecated vim.g.cp_config fallback was kept for backwards compatibility after the rename to vim.g.cp in v0.7.6. Solution: drop the shim entirely and update the setup() deprecation target to v0.7.7. --- lua/cp/init.lua | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lua/cp/init.lua b/lua/cp/init.lua index 1837901..3cb36c1 100644 --- a/lua/cp/init.lua +++ b/lua/cp/init.lua @@ -17,10 +17,6 @@ local function ensure_initialized() if initialized then return end - if vim.g.cp_config then - vim.deprecate('vim.g.cp_config', 'vim.g.cp', 'v0.7.6', 'cp.nvim', false) - vim.g.cp = vim.g.cp or vim.g.cp_config - end local user_config = vim.g.cp or {} local config = config_module.setup(user_config) config_module.set_current_config(config) @@ -40,7 +36,7 @@ end ---@deprecated Use `vim.g.cp` instead function M.setup(user_config) - vim.deprecate('require("cp").setup()', 'vim.g.cp', 'v0.1.0', 'cp.nvim', false) + vim.deprecate('require("cp").setup()', 'vim.g.cp', 'v0.7.7', 'cp.nvim', false) if user_config then vim.g.cp = vim.tbl_deep_extend('force', vim.g.cp or {}, user_config) From 029ea125b97320ff5c2884bf84bf5aa4e7077c79 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sat, 7 Feb 2026 13:22:19 -0500 Subject: [PATCH 05/86] feat: add mappings for all primary actions Problem: users who want keybindings must call vim.cmd('CP run') or reach into internal Lua modules directly. There is no stable, discoverable, lazy-load-friendly public API for key binding. Solution: define 7 mappings in plugin/cp.lua that dispatch through the same handle_command() code path as :CP. Document them in a new MAPPINGS section in the vimdoc with helptags and an example config block. --- doc/cp.nvim.txt | 37 +++++++++++++++++++++++++++++++++++++ plugin/cp.lua | 14 ++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/doc/cp.nvim.txt b/doc/cp.nvim.txt index 17fd18c..9361121 100644 --- a/doc/cp.nvim.txt +++ b/doc/cp.nvim.txt @@ -202,6 +202,43 @@ Debug Builds ~ } < +============================================================================== +MAPPINGS *cp-mappings* + +cp.nvim provides mappings for all primary actions. These dispatch +through the same code path as |:CP|. + + *(cp-run)* +(cp-run) Run tests in I/O view. Equivalent to :CP run. + + *(cp-panel)* +(cp-panel) Open full-screen test panel. Equivalent to :CP panel. + + *(cp-edit)* +(cp-edit) Open the test case editor. Equivalent to :CP edit. + + *(cp-next)* +(cp-next) Navigate to the next problem. Equivalent to :CP next. + + *(cp-prev)* +(cp-prev) Navigate to the previous problem. Equivalent to :CP prev. + + *(cp-pick)* +(cp-pick) Launch the contest picker. Equivalent to :CP pick. + + *(cp-interact)* +(cp-interact) Open interactive mode. Equivalent to :CP interact. + +Example configuration: >lua + vim.keymap.set('n', 'cr', '(cp-run)') + vim.keymap.set('n', 'cp', '(cp-panel)') + vim.keymap.set('n', 'ce', '(cp-edit)') + vim.keymap.set('n', 'cn', '(cp-next)') + vim.keymap.set('n', 'cN', '(cp-prev)') + vim.keymap.set('n', 'cc', '(cp-pick)') + vim.keymap.set('n', 'ci', '(cp-interact)') +< + ============================================================================== CONFIGURATION *cp-config* diff --git a/plugin/cp.lua b/plugin/cp.lua index b91a3d5..2689e71 100644 --- a/plugin/cp.lua +++ b/plugin/cp.lua @@ -154,3 +154,17 @@ end, { return {} end, }) + +local function cp_action(action) + return function() + require('cp').handle_command({ fargs = { action } }) + end +end + +vim.keymap.set('n', '(cp-run)', cp_action('run'), { desc = 'CP run tests' }) +vim.keymap.set('n', '(cp-panel)', cp_action('panel'), { desc = 'CP open panel' }) +vim.keymap.set('n', '(cp-edit)', cp_action('edit'), { desc = 'CP edit test cases' }) +vim.keymap.set('n', '(cp-next)', cp_action('next'), { desc = 'CP next problem' }) +vim.keymap.set('n', '(cp-prev)', cp_action('prev'), { desc = 'CP previous problem' }) +vim.keymap.set('n', '(cp-pick)', cp_action('pick'), { desc = 'CP pick contest' }) +vim.keymap.set('n', '(cp-interact)', cp_action('interact'), { desc = 'CP interactive mode' }) From 49df7e015d9c37b3e8ded3842a85d18878a7150e Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 17 Feb 2026 20:47:52 -0500 Subject: [PATCH 06/86] docs: add setup section and reorder vimdoc Problem: the vimdoc had no setup section, and configuration was buried after commands and mappings. Solution: add a cp-setup section with lazy.nvim example and move both setup and configuration above commands for better discoverability. --- doc/cp.nvim.txt | 444 ++++++++++++++++++++++++------------------------ 1 file changed, 226 insertions(+), 218 deletions(-) diff --git a/doc/cp.nvim.txt b/doc/cp.nvim.txt index 9361121..e85ea45 100644 --- a/doc/cp.nvim.txt +++ b/doc/cp.nvim.txt @@ -19,225 +19,13 @@ REQUIREMENTS *cp-requirements* - uv package manager (https://docs.astral.sh/uv/) ============================================================================== -COMMANDS *cp-commands* +SETUP *cp-setup* -:CP *:CP* - cp.nvim uses a single :CP command with intelligent argument parsing: - - Setup Commands ~ - :CP {platform} {contest_id} [--lang {language}] - Full setup: set platform and load contest metadata. - Scrapes test cases and creates source file. - --lang: Use specific language (default: platform default) - Examples: > - :CP codeforces 1933 - :CP codeforces 1933 --lang python -< - View Commands ~ - :CP run [all|n|n,m,...] [--debug] - Run tests in I/O view (see |cp-io-view|). - Lightweight split showing test verdicts. - - Execution modes: - • :CP run Combined: single execution with all tests - (auto-switches to individual when multiple samples) - • :CP run all Individual: N separate executions - • :CP run n Individual: run test n only - • :CP run n,m,... Individual: run specific tests (e.g. nth and mth) - - --debug: Use debug build (builds to build/.dbg) - - Combined mode runs all test inputs in one execution (matching - platform behavior for multi-test problems). When a problem has - multiple independent sample test cases, :CP run auto-switches to - individual mode to run each sample separately. - - Examples: > - :CP run " Combined: all tests, one execution - :CP run all " Individual: all tests, N executions - :CP run 2 " Individual: test 2 only - :CP run 1,3,5 " Individual: tests 1, 3, and 5 - :CP run all --debug " Individual with debug build -< - :CP panel [--debug] [n] - Open full-screen test panel (see |cp-panel|). - Aggregate table with diff modes for detailed analysis. - Optional [n] focuses on specific test. - --debug: Use debug build (with sanitizers, etc.) - Examples: > - :CP panel " All tests - :CP panel --debug 3 " Test 3, debug build -< - - :CP pick [--lang {language}] - Launch configured picker for interactive - platform/contest selection. - --lang: Pre-select language for chosen contest. - Example: > - :CP pick - :CP pick --lang python -< - - :CP interact [script] - Open an interactive terminal for the current problem. - If an executable interactor is provided, runs the compiled - binary against the source file (see - *cp-interact*). Otherwise, runs the source - file. Only valid for interactive problems. - - Navigation Commands ~ - :CP next [--lang {language}] - Navigate to next problem in current contest. - Stops at last problem (no wrapping). - --lang: Use specific language for next problem. - By default, preserves current file's language if - enabled for the new problem, otherwise uses platform - default. - Examples: > - :CP next - :CP next --lang python -< - :CP prev [--lang {language}] - Navigate to previous problem in current contest. - Stops at first problem (no wrapping). - --lang: Use specific language for previous problem. - By default, preserves current file's language if - enabled for the new problem, otherwise uses platform - default. - Examples: > - :CP prev - :CP prev --lang cpp -< - :CP {problem_id} [--lang {language}] - Jump to problem {problem_id} in a contest. - Requires that a contest has already been set up. - --lang: Use specific language for this problem. - Examples: > - :CP B - :CP C --lang python -< - - Edit Commands ~ - :CP edit [n] - Open grid test editor showing all test cases. - Tests displayed as 2×N grid (2 rows, N columns): - • Top row: Test inputs (editable) - • Bottom row: Expected outputs (editable) - - Optional [n]: Jump cursor to test n's input buffer - - Changes saved to both cache and disk on exit, - taking effect immediately in :CP run and CLI. - - Keybindings (configurable via |EditConfig|): - q Save all and exit editor - ]t Jump to next test column - [t Jump to previous test column - gd Delete current test column - ga Add new test column at end - Normal window navigation - - Examples: > - :CP edit " Edit all tests - :CP edit 3 " Edit all, start at test 3 -< - - State Restoration ~ - :CP Restore state from current file. - Automatically detects platform, contest, problem, - and language from cached state. Use this after - switching files to restore your CP environment. - - Cache Commands ~ - :CP cache clear [platform] [contest] - Clear cache data at different granularities: - • No args: Clear all cached data - • [platform]: Clear all data for a platform - • [platform] [contest]: Clear specific contest - Examples: > - :CP cache clear - :CP cache clear codeforces - :CP cache clear codeforces 1848 -< - :CP cache read - View the cache in a pretty-printed lua buffer. - Exit with q. - -Template Variables ~ - *cp-template-vars* - Command templates support variable substitution using {variable} syntax: - - • {source} Source file path (e.g. "abc324a.cpp") - • {binary} Output binary path (e.g. "build/abc324a.run" or - "build/abc324a.dbg" for debug builds) - - Example template: > - build = { 'g++', '{source}', '-o', '{binary}', '-std=c++17' } -< Would expand to: > - g++ abc324a.cpp -o build/abc324a.run -std=c++17 -< -Debug Builds ~ - *cp-debug-builds* - The --debug flag uses the debug command configuration instead of build: - - • Normal build: commands.build → outputs to build/.run - • Debug build: commands.debug → outputs to build/.dbg - - Debug builds typically include sanitizers (address, undefined behavior) to - catch memory errors, buffer overflows, and other issues. Both binaries - coexist, so you can switch between normal and debug mode without - recompiling. - - Example debug configuration: > - languages = { - cpp = { - extension = 'cc', - commands = { - build = { 'g++', '-std=c++17', '{source}', '-o', '{binary}' }, - run = { '{binary}' }, - debug = { 'g++', '-std=c++17', '-fsanitize=address,undefined', - '{source}', '-o', '{binary}' }, - } - } - } -< - -============================================================================== -MAPPINGS *cp-mappings* - -cp.nvim provides mappings for all primary actions. These dispatch -through the same code path as |:CP|. - - *(cp-run)* -(cp-run) Run tests in I/O view. Equivalent to :CP run. - - *(cp-panel)* -(cp-panel) Open full-screen test panel. Equivalent to :CP panel. - - *(cp-edit)* -(cp-edit) Open the test case editor. Equivalent to :CP edit. - - *(cp-next)* -(cp-next) Navigate to the next problem. Equivalent to :CP next. - - *(cp-prev)* -(cp-prev) Navigate to the previous problem. Equivalent to :CP prev. - - *(cp-pick)* -(cp-pick) Launch the contest picker. Equivalent to :CP pick. - - *(cp-interact)* -(cp-interact) Open interactive mode. Equivalent to :CP interact. - -Example configuration: >lua - vim.keymap.set('n', 'cr', '(cp-run)') - vim.keymap.set('n', 'cp', '(cp-panel)') - vim.keymap.set('n', 'ce', '(cp-edit)') - vim.keymap.set('n', 'cn', '(cp-next)') - vim.keymap.set('n', 'cN', '(cp-prev)') - vim.keymap.set('n', 'cc', '(cp-pick)') - vim.keymap.set('n', 'ci', '(cp-interact)') +Load cp.nvim with your package manager. For example, with lazy.nvim: >lua + { 'barrettruth/cp.nvim' } < +The plugin works automatically with no configuration required. For +customization, see |cp-config|. ============================================================================== CONFIGURATION *cp-config* @@ -462,12 +250,232 @@ run CSES problems with Rust using the single schema: print("Source file: " .. state.get_source_file()) end, setup_io_input = function(bufnr, state) - -- Custom setup for input buffer vim.api.nvim_set_option_value('number', false, { buf = bufnr }) end } < +============================================================================== +COMMANDS *cp-commands* + +:CP *:CP* + cp.nvim uses a single :CP command with intelligent argument parsing: + + Setup Commands ~ + :CP {platform} {contest_id} [--lang {language}] + Full setup: set platform and load contest metadata. + Scrapes test cases and creates source file. + --lang: Use specific language (default: platform default) + Examples: > + :CP codeforces 1933 + :CP codeforces 1933 --lang python +< + View Commands ~ + :CP run [all|n|n,m,...] [--debug] + Run tests in I/O view (see |cp-io-view|). + Lightweight split showing test verdicts. + + Execution modes: + • :CP run Combined: single execution with all tests + (auto-switches to individual when multiple samples) + • :CP run all Individual: N separate executions + • :CP run n Individual: run test n only + • :CP run n,m,... Individual: run specific tests (e.g. nth and mth) + + --debug: Use debug build (builds to build/.dbg) + + Combined mode runs all test inputs in one execution (matching + platform behavior for multi-test problems). When a problem has + multiple independent sample test cases, :CP run auto-switches to + individual mode to run each sample separately. + + Examples: > + :CP run " Combined: all tests, one execution + :CP run all " Individual: all tests, N executions + :CP run 2 " Individual: test 2 only + :CP run 1,3,5 " Individual: tests 1, 3, and 5 + :CP run all --debug " Individual with debug build +< + :CP panel [--debug] [n] + Open full-screen test panel (see |cp-panel|). + Aggregate table with diff modes for detailed analysis. + Optional [n] focuses on specific test. + --debug: Use debug build (with sanitizers, etc.) + Examples: > + :CP panel " All tests + :CP panel --debug 3 " Test 3, debug build +< + + :CP pick [--lang {language}] + Launch configured picker for interactive + platform/contest selection. + --lang: Pre-select language for chosen contest. + Example: > + :CP pick + :CP pick --lang python +< + + :CP interact [script] + Open an interactive terminal for the current problem. + If an executable interactor is provided, runs the compiled + binary against the source file (see + *cp-interact*). Otherwise, runs the source + file. Only valid for interactive problems. + + Navigation Commands ~ + :CP next [--lang {language}] + Navigate to next problem in current contest. + Stops at last problem (no wrapping). + --lang: Use specific language for next problem. + By default, preserves current file's language if + enabled for the new problem, otherwise uses platform + default. + Examples: > + :CP next + :CP next --lang python +< + :CP prev [--lang {language}] + Navigate to previous problem in current contest. + Stops at first problem (no wrapping). + --lang: Use specific language for previous problem. + By default, preserves current file's language if + enabled for the new problem, otherwise uses platform + default. + Examples: > + :CP prev + :CP prev --lang cpp +< + :CP {problem_id} [--lang {language}] + Jump to problem {problem_id} in a contest. + Requires that a contest has already been set up. + --lang: Use specific language for this problem. + Examples: > + :CP B + :CP C --lang python +< + + Edit Commands ~ + :CP edit [n] + Open grid test editor showing all test cases. + Tests displayed as 2×N grid (2 rows, N columns): + • Top row: Test inputs (editable) + • Bottom row: Expected outputs (editable) + + Optional [n]: Jump cursor to test n's input buffer + + Changes saved to both cache and disk on exit, + taking effect immediately in :CP run and CLI. + + Keybindings (configurable via |EditConfig|): + q Save all and exit editor + ]t Jump to next test column + [t Jump to previous test column + gd Delete current test column + ga Add new test column at end + Normal window navigation + + Examples: > + :CP edit " Edit all tests + :CP edit 3 " Edit all, start at test 3 +< + + State Restoration ~ + :CP Restore state from current file. + Automatically detects platform, contest, problem, + and language from cached state. Use this after + switching files to restore your CP environment. + + Cache Commands ~ + :CP cache clear [platform] [contest] + Clear cache data at different granularities: + • No args: Clear all cached data + • [platform]: Clear all data for a platform + • [platform] [contest]: Clear specific contest + Examples: > + :CP cache clear + :CP cache clear codeforces + :CP cache clear codeforces 1848 +< + :CP cache read + View the cache in a pretty-printed lua buffer. + Exit with q. + +Template Variables ~ + *cp-template-vars* + Command templates support variable substitution using {variable} syntax: + + • {source} Source file path (e.g. "abc324a.cpp") + • {binary} Output binary path (e.g. "build/abc324a.run" or + "build/abc324a.dbg" for debug builds) + + Example template: > + build = { 'g++', '{source}', '-o', '{binary}', '-std=c++17' } +< Would expand to: > + g++ abc324a.cpp -o build/abc324a.run -std=c++17 +< +Debug Builds ~ + *cp-debug-builds* + The --debug flag uses the debug command configuration instead of build: + + • Normal build: commands.build → outputs to build/.run + • Debug build: commands.debug → outputs to build/.dbg + + Debug builds typically include sanitizers (address, undefined behavior) to + catch memory errors, buffer overflows, and other issues. Both binaries + coexist, so you can switch between normal and debug mode without + recompiling. + + Example debug configuration: > + languages = { + cpp = { + extension = 'cc', + commands = { + build = { 'g++', '-std=c++17', '{source}', '-o', '{binary}' }, + run = { '{binary}' }, + debug = { 'g++', '-std=c++17', '-fsanitize=address,undefined', + '{source}', '-o', '{binary}' }, + } + } + } +< + +============================================================================== +MAPPINGS *cp-mappings* + +cp.nvim provides mappings for all primary actions. These dispatch +through the same code path as |:CP|. + + *(cp-run)* +(cp-run) Run tests in I/O view. Equivalent to :CP run. + + *(cp-panel)* +(cp-panel) Open full-screen test panel. Equivalent to :CP panel. + + *(cp-edit)* +(cp-edit) Open the test case editor. Equivalent to :CP edit. + + *(cp-next)* +(cp-next) Navigate to the next problem. Equivalent to :CP next. + + *(cp-prev)* +(cp-prev) Navigate to the previous problem. Equivalent to :CP prev. + + *(cp-pick)* +(cp-pick) Launch the contest picker. Equivalent to :CP pick. + + *(cp-interact)* +(cp-interact) Open interactive mode. Equivalent to :CP interact. + +Example configuration: >lua + vim.keymap.set('n', 'cr', '(cp-run)') + vim.keymap.set('n', 'cp', '(cp-panel)') + vim.keymap.set('n', 'ce', '(cp-edit)') + vim.keymap.set('n', 'cn', '(cp-next)') + vim.keymap.set('n', 'cN', '(cp-prev)') + vim.keymap.set('n', 'cc', '(cp-pick)') + vim.keymap.set('n', 'ci', '(cp-interact)') +< + ============================================================================== LANGUAGE SELECTION *cp-lang-selection* From 51504b0121ba7231d529fdde976d631d2c4079f7 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 17 Feb 2026 21:10:29 -0500 Subject: [PATCH 07/86] fix: flake config; --- .envrc | 3 --- .gitignore | 3 +++ flake.lock | 27 +++++++++++++++++++++++++++ flake.nix | 12 ++++++++++++ 4 files changed, 42 insertions(+), 3 deletions(-) delete mode 100644 .envrc create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.envrc b/.envrc deleted file mode 100644 index aa57334..0000000 --- a/.envrc +++ /dev/null @@ -1,3 +0,0 @@ -VIRTUAL_ENV="$PWD/.venv" -PATH_add "$VIRTUAL_ENV/bin" -export VIRTUAL_ENV diff --git a/.gitignore b/.gitignore index 45bc345..a489c55 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ __pycache__ .claude/ node_modules/ + +.envrc +.direnv/ diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..5dcdbd4 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1771008912, + "narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a82ccc39b39b621151d6732718e3e250109076fa", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..583a505 --- /dev/null +++ b/flake.nix @@ -0,0 +1,12 @@ +{ + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + outputs = { self, nixpkgs }: { + devShells.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.mkShell { + packages = with nixpkgs.legacyPackages.x86_64-linux; [ + uv + python312 + ]; + }; + }; +} From da433068ef666730c87530d4f4fd18c7659abd63 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 17 Feb 2026 21:10:56 -0500 Subject: [PATCH 08/86] remove straggler file --- new | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 new diff --git a/new b/new deleted file mode 100644 index e69de29..0000000 From 04d0c124cfbe9a63c7e49bb6b38f179a80a4e061 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 17 Feb 2026 21:11:11 -0500 Subject: [PATCH 09/86] fix: remove flake config --- flake.lock | 27 --------------------------- flake.nix | 12 ------------ 2 files changed, 39 deletions(-) delete mode 100644 flake.lock delete mode 100644 flake.nix diff --git a/flake.lock b/flake.lock deleted file mode 100644 index 5dcdbd4..0000000 --- a/flake.lock +++ /dev/null @@ -1,27 +0,0 @@ -{ - "nodes": { - "nixpkgs": { - "locked": { - "lastModified": 1771008912, - "narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "a82ccc39b39b621151d6732718e3e250109076fa", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "nixpkgs": "nixpkgs" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index 583a505..0000000 --- a/flake.nix +++ /dev/null @@ -1,12 +0,0 @@ -{ - inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - - outputs = { self, nixpkgs }: { - devShells.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.mkShell { - packages = with nixpkgs.legacyPackages.x86_64-linux; [ - uv - python312 - ]; - }; - }; -} From b36ffba63a8ee0ed47ab51385129a52f11cbefb0 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 17 Feb 2026 21:11:29 -0500 Subject: [PATCH 10/86] feat(nix): initial flake config; --- flake.lock | 27 +++++++++++++++++++++++++++ flake.nix | 12 ++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..5dcdbd4 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1771008912, + "narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a82ccc39b39b621151d6732718e3e250109076fa", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..583a505 --- /dev/null +++ b/flake.nix @@ -0,0 +1,12 @@ +{ + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + outputs = { self, nixpkgs }: { + devShells.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.mkShell { + packages = with nixpkgs.legacyPackages.x86_64-linux; [ + uv + python312 + ]; + }; + }; +} From 1162e7046be9bf0a256fb2afcbfdfd3c6700ab69 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Wed, 18 Feb 2026 13:33:49 -0500 Subject: [PATCH 11/86] try to fix the setup --- flake.lock | 18 +- flake.nix | 78 ++- lua/cp/health.lua | 35 +- lua/cp/scraper.lua | 7 +- lua/cp/ui/views.lua | 23 +- lua/cp/utils.lua | 129 +++- pyproject.toml | 2 - scrapers/codechef.py | 7 +- scrapers/codeforces.py | 14 +- tests/conftest.py | 33 +- uv.lock | 1269 ---------------------------------------- 11 files changed, 256 insertions(+), 1359 deletions(-) diff --git a/flake.lock b/flake.lock index 5dcdbd4..224b38d 100644 --- a/flake.lock +++ b/flake.lock @@ -18,7 +18,23 @@ }, "root": { "inputs": { - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "systems": "systems" + } + }, + "systems": { + "locked": { + "lastModified": 1689347949, + "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", + "owner": "nix-systems", + "repo": "default-linux", + "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default-linux", + "type": "github" } } }, diff --git a/flake.nix b/flake.nix index 583a505..705747f 100644 --- a/flake.nix +++ b/flake.nix @@ -1,12 +1,72 @@ { - inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - - outputs = { self, nixpkgs }: { - devShells.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.mkShell { - packages = with nixpkgs.legacyPackages.x86_64-linux; [ - uv - python312 - ]; - }; + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + systems.url = "github:nix-systems/default-linux"; }; + + outputs = + { + self, + nixpkgs, + systems, + }: + let + eachSystem = nixpkgs.lib.genAttrs (import systems); + pkgsFor = system: nixpkgs.legacyPackages.${system}; + + mkPythonEnv = + pkgs: + pkgs.python312.withPackages (ps: [ + ps.backoff + ps.beautifulsoup4 + ps.curl-cffi + ps.httpx + ps.ndjson + ps.pydantic + ps.requests + ]); + + mkPlugin = + pkgs: + let + pythonEnv = mkPythonEnv pkgs; + in + pkgs.vimUtils.buildVimPlugin { + pname = "cp-nvim"; + version = "0-unstable-${self.shortRev or self.dirtyShortRev or "dev"}"; + src = self; + postPatch = '' + substituteInPlace lua/cp/utils.lua \ + --replace-fail "local _nix_python = nil" \ + "local _nix_python = '${pythonEnv.interpreter}'" + ''; + nvimSkipModule = [ + "cp.pickers.telescope" + "cp.version" + ]; + passthru = { inherit pythonEnv; }; + meta.description = "Competitive programming plugin for Neovim"; + }; + in + { + overlays.default = final: prev: { + vimPlugins = prev.vimPlugins // { + cp-nvim = mkPlugin final; + }; + }; + + packages = eachSystem (system: { + default = mkPlugin (pkgsFor system); + pythonEnv = mkPythonEnv (pkgsFor system); + }); + + devShells = eachSystem (system: { + default = (pkgsFor system).mkShell { + packages = with (pkgsFor system); [ + uv + python312 + ]; + }; + }); + }; } diff --git a/lua/cp/health.lua b/lua/cp/health.lua index ba3879a..6a3913f 100644 --- a/lua/cp/health.lua +++ b/lua/cp/health.lua @@ -5,6 +5,8 @@ local utils = require('cp.utils') local function check() vim.health.start('cp.nvim [required] ~') + utils.setup_python_env() + if vim.fn.has('nvim-0.10.0') == 1 then vim.health.ok('Neovim 0.10.0+ detected') else @@ -16,22 +18,31 @@ local function check() vim.health.error('Windows is not supported') end - if vim.fn.executable('uv') == 1 then - vim.health.ok('uv executable found') - local r = vim.system({ 'uv', '--version' }, { text = true }):wait() + if utils.is_nix_build() then + vim.health.ok('Nix-built Python environment detected') + local py = utils.get_nix_python() + local r = vim.system({ py, '--version' }, { text = true }):wait() if r.code == 0 then - vim.health.info('uv version: ' .. r.stdout:gsub('\n', '')) + vim.health.info('Python: ' .. r.stdout:gsub('\n', '')) end else - vim.health.warn('uv not found (install https://docs.astral.sh/uv/ for scraping)') - end + if vim.fn.executable('uv') == 1 then + vim.health.ok('uv executable found') + local r = vim.system({ 'uv', '--version' }, { text = true }):wait() + if r.code == 0 then + vim.health.info('uv version: ' .. r.stdout:gsub('\n', '')) + end + else + vim.health.warn('uv not found (install https://docs.astral.sh/uv/ for scraping)') + end - local plugin_path = utils.get_plugin_path() - local venv_dir = plugin_path .. '/.venv' - if vim.fn.isdirectory(venv_dir) == 1 then - vim.health.ok('Python virtual environment found at ' .. venv_dir) - else - vim.health.info('Python virtual environment not set up (created on first scrape)') + local plugin_path = utils.get_plugin_path() + local venv_dir = plugin_path .. '/.venv' + if vim.fn.isdirectory(venv_dir) == 1 then + vim.health.ok('Python virtual environment found at ' .. venv_dir) + else + vim.health.info('Python virtual environment not set up (created on first scrape)') + end end local time_cap = utils.time_capability() diff --git a/lua/cp/scraper.lua b/lua/cp/scraper.lua index c42d8be..4771cc1 100644 --- a/lua/cp/scraper.lua +++ b/lua/cp/scraper.lua @@ -26,7 +26,8 @@ end ---@param opts { sync?: boolean, ndjson?: boolean, on_event?: fun(ev: table), on_exit?: fun(result: table) } local function run_scraper(platform, subcommand, args, opts) local plugin_path = utils.get_plugin_path() - local cmd = { 'uv', 'run', '--directory', plugin_path, '-m', 'scrapers.' .. platform, subcommand } + local cmd = utils.get_python_cmd(platform, plugin_path) + vim.list_extend(cmd, { subcommand }) vim.list_extend(cmd, args) local env = vim.fn.environ() @@ -43,7 +44,7 @@ local function run_scraper(platform, subcommand, args, opts) local handle handle = uv.spawn( cmd[1], - { args = vim.list_slice(cmd, 2), stdio = { nil, stdout, stderr }, env = env }, + { args = vim.list_slice(cmd, 2), stdio = { nil, stdout, stderr }, env = env, cwd = plugin_path }, function(code, signal) if buf ~= '' and opts.on_event then local ok_tail, ev_tail = pcall(vim.json.decode, buf) @@ -102,7 +103,7 @@ local function run_scraper(platform, subcommand, args, opts) return end - local sysopts = { text = true, timeout = 30000, env = env } + local sysopts = { text = true, timeout = 30000, env = env, cwd = plugin_path } if opts and opts.sync then local result = vim.system(cmd, sysopts):wait() return syshandle(result) diff --git a/lua/cp/ui/views.lua b/lua/cp/ui/views.lua index c1d25fc..2d67569 100644 --- a/lua/cp/ui/views.lua +++ b/lua/cp/ui/views.lua @@ -121,13 +121,22 @@ function M.toggle_interactive(interactor_cmd) end local orchestrator = vim.fn.fnamemodify(utils.get_plugin_path() .. '/scripts/interact.py', ':p') - cmdline = table.concat({ - 'uv', - 'run', - vim.fn.shellescape(orchestrator), - vim.fn.shellescape(interactor), - vim.fn.shellescape(binary), - }, ' ') + if utils.is_nix_build() then + cmdline = table.concat({ + vim.fn.shellescape(utils.get_nix_python()), + vim.fn.shellescape(orchestrator), + vim.fn.shellescape(interactor), + vim.fn.shellescape(binary), + }, ' ') + else + cmdline = table.concat({ + 'uv', + 'run', + vim.fn.shellescape(orchestrator), + vim.fn.shellescape(interactor), + vim.fn.shellescape(binary), + }, ' ') + end else cmdline = vim.fn.shellescape(binary) end diff --git a/lua/cp/utils.lua b/lua/cp/utils.lua index 654c2ef..0ac4916 100644 --- a/lua/cp/utils.lua +++ b/lua/cp/utils.lua @@ -2,6 +2,8 @@ local M = {} local logger = require('cp.log') +local _nix_python = nil + local uname = vim.loop.os_uname() local _time_cached = false @@ -79,43 +81,116 @@ function M.get_plugin_path() return vim.fn.fnamemodify(plugin_path, ':h:h:h') end +function M.is_nix_build() + return _nix_python ~= nil +end + +function M.get_nix_python() + return _nix_python +end + +function M.get_python_cmd(module, plugin_path) + if _nix_python then + return { _nix_python, '-m', 'scrapers.' .. module } + end + return { 'uv', 'run', '--directory', plugin_path, '-m', 'scrapers.' .. module } +end + local python_env_setup = false +local function discover_nix_python() + local cache_dir = vim.fn.stdpath('cache') .. '/cp-nvim' + local cache_file = cache_dir .. '/nix-python' + + local f = io.open(cache_file, 'r') + if f then + local cached = f:read('*l') + f:close() + if cached and vim.fn.executable(cached) == 1 then + _nix_python = cached + return true + end + end + + local plugin_path = M.get_plugin_path() + local result = vim + .system( + { 'nix', 'build', plugin_path .. '#pythonEnv', '--no-link', '--print-out-paths' }, + { text = true } + ) + :wait() + + if result.code ~= 0 then + logger.log('nix build #pythonEnv failed: ' .. (result.stderr or ''), vim.log.levels.WARN) + return false + end + + local store_path = result.stdout:gsub('%s+$', '') + local python_path = store_path .. '/bin/python3' + + if vim.fn.executable(python_path) ~= 1 then + logger.log('nix python not executable at ' .. python_path, vim.log.levels.WARN) + return false + end + + vim.fn.mkdir(cache_dir, 'p') + f = io.open(cache_file, 'w') + if f then + f:write(python_path) + f:close() + end + + _nix_python = python_path + return true +end + ---@return boolean success function M.setup_python_env() if python_env_setup then return true end - local plugin_path = M.get_plugin_path() - local venv_dir = plugin_path .. '/.venv' - - if vim.fn.executable('uv') == 0 then - logger.log( - 'uv is not installed. Install it to enable problem scraping: https://docs.astral.sh/uv/', - vim.log.levels.WARN - ) - return false + if _nix_python then + python_env_setup = true + return true end - if vim.fn.isdirectory(venv_dir) == 0 then - logger.log('Setting up Python environment for scrapers...') - local env = vim.fn.environ() - env.VIRTUAL_ENV = '' - env.PYTHONPATH = '' - env.CONDA_PREFIX = '' - local result = vim - .system({ 'uv', 'sync' }, { cwd = plugin_path, text = true, env = env }) - :wait() - if result.code ~= 0 then - logger.log('Failed to setup Python environment: ' .. result.stderr, vim.log.levels.ERROR) - return false + if vim.fn.executable('uv') == 1 then + local plugin_path = M.get_plugin_path() + local venv_dir = plugin_path .. '/.venv' + + if vim.fn.isdirectory(venv_dir) == 0 then + logger.log('Setting up Python environment for scrapers...') + local env = vim.fn.environ() + env.VIRTUAL_ENV = '' + env.PYTHONPATH = '' + env.CONDA_PREFIX = '' + local result = vim + .system({ 'uv', 'sync' }, { cwd = plugin_path, text = true, env = env }) + :wait() + if result.code ~= 0 then + logger.log('Failed to setup Python environment: ' .. result.stderr, vim.log.levels.ERROR) + return false + end + logger.log('Python environment setup complete.') end - logger.log('Python environment setup complete.') + + python_env_setup = true + return true end - python_env_setup = true - return true + if vim.fn.executable('nix') == 1 then + if discover_nix_python() then + python_env_setup = true + return true + end + end + + logger.log( + 'No Python environment available. Install uv (https://docs.astral.sh/uv/) or use nix.', + vim.log.levels.WARN + ) + return false end --- Configure the buffer with good defaults @@ -170,12 +245,8 @@ function M.check_required_runtime() return false, 'GNU timeout not found: ' .. (timeout.reason or '') end - if vim.fn.executable('uv') ~= 1 then - return false, 'uv not found (https://docs.astral.sh/uv/)' - end - if not M.setup_python_env() then - return false, 'failed to set up Python virtual environment' + return false, 'no Python environment available (install uv or nix)' end return true diff --git a/pyproject.toml b/pyproject.toml index 44e46a4..b18e7aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,8 +12,6 @@ dependencies = [ "ndjson>=0.3.1", "pydantic>=2.11.10", "requests>=2.32.5", - "scrapling[fetchers]>=0.3.5", - "types-requests>=2.32.4.20250913", ] [dependency-groups] diff --git a/scrapers/codechef.py b/scrapers/codechef.py index 0687c1e..46c2a1c 100644 --- a/scrapers/codechef.py +++ b/scrapers/codechef.py @@ -6,7 +6,7 @@ import re from typing import Any import httpx -from scrapling.fetchers import Fetcher +from curl_cffi import requests as curl_requests from .base import BaseScraper from .models import ( @@ -50,8 +50,9 @@ def _extract_memory_limit(html: str) -> float: def _fetch_html_sync(url: str) -> str: - response = Fetcher.get(url) - return str(response.body) + response = curl_requests.get(url, impersonate="chrome", timeout=TIMEOUT_S) + response.raise_for_status() + return response.text class CodeChefScraper(BaseScraper): diff --git a/scrapers/codeforces.py b/scrapers/codeforces.py index cf172b8..1d01b8d 100644 --- a/scrapers/codeforces.py +++ b/scrapers/codeforces.py @@ -2,13 +2,12 @@ import asyncio import json -import logging import re from typing import Any import requests from bs4 import BeautifulSoup, Tag -from scrapling.fetchers import Fetcher +from curl_cffi import requests as curl_requests from .base import BaseScraper from .models import ( @@ -19,10 +18,6 @@ from .models import ( TestCase, ) -# suppress scrapling logging - https://github.com/D4Vinci/Scrapling/issues/31) -logging.getLogger("scrapling").setLevel(logging.CRITICAL) - - BASE_URL = "https://codeforces.com" API_CONTEST_LIST_URL = f"{BASE_URL}/api/contest.list" TIMEOUT_SECONDS = 30 @@ -140,10 +135,9 @@ def _is_interactive(block: Tag) -> bool: def _fetch_problems_html(contest_id: str) -> str: url = f"{BASE_URL}/contest/{contest_id}/problems" - page = Fetcher.get( - url, - ) - return page.html_content + response = curl_requests.get(url, impersonate="chrome", timeout=TIMEOUT_SECONDS) + response.raise_for_status() + return response.text def _parse_all_blocks(html: str) -> list[dict[str, Any]]: diff --git a/tests/conftest.py b/tests/conftest.py index aaefec8..deb7e3a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ from typing import Any import httpx import pytest import requests -from scrapling import fetchers +from curl_cffi import requests as curl_requests ROOT = Path(__file__).resolve().parent.parent FIX = Path(__file__).resolve().parent / "fixtures" @@ -136,12 +136,15 @@ def run_scraper_offline(fixture_text): case "codeforces": - class MockCodeForcesPage: + class MockCurlResponse: def __init__(self, html: str): - self.html_content = html + self.text = html - def _mock_stealthy_fetch(url: str, **kwargs): - return MockCodeForcesPage(_router_codeforces(url=url)) + def raise_for_status(self): + pass + + def _mock_curl_get(url: str, **kwargs): + return MockCurlResponse(_router_codeforces(url=url)) def _mock_requests_get(url: str, **kwargs): if "api/contest.list" in url: @@ -172,7 +175,7 @@ def run_scraper_offline(fixture_text): raise AssertionError(f"Unexpected requests.get call: {url}") return { - "Fetcher.get": _mock_stealthy_fetch, + "curl_requests.get": _mock_curl_get, "requests.get": _mock_requests_get, } @@ -212,21 +215,23 @@ def run_scraper_offline(fixture_text): return MockResponse(data) raise AssertionError(f"No fixture for CodeChef url={url!r}") - class MockCodeChefPage: + class MockCodeChefCurlResponse: def __init__(self, html: str): - self.body = html - self.status = 200 + self.text = html - def _mock_stealthy_fetch(url: str, **kwargs): + def raise_for_status(self): + pass + + def _mock_curl_get(url: str, **kwargs): if "/problems/" in url: problem_id = url.rstrip("/").split("/")[-1] html = fixture_text(f"codechef/{problem_id}.html") - return MockCodeChefPage(html) + return MockCodeChefCurlResponse(html) raise AssertionError(f"No fixture for CodeChef url={url!r}") return { "__offline_get_async": __offline_get_async, - "Fetcher.get": _mock_stealthy_fetch, + "curl_requests.get": _mock_curl_get, } case _: @@ -245,7 +250,7 @@ def run_scraper_offline(fixture_text): offline_fetches = _make_offline_fetches(scraper_name) if scraper_name == "codeforces": - fetchers.Fetcher.get = offline_fetches["Fetcher.get"] + curl_requests.get = offline_fetches["curl_requests.get"] requests.get = offline_fetches["requests.get"] elif scraper_name == "atcoder": ns._fetch = offline_fetches["_fetch"] @@ -254,7 +259,7 @@ def run_scraper_offline(fixture_text): httpx.AsyncClient.get = offline_fetches["__offline_fetch_text"] elif scraper_name == "codechef": httpx.AsyncClient.get = offline_fetches["__offline_get_async"] - fetchers.Fetcher.get = offline_fetches["Fetcher.get"] + curl_requests.get = offline_fetches["curl_requests.get"] scraper_class = getattr(ns, scraper_classes[scraper_name]) scraper = scraper_class() diff --git a/uv.lock b/uv.lock index 01156d6..66bd81e 100644 --- a/uv.lock +++ b/uv.lock @@ -2,130 +2,6 @@ version = 1 revision = 3 requires-python = ">=3.11" -[[package]] -name = "aiohappyeyeballs" -version = "2.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, -] - -[[package]] -name = "aiohttp" -version = "3.13.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohappyeyeballs" }, - { name = "aiosignal" }, - { name = "attrs" }, - { name = "frozenlist" }, - { name = "multidict" }, - { name = "propcache" }, - { name = "yarl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1c/ce/3b83ebba6b3207a7135e5fcaba49706f8a4b6008153b4e30540c982fae26/aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca", size = 7837994, upload-time = "2025-10-28T20:59:39.937Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/74/b321e7d7ca762638cdf8cdeceb39755d9c745aff7a64c8789be96ddf6e96/aiohttp-3.13.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4647d02df098f6434bafd7f32ad14942f05a9caa06c7016fdcc816f343997dd0", size = 743409, upload-time = "2025-10-28T20:56:00.354Z" }, - { url = "https://files.pythonhosted.org/packages/99/3d/91524b905ec473beaf35158d17f82ef5a38033e5809fe8742e3657cdbb97/aiohttp-3.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e3403f24bcb9c3b29113611c3c16a2a447c3953ecf86b79775e7be06f7ae7ccb", size = 497006, upload-time = "2025-10-28T20:56:01.85Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d3/7f68bc02a67716fe80f063e19adbd80a642e30682ce74071269e17d2dba1/aiohttp-3.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:43dff14e35aba17e3d6d5ba628858fb8cb51e30f44724a2d2f0c75be492c55e9", size = 493195, upload-time = "2025-10-28T20:56:03.314Z" }, - { url = "https://files.pythonhosted.org/packages/98/31/913f774a4708775433b7375c4f867d58ba58ead833af96c8af3621a0d243/aiohttp-3.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2a9ea08e8c58bb17655630198833109227dea914cd20be660f52215f6de5613", size = 1747759, upload-time = "2025-10-28T20:56:04.904Z" }, - { url = "https://files.pythonhosted.org/packages/e8/63/04efe156f4326f31c7c4a97144f82132c3bb21859b7bb84748d452ccc17c/aiohttp-3.13.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53b07472f235eb80e826ad038c9d106c2f653584753f3ddab907c83f49eedead", size = 1704456, upload-time = "2025-10-28T20:56:06.986Z" }, - { url = "https://files.pythonhosted.org/packages/8e/02/4e16154d8e0a9cf4ae76f692941fd52543bbb148f02f098ca73cab9b1c1b/aiohttp-3.13.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e736c93e9c274fce6419af4aac199984d866e55f8a4cec9114671d0ea9688780", size = 1807572, upload-time = "2025-10-28T20:56:08.558Z" }, - { url = "https://files.pythonhosted.org/packages/34/58/b0583defb38689e7f06798f0285b1ffb3a6fb371f38363ce5fd772112724/aiohttp-3.13.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ff5e771f5dcbc81c64898c597a434f7682f2259e0cd666932a913d53d1341d1a", size = 1895954, upload-time = "2025-10-28T20:56:10.545Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f3/083907ee3437425b4e376aa58b2c915eb1a33703ec0dc30040f7ae3368c6/aiohttp-3.13.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3b6fb0c207cc661fa0bf8c66d8d9b657331ccc814f4719468af61034b478592", size = 1747092, upload-time = "2025-10-28T20:56:12.118Z" }, - { url = "https://files.pythonhosted.org/packages/ac/61/98a47319b4e425cc134e05e5f3fc512bf9a04bf65aafd9fdcda5d57ec693/aiohttp-3.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:97a0895a8e840ab3520e2288db7cace3a1981300d48babeb50e7425609e2e0ab", size = 1606815, upload-time = "2025-10-28T20:56:14.191Z" }, - { url = "https://files.pythonhosted.org/packages/97/4b/e78b854d82f66bb974189135d31fce265dee0f5344f64dd0d345158a5973/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9e8f8afb552297aca127c90cb840e9a1d4bfd6a10d7d8f2d9176e1acc69bad30", size = 1723789, upload-time = "2025-10-28T20:56:16.101Z" }, - { url = "https://files.pythonhosted.org/packages/ed/fc/9d2ccc794fc9b9acd1379d625c3a8c64a45508b5091c546dea273a41929e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ed2f9c7216e53c3df02264f25d824b079cc5914f9e2deba94155190ef648ee40", size = 1718104, upload-time = "2025-10-28T20:56:17.655Z" }, - { url = "https://files.pythonhosted.org/packages/66/65/34564b8765ea5c7d79d23c9113135d1dd3609173da13084830f1507d56cf/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:99c5280a329d5fa18ef30fd10c793a190d996567667908bef8a7f81f8202b948", size = 1785584, upload-time = "2025-10-28T20:56:19.238Z" }, - { url = "https://files.pythonhosted.org/packages/30/be/f6a7a426e02fc82781afd62016417b3948e2207426d90a0e478790d1c8a4/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ca6ffef405fc9c09a746cb5d019c1672cd7f402542e379afc66b370833170cf", size = 1595126, upload-time = "2025-10-28T20:56:20.836Z" }, - { url = "https://files.pythonhosted.org/packages/e5/c7/8e22d5d28f94f67d2af496f14a83b3c155d915d1fe53d94b66d425ec5b42/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:47f438b1a28e926c37632bff3c44df7d27c9b57aaf4e34b1def3c07111fdb782", size = 1800665, upload-time = "2025-10-28T20:56:22.922Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/91133c8b68b1da9fc16555706aa7276fdf781ae2bb0876c838dd86b8116e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9acda8604a57bb60544e4646a4615c1866ee6c04a8edef9b8ee6fd1d8fa2ddc8", size = 1739532, upload-time = "2025-10-28T20:56:25.924Z" }, - { url = "https://files.pythonhosted.org/packages/17/6b/3747644d26a998774b21a616016620293ddefa4d63af6286f389aedac844/aiohttp-3.13.2-cp311-cp311-win32.whl", hash = "sha256:868e195e39b24aaa930b063c08bb0c17924899c16c672a28a65afded9c46c6ec", size = 431876, upload-time = "2025-10-28T20:56:27.524Z" }, - { url = "https://files.pythonhosted.org/packages/c3/63/688462108c1a00eb9f05765331c107f95ae86f6b197b865d29e930b7e462/aiohttp-3.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:7fd19df530c292542636c2a9a85854fab93474396a52f1695e799186bbd7f24c", size = 456205, upload-time = "2025-10-28T20:56:29.062Z" }, - { url = "https://files.pythonhosted.org/packages/29/9b/01f00e9856d0a73260e86dd8ed0c2234a466c5c1712ce1c281548df39777/aiohttp-3.13.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b1e56bab2e12b2b9ed300218c351ee2a3d8c8fdab5b1ec6193e11a817767e47b", size = 737623, upload-time = "2025-10-28T20:56:30.797Z" }, - { url = "https://files.pythonhosted.org/packages/5a/1b/4be39c445e2b2bd0aab4ba736deb649fabf14f6757f405f0c9685019b9e9/aiohttp-3.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:364e25edaabd3d37b1db1f0cbcee8c73c9a3727bfa262b83e5e4cf3489a2a9dc", size = 492664, upload-time = "2025-10-28T20:56:32.708Z" }, - { url = "https://files.pythonhosted.org/packages/28/66/d35dcfea8050e131cdd731dff36434390479b4045a8d0b9d7111b0a968f1/aiohttp-3.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c5c94825f744694c4b8db20b71dba9a257cd2ba8e010a803042123f3a25d50d7", size = 491808, upload-time = "2025-10-28T20:56:34.57Z" }, - { url = "https://files.pythonhosted.org/packages/00/29/8e4609b93e10a853b65f8291e64985de66d4f5848c5637cddc70e98f01f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba2715d842ffa787be87cbfce150d5e88c87a98e0b62e0f5aa489169a393dbbb", size = 1738863, upload-time = "2025-10-28T20:56:36.377Z" }, - { url = "https://files.pythonhosted.org/packages/9d/fa/4ebdf4adcc0def75ced1a0d2d227577cd7b1b85beb7edad85fcc87693c75/aiohttp-3.13.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:585542825c4bc662221fb257889e011a5aa00f1ae4d75d1d246a5225289183e3", size = 1700586, upload-time = "2025-10-28T20:56:38.034Z" }, - { url = "https://files.pythonhosted.org/packages/da/04/73f5f02ff348a3558763ff6abe99c223381b0bace05cd4530a0258e52597/aiohttp-3.13.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39d02cb6025fe1aabca329c5632f48c9532a3dabccd859e7e2f110668972331f", size = 1768625, upload-time = "2025-10-28T20:56:39.75Z" }, - { url = "https://files.pythonhosted.org/packages/f8/49/a825b79ffec124317265ca7d2344a86bcffeb960743487cb11988ffb3494/aiohttp-3.13.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e67446b19e014d37342f7195f592a2a948141d15a312fe0e700c2fd2f03124f6", size = 1867281, upload-time = "2025-10-28T20:56:41.471Z" }, - { url = "https://files.pythonhosted.org/packages/b9/48/adf56e05f81eac31edcfae45c90928f4ad50ef2e3ea72cb8376162a368f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4356474ad6333e41ccefd39eae869ba15a6c5299c9c01dfdcfdd5c107be4363e", size = 1752431, upload-time = "2025-10-28T20:56:43.162Z" }, - { url = "https://files.pythonhosted.org/packages/30/ab/593855356eead019a74e862f21523db09c27f12fd24af72dbc3555b9bfd9/aiohttp-3.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeacf451c99b4525f700f078becff32c32ec327b10dcf31306a8a52d78166de7", size = 1562846, upload-time = "2025-10-28T20:56:44.85Z" }, - { url = "https://files.pythonhosted.org/packages/39/0f/9f3d32271aa8dc35036e9668e31870a9d3b9542dd6b3e2c8a30931cb27ae/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8a9b889aeabd7a4e9af0b7f4ab5ad94d42e7ff679aaec6d0db21e3b639ad58d", size = 1699606, upload-time = "2025-10-28T20:56:46.519Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3c/52d2658c5699b6ef7692a3f7128b2d2d4d9775f2a68093f74bca06cf01e1/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fa89cb11bc71a63b69568d5b8a25c3ca25b6d54c15f907ca1c130d72f320b76b", size = 1720663, upload-time = "2025-10-28T20:56:48.528Z" }, - { url = "https://files.pythonhosted.org/packages/9b/d4/8f8f3ff1fb7fb9e3f04fcad4e89d8a1cd8fc7d05de67e3de5b15b33008ff/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8aa7c807df234f693fed0ecd507192fc97692e61fee5702cdc11155d2e5cadc8", size = 1737939, upload-time = "2025-10-28T20:56:50.77Z" }, - { url = "https://files.pythonhosted.org/packages/03/d3/ddd348f8a27a634daae39a1b8e291ff19c77867af438af844bf8b7e3231b/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9eb3e33fdbe43f88c3c75fa608c25e7c47bbd80f48d012763cb67c47f39a7e16", size = 1555132, upload-time = "2025-10-28T20:56:52.568Z" }, - { url = "https://files.pythonhosted.org/packages/39/b8/46790692dc46218406f94374903ba47552f2f9f90dad554eed61bfb7b64c/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9434bc0d80076138ea986833156c5a48c9c7a8abb0c96039ddbb4afc93184169", size = 1764802, upload-time = "2025-10-28T20:56:54.292Z" }, - { url = "https://files.pythonhosted.org/packages/ba/e4/19ce547b58ab2a385e5f0b8aa3db38674785085abcf79b6e0edd1632b12f/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff15c147b2ad66da1f2cbb0622313f2242d8e6e8f9b79b5206c84523a4473248", size = 1719512, upload-time = "2025-10-28T20:56:56.428Z" }, - { url = "https://files.pythonhosted.org/packages/70/30/6355a737fed29dcb6dfdd48682d5790cb5eab050f7b4e01f49b121d3acad/aiohttp-3.13.2-cp312-cp312-win32.whl", hash = "sha256:27e569eb9d9e95dbd55c0fc3ec3a9335defbf1d8bc1d20171a49f3c4c607b93e", size = 426690, upload-time = "2025-10-28T20:56:58.736Z" }, - { url = "https://files.pythonhosted.org/packages/0a/0d/b10ac09069973d112de6ef980c1f6bb31cb7dcd0bc363acbdad58f927873/aiohttp-3.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:8709a0f05d59a71f33fd05c17fc11fcb8c30140506e13c2f5e8ee1b8964e1b45", size = 453465, upload-time = "2025-10-28T20:57:00.795Z" }, - { url = "https://files.pythonhosted.org/packages/bf/78/7e90ca79e5aa39f9694dcfd74f4720782d3c6828113bb1f3197f7e7c4a56/aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be", size = 732139, upload-time = "2025-10-28T20:57:02.455Z" }, - { url = "https://files.pythonhosted.org/packages/db/ed/1f59215ab6853fbaa5c8495fa6cbc39edfc93553426152b75d82a5f32b76/aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742", size = 490082, upload-time = "2025-10-28T20:57:04.784Z" }, - { url = "https://files.pythonhosted.org/packages/68/7b/fe0fe0f5e05e13629d893c760465173a15ad0039c0a5b0d0040995c8075e/aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293", size = 489035, upload-time = "2025-10-28T20:57:06.894Z" }, - { url = "https://files.pythonhosted.org/packages/d2/04/db5279e38471b7ac801d7d36a57d1230feeee130bbe2a74f72731b23c2b1/aiohttp-3.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811", size = 1720387, upload-time = "2025-10-28T20:57:08.685Z" }, - { url = "https://files.pythonhosted.org/packages/31/07/8ea4326bd7dae2bd59828f69d7fdc6e04523caa55e4a70f4a8725a7e4ed2/aiohttp-3.13.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a", size = 1688314, upload-time = "2025-10-28T20:57:10.693Z" }, - { url = "https://files.pythonhosted.org/packages/48/ab/3d98007b5b87ffd519d065225438cc3b668b2f245572a8cb53da5dd2b1bc/aiohttp-3.13.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4", size = 1756317, upload-time = "2025-10-28T20:57:12.563Z" }, - { url = "https://files.pythonhosted.org/packages/97/3d/801ca172b3d857fafb7b50c7c03f91b72b867a13abca982ed6b3081774ef/aiohttp-3.13.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a", size = 1858539, upload-time = "2025-10-28T20:57:14.623Z" }, - { url = "https://files.pythonhosted.org/packages/f7/0d/4764669bdf47bd472899b3d3db91fffbe925c8e3038ec591a2fd2ad6a14d/aiohttp-3.13.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e", size = 1739597, upload-time = "2025-10-28T20:57:16.399Z" }, - { url = "https://files.pythonhosted.org/packages/c4/52/7bd3c6693da58ba16e657eb904a5b6decfc48ecd06e9ac098591653b1566/aiohttp-3.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb", size = 1555006, upload-time = "2025-10-28T20:57:18.288Z" }, - { url = "https://files.pythonhosted.org/packages/48/30/9586667acec5993b6f41d2ebcf96e97a1255a85f62f3c653110a5de4d346/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded", size = 1683220, upload-time = "2025-10-28T20:57:20.241Z" }, - { url = "https://files.pythonhosted.org/packages/71/01/3afe4c96854cfd7b30d78333852e8e851dceaec1c40fd00fec90c6402dd2/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b", size = 1712570, upload-time = "2025-10-28T20:57:22.253Z" }, - { url = "https://files.pythonhosted.org/packages/11/2c/22799d8e720f4697a9e66fd9c02479e40a49de3de2f0bbe7f9f78a987808/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8", size = 1733407, upload-time = "2025-10-28T20:57:24.37Z" }, - { url = "https://files.pythonhosted.org/packages/34/cb/90f15dd029f07cebbd91f8238a8b363978b530cd128488085b5703683594/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04", size = 1550093, upload-time = "2025-10-28T20:57:26.257Z" }, - { url = "https://files.pythonhosted.org/packages/69/46/12dce9be9d3303ecbf4d30ad45a7683dc63d90733c2d9fe512be6716cd40/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476", size = 1758084, upload-time = "2025-10-28T20:57:28.349Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c8/0932b558da0c302ffd639fc6362a313b98fdf235dc417bc2493da8394df7/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23", size = 1716987, upload-time = "2025-10-28T20:57:30.233Z" }, - { url = "https://files.pythonhosted.org/packages/5d/8b/f5bd1a75003daed099baec373aed678f2e9b34f2ad40d85baa1368556396/aiohttp-3.13.2-cp313-cp313-win32.whl", hash = "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254", size = 425859, upload-time = "2025-10-28T20:57:32.105Z" }, - { url = "https://files.pythonhosted.org/packages/5d/28/a8a9fc6957b2cee8902414e41816b5ab5536ecf43c3b1843c10e82c559b2/aiohttp-3.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a", size = 452192, upload-time = "2025-10-28T20:57:34.166Z" }, - { url = "https://files.pythonhosted.org/packages/9b/36/e2abae1bd815f01c957cbf7be817b3043304e1c87bad526292a0410fdcf9/aiohttp-3.13.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2475391c29230e063ef53a66669b7b691c9bfc3f1426a0f7bcdf1216bdbac38b", size = 735234, upload-time = "2025-10-28T20:57:36.415Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e3/1ee62dde9b335e4ed41db6bba02613295a0d5b41f74a783c142745a12763/aiohttp-3.13.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f33c8748abef4d8717bb20e8fb1b3e07c6adacb7fd6beaae971a764cf5f30d61", size = 490733, upload-time = "2025-10-28T20:57:38.205Z" }, - { url = "https://files.pythonhosted.org/packages/1a/aa/7a451b1d6a04e8d15a362af3e9b897de71d86feac3babf8894545d08d537/aiohttp-3.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ae32f24bbfb7dbb485a24b30b1149e2f200be94777232aeadba3eecece4d0aa4", size = 491303, upload-time = "2025-10-28T20:57:40.122Z" }, - { url = "https://files.pythonhosted.org/packages/57/1e/209958dbb9b01174870f6a7538cd1f3f28274fdbc88a750c238e2c456295/aiohttp-3.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7f02042c1f009ffb70067326ef183a047425bb2ff3bc434ead4dd4a4a66a2b", size = 1717965, upload-time = "2025-10-28T20:57:42.28Z" }, - { url = "https://files.pythonhosted.org/packages/08/aa/6a01848d6432f241416bc4866cae8dc03f05a5a884d2311280f6a09c73d6/aiohttp-3.13.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93655083005d71cd6c072cdab54c886e6570ad2c4592139c3fb967bfc19e4694", size = 1667221, upload-time = "2025-10-28T20:57:44.869Z" }, - { url = "https://files.pythonhosted.org/packages/87/4f/36c1992432d31bbc789fa0b93c768d2e9047ec8c7177e5cd84ea85155f36/aiohttp-3.13.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0db1e24b852f5f664cd728db140cf11ea0e82450471232a394b3d1a540b0f906", size = 1757178, upload-time = "2025-10-28T20:57:47.216Z" }, - { url = "https://files.pythonhosted.org/packages/ac/b4/8e940dfb03b7e0f68a82b88fd182b9be0a65cb3f35612fe38c038c3112cf/aiohttp-3.13.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b009194665bcd128e23eaddef362e745601afa4641930848af4c8559e88f18f9", size = 1838001, upload-time = "2025-10-28T20:57:49.337Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ef/39f3448795499c440ab66084a9db7d20ca7662e94305f175a80f5b7e0072/aiohttp-3.13.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c038a8fdc8103cd51dbd986ecdce141473ffd9775a7a8057a6ed9c3653478011", size = 1716325, upload-time = "2025-10-28T20:57:51.327Z" }, - { url = "https://files.pythonhosted.org/packages/d7/51/b311500ffc860b181c05d91c59a1313bdd05c82960fdd4035a15740d431e/aiohttp-3.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66bac29b95a00db411cd758fea0e4b9bdba6d549dfe333f9a945430f5f2cc5a6", size = 1547978, upload-time = "2025-10-28T20:57:53.554Z" }, - { url = "https://files.pythonhosted.org/packages/31/64/b9d733296ef79815226dab8c586ff9e3df41c6aff2e16c06697b2d2e6775/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4ebf9cfc9ba24a74cf0718f04aac2a3bbe745902cc7c5ebc55c0f3b5777ef213", size = 1682042, upload-time = "2025-10-28T20:57:55.617Z" }, - { url = "https://files.pythonhosted.org/packages/3f/30/43d3e0f9d6473a6db7d472104c4eff4417b1e9df01774cb930338806d36b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a4b88ebe35ce54205c7074f7302bd08a4cb83256a3e0870c72d6f68a3aaf8e49", size = 1680085, upload-time = "2025-10-28T20:57:57.59Z" }, - { url = "https://files.pythonhosted.org/packages/16/51/c709f352c911b1864cfd1087577760ced64b3e5bee2aa88b8c0c8e2e4972/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:98c4fb90bb82b70a4ed79ca35f656f4281885be076f3f970ce315402b53099ae", size = 1728238, upload-time = "2025-10-28T20:57:59.525Z" }, - { url = "https://files.pythonhosted.org/packages/19/e2/19bd4c547092b773caeb48ff5ae4b1ae86756a0ee76c16727fcfd281404b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:ec7534e63ae0f3759df3a1ed4fa6bc8f75082a924b590619c0dd2f76d7043caa", size = 1544395, upload-time = "2025-10-28T20:58:01.914Z" }, - { url = "https://files.pythonhosted.org/packages/cf/87/860f2803b27dfc5ed7be532832a3498e4919da61299b4a1f8eb89b8ff44d/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5b927cf9b935a13e33644cbed6c8c4b2d0f25b713d838743f8fe7191b33829c4", size = 1742965, upload-time = "2025-10-28T20:58:03.972Z" }, - { url = "https://files.pythonhosted.org/packages/67/7f/db2fc7618925e8c7a601094d5cbe539f732df4fb570740be88ed9e40e99a/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:88d6c017966a78c5265d996c19cdb79235be5e6412268d7e2ce7dee339471b7a", size = 1697585, upload-time = "2025-10-28T20:58:06.189Z" }, - { url = "https://files.pythonhosted.org/packages/0c/07/9127916cb09bb38284db5036036042b7b2c514c8ebaeee79da550c43a6d6/aiohttp-3.13.2-cp314-cp314-win32.whl", hash = "sha256:f7c183e786e299b5d6c49fb43a769f8eb8e04a2726a2bd5887b98b5cc2d67940", size = 431621, upload-time = "2025-10-28T20:58:08.636Z" }, - { url = "https://files.pythonhosted.org/packages/fb/41/554a8a380df6d3a2bba8a7726429a23f4ac62aaf38de43bb6d6cde7b4d4d/aiohttp-3.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:fe242cd381e0fb65758faf5ad96c2e460df6ee5b2de1072fe97e4127927e00b4", size = 457627, upload-time = "2025-10-28T20:58:11Z" }, - { url = "https://files.pythonhosted.org/packages/c7/8e/3824ef98c039d3951cb65b9205a96dd2b20f22241ee17d89c5701557c826/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f10d9c0b0188fe85398c61147bbd2a657d616c876863bfeff43376e0e3134673", size = 767360, upload-time = "2025-10-28T20:58:13.358Z" }, - { url = "https://files.pythonhosted.org/packages/a4/0f/6a03e3fc7595421274fa34122c973bde2d89344f8a881b728fa8c774e4f1/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e7c952aefdf2460f4ae55c5e9c3e80aa72f706a6317e06020f80e96253b1accd", size = 504616, upload-time = "2025-10-28T20:58:15.339Z" }, - { url = "https://files.pythonhosted.org/packages/c6/aa/ed341b670f1bc8a6f2c6a718353d13b9546e2cef3544f573c6a1ff0da711/aiohttp-3.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c20423ce14771d98353d2e25e83591fa75dfa90a3c1848f3d7c68243b4fbded3", size = 509131, upload-time = "2025-10-28T20:58:17.693Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f0/c68dac234189dae5c4bbccc0f96ce0cc16b76632cfc3a08fff180045cfa4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e96eb1a34396e9430c19d8338d2ec33015e4a87ef2b4449db94c22412e25ccdf", size = 1864168, upload-time = "2025-10-28T20:58:20.113Z" }, - { url = "https://files.pythonhosted.org/packages/8f/65/75a9a76db8364b5d0e52a0c20eabc5d52297385d9af9c35335b924fafdee/aiohttp-3.13.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23fb0783bc1a33640036465019d3bba069942616a6a2353c6907d7fe1ccdaf4e", size = 1719200, upload-time = "2025-10-28T20:58:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/f5/55/8df2ed78d7f41d232f6bd3ff866b6f617026551aa1d07e2f03458f964575/aiohttp-3.13.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1a9bea6244a1d05a4e57c295d69e159a5c50d8ef16aa390948ee873478d9a5", size = 1843497, upload-time = "2025-10-28T20:58:24.672Z" }, - { url = "https://files.pythonhosted.org/packages/e9/e0/94d7215e405c5a02ccb6a35c7a3a6cfff242f457a00196496935f700cde5/aiohttp-3.13.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a3d54e822688b56e9f6b5816fb3de3a3a64660efac64e4c2dc435230ad23bad", size = 1935703, upload-time = "2025-10-28T20:58:26.758Z" }, - { url = "https://files.pythonhosted.org/packages/0b/78/1eeb63c3f9b2d1015a4c02788fb543141aad0a03ae3f7a7b669b2483f8d4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a653d872afe9f33497215745da7a943d1dc15b728a9c8da1c3ac423af35178e", size = 1792738, upload-time = "2025-10-28T20:58:29.787Z" }, - { url = "https://files.pythonhosted.org/packages/41/75/aaf1eea4c188e51538c04cc568040e3082db263a57086ea74a7d38c39e42/aiohttp-3.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:56d36e80d2003fa3fc0207fac644216d8532e9504a785ef9a8fd013f84a42c61", size = 1624061, upload-time = "2025-10-28T20:58:32.529Z" }, - { url = "https://files.pythonhosted.org/packages/9b/c2/3b6034de81fbcc43de8aeb209073a2286dfb50b86e927b4efd81cf848197/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:78cd586d8331fb8e241c2dd6b2f4061778cc69e150514b39a9e28dd050475661", size = 1789201, upload-time = "2025-10-28T20:58:34.618Z" }, - { url = "https://files.pythonhosted.org/packages/c9/38/c15dcf6d4d890217dae79d7213988f4e5fe6183d43893a9cf2fe9e84ca8d/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:20b10bbfbff766294fe99987f7bb3b74fdd2f1a2905f2562132641ad434dcf98", size = 1776868, upload-time = "2025-10-28T20:58:38.835Z" }, - { url = "https://files.pythonhosted.org/packages/04/75/f74fd178ac81adf4f283a74847807ade5150e48feda6aef024403716c30c/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9ec49dff7e2b3c85cdeaa412e9d438f0ecd71676fde61ec57027dd392f00c693", size = 1790660, upload-time = "2025-10-28T20:58:41.507Z" }, - { url = "https://files.pythonhosted.org/packages/e7/80/7368bd0d06b16b3aba358c16b919e9c46cf11587dc572091031b0e9e3ef0/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:94f05348c4406450f9d73d38efb41d669ad6cd90c7ee194810d0eefbfa875a7a", size = 1617548, upload-time = "2025-10-28T20:58:43.674Z" }, - { url = "https://files.pythonhosted.org/packages/7d/4b/a6212790c50483cb3212e507378fbe26b5086d73941e1ec4b56a30439688/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:fa4dcb605c6f82a80c7f95713c2b11c3b8e9893b3ebd2bc9bde93165ed6107be", size = 1817240, upload-time = "2025-10-28T20:58:45.787Z" }, - { url = "https://files.pythonhosted.org/packages/ff/f7/ba5f0ba4ea8d8f3c32850912944532b933acbf0f3a75546b89269b9b7dde/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf00e5db968c3f67eccd2778574cf64d8b27d95b237770aa32400bd7a1ca4f6c", size = 1762334, upload-time = "2025-10-28T20:58:47.936Z" }, - { url = "https://files.pythonhosted.org/packages/7e/83/1a5a1856574588b1cad63609ea9ad75b32a8353ac995d830bf5da9357364/aiohttp-3.13.2-cp314-cp314t-win32.whl", hash = "sha256:d23b5fe492b0805a50d3371e8a728a9134d8de5447dce4c885f5587294750734", size = 464685, upload-time = "2025-10-28T20:58:50.642Z" }, - { url = "https://files.pythonhosted.org/packages/9f/4d/d22668674122c08f4d56972297c51a624e64b3ed1efaa40187607a7cb66e/aiohttp-3.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:ff0a7b0a82a7ab905cbda74006318d1b12e37c797eb1b0d4eb3e316cf47f658f", size = 498093, upload-time = "2025-10-28T20:58:52.782Z" }, -] - -[[package]] -name = "aiosignal" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "frozenlist" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, -] - [[package]] name = "annotated-types" version = "0.7.0" @@ -148,15 +24,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, ] -[[package]] -name = "attrs" -version = "25.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, -] - [[package]] name = "backoff" version = "2.2.1" @@ -191,44 +58,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, ] -[[package]] -name = "browserforge" -version = "1.2.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/5c/fe4d8cc5d5e61a5b1585190bba19d25bb76c45fdfe9c7bf264f5301fcf33/browserforge-1.2.3.tar.gz", hash = "sha256:d5bec6dffd4748b30fbac9f9c1ef33b26c01a23185240bf90011843e174b7ecc", size = 38072, upload-time = "2025-01-29T09:45:48.711Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/53/c60eb5bd26cf8689e361031bebc431437bc988555e80ba52d48c12c1d866/browserforge-1.2.3-py3-none-any.whl", hash = "sha256:a6c71ed4688b2f1b0bee757ca82ddad0007cbba68a71eca66ca607dde382f132", size = 39626, upload-time = "2025-01-29T09:45:47.531Z" }, -] - -[[package]] -name = "camoufox" -version = "0.4.11" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "browserforge" }, - { name = "click" }, - { name = "language-tags" }, - { name = "lxml" }, - { name = "numpy" }, - { name = "orjson" }, - { name = "platformdirs" }, - { name = "playwright" }, - { name = "pysocks" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "screeninfo" }, - { name = "tqdm" }, - { name = "typing-extensions" }, - { name = "ua-parser" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d3/15/e0a1b586e354ea6b8d6612717bf4372aaaa6753444d5d006caf0bb116466/camoufox-0.4.11.tar.gz", hash = "sha256:0a2c9d24ac5070c104e7c2b125c0a3937f70efa416084ef88afe94c32a72eebe", size = 64409, upload-time = "2025-01-29T09:33:20.019Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/7b/a2f099a5afb9660271b3f20f6056ba679e7ab4eba42682266a65d5730f7e/camoufox-0.4.11-py3-none-any.whl", hash = "sha256:83864d434d159a7566990aa6524429a8d1a859cbf84d2f64ef4a9f29e7d2e5ff", size = 71628, upload-time = "2025-01-29T09:33:18.558Z" }, -] - [[package]] name = "certifi" version = "2025.11.12" @@ -390,18 +219,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] -[[package]] -name = "click" -version = "8.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -411,15 +228,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] -[[package]] -name = "cssselect" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/0a/c3ea9573b1dc2e151abfe88c7fe0c26d1892fe6ed02d0cdb30f0d57029d5/cssselect-1.3.0.tar.gz", hash = "sha256:57f8a99424cfab289a1b6a816a43075a4b00948c86b4dcf3ef4ee7e15f7ab0c7", size = 42870, upload-time = "2025-03-10T09:30:29.638Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/58/257350f7db99b4ae12b614a36256d9cc870d71d9e451e79c2dc3b23d7c3c/cssselect-1.3.0-py3-none-any.whl", hash = "sha256:56d1bf3e198080cc1667e137bc51de9cadfca259f03c2d4e09037b3e01e30f0d", size = 18786, upload-time = "2025-03-10T09:30:28.048Z" }, -] - [[package]] name = "curl-cffi" version = "0.13.0" @@ -441,20 +249,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/0f/9c5275f17ad6ff5be70edb8e0120fdc184a658c9577ca426d4230f654beb/curl_cffi-0.13.0-cp39-abi3-win_arm64.whl", hash = "sha256:d438a3b45244e874794bc4081dc1e356d2bb926dcc7021e5a8fef2e2105ef1d8", size = 1365753, upload-time = "2025-08-06T13:05:41.879Z" }, ] -[[package]] -name = "cython" -version = "3.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/29/17/55fc687ba986f2210298fa2f60fec265fa3004c3f9a1e958ea1fe2d4e061/cython-3.2.2.tar.gz", hash = "sha256:c3add3d483acc73129a61d105389344d792c17e7c1cee24863f16416bd071634", size = 3275797, upload-time = "2025-11-30T12:48:20.942Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/ba/d785f60564a43bddbb7316134252a55d67ff6f164f0be90c4bf31482da82/cython-3.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d140c2701cbb8cf960300cf1b67f3b4fa9d294d32e51b85f329bff56936a82fd", size = 2951181, upload-time = "2025-11-30T12:48:39.723Z" }, - { url = "https://files.pythonhosted.org/packages/57/0f/6fd78dc581373722bb9dedfc90c35b59ba88af988756315af227a877c7a2/cython-3.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:692a41c8fe06fb2dc55ca2c8d71c80c469fd16fe69486ed99f3b3cbb2d3af83f", size = 2968037, upload-time = "2025-11-30T12:48:47.279Z" }, - { url = "https://files.pythonhosted.org/packages/a2/4f/b5355918962ec28b376eb8e357c718d58baf32d6df7017be8d147dd4ba29/cython-3.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa24cd0bdab27ca099b2467806c684404add597c1108e07ddf7b6471653c85d7", size = 2958578, upload-time = "2025-11-30T12:48:55.354Z" }, - { url = "https://files.pythonhosted.org/packages/c0/f2/cd60f639f0fde38b71319d7b6808e1ff17a6fd7f3feaff475b866a5c0aef/cython-3.2.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:177faf4d61e9f2d4d2db61194ac9ec16d3fe3041c1b6830f871a01935319eeb3", size = 2969023, upload-time = "2025-11-30T12:49:02.734Z" }, - { url = "https://files.pythonhosted.org/packages/f4/69/5430879d35235ec3d5ffd778862173b6419390509ae4e37a72bdd45d9e86/cython-3.2.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:a6387e3ad31342443916db9a419509935fddd8d4cbac34aab9c895ae55326a56", size = 2874031, upload-time = "2025-11-30T12:49:18.34Z" }, - { url = "https://files.pythonhosted.org/packages/76/f2/98fd8d0b514622a789fd2824b59bd6041b799aaeeba14a8d92d52f6654dd/cython-3.2.2-py3-none-any.whl", hash = "sha256:13b99ecb9482aff6a6c12d1ca6feef6940c507af909914b49f568de74fa965fb", size = 1255106, upload-time = "2025-11-30T12:48:18.454Z" }, -] - [[package]] name = "distlib" version = "0.4.0" @@ -473,172 +267,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, ] -[[package]] -name = "frozenlist" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, - { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, - { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, - { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, - { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, - { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, - { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, - { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, - { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, - { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, - { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, - { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, - { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, - { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, - { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, - { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, - { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, - { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, - { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, - { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, - { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, - { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, - { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, - { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, - { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, - { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, - { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, - { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, - { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, - { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, - { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, - { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, - { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, - { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, - { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, - { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, - { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, - { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, - { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, - { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, - { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, - { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, - { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, - { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, - { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, - { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, - { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, - { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, - { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, - { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, - { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, - { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, - { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, - { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, - { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, - { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, - { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, - { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, - { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, - { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, - { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, - { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, - { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, - { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, - { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, - { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, - { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, - { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, - { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, - { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, - { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, - { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, - { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, -] - -[[package]] -name = "geoip2" -version = "5.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "maxminddb" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/18/70/3d9e87289f79713aaf0fea9df4aa8e68776640fe59beb6299bb214610cfd/geoip2-5.2.0.tar.gz", hash = "sha256:6c9ded1953f8eb16043ed0a8ea20e6e9524ea7b65eb745724e12490aca44ef00", size = 176498, upload-time = "2025-11-20T18:21:08.874Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/d2/d55df737199a52b9d06e742ed2a608c525f0677e40375951372e65714fbd/geoip2-5.2.0-py3-none-any.whl", hash = "sha256:3d1546fd4eb7cad20445d027d2d9e81d3a71c074e019383f30db5d45e2c23320", size = 28991, upload-time = "2025-11-20T18:21:07.178Z" }, -] - -[[package]] -name = "greenlet" -version = "3.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/cb/48e964c452ca2b92175a9b2dca037a553036cb053ba69e284650ce755f13/greenlet-3.3.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e29f3018580e8412d6aaf5641bb7745d38c85228dacf51a73bd4e26ddf2a6a8e", size = 274908, upload-time = "2025-12-04T14:23:26.435Z" }, - { url = "https://files.pythonhosted.org/packages/28/da/38d7bff4d0277b594ec557f479d65272a893f1f2a716cad91efeb8680953/greenlet-3.3.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a687205fb22794e838f947e2194c0566d3812966b41c78709554aa883183fb62", size = 577113, upload-time = "2025-12-04T14:50:05.493Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f2/89c5eb0faddc3ff014f1c04467d67dee0d1d334ab81fadbf3744847f8a8a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4243050a88ba61842186cb9e63c7dfa677ec146160b0efd73b855a3d9c7fcf32", size = 590338, upload-time = "2025-12-04T14:57:41.136Z" }, - { url = "https://files.pythonhosted.org/packages/80/d7/db0a5085035d05134f8c089643da2b44cc9b80647c39e93129c5ef170d8f/greenlet-3.3.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:670d0f94cd302d81796e37299bcd04b95d62403883b24225c6b5271466612f45", size = 601098, upload-time = "2025-12-04T15:07:11.898Z" }, - { url = "https://files.pythonhosted.org/packages/dc/a6/e959a127b630a58e23529972dbc868c107f9d583b5a9f878fb858c46bc1a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb3a8ec3db4a3b0eb8a3c25436c2d49e3505821802074969db017b87bc6a948", size = 590206, upload-time = "2025-12-04T14:26:01.254Z" }, - { url = "https://files.pythonhosted.org/packages/48/60/29035719feb91798693023608447283b266b12efc576ed013dd9442364bb/greenlet-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2de5a0b09eab81fc6a382791b995b1ccf2b172a9fec934747a7a23d2ff291794", size = 1550668, upload-time = "2025-12-04T15:04:22.439Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5f/783a23754b691bfa86bd72c3033aa107490deac9b2ef190837b860996c9f/greenlet-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4449a736606bd30f27f8e1ff4678ee193bc47f6ca810d705981cfffd6ce0d8c5", size = 1615483, upload-time = "2025-12-04T14:27:28.083Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d5/c339b3b4bc8198b7caa4f2bd9fd685ac9f29795816d8db112da3d04175bb/greenlet-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:7652ee180d16d447a683c04e4c5f6441bae7ba7b17ffd9f6b3aff4605e9e6f71", size = 301164, upload-time = "2025-12-04T14:42:51.577Z" }, - { url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" }, - { url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" }, - { url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" }, - { url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" }, - { url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" }, - { url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" }, - { url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" }, - { url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964, upload-time = "2025-12-04T14:36:58.316Z" }, - { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, - { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, - { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" }, - { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, - { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, - { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, - { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" }, - { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, - { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, - { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, - { url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" }, - { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, - { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" }, - { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, - { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, - { url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" }, - { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, - { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, - { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, -] - [[package]] name = "h11" version = "0.16.0" @@ -703,353 +331,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] -[[package]] -name = "language-tags" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/7e/b6a0efe4fee11e9742c1baaedf7c574084238a70b03c1d8eb2761383848f/language_tags-1.2.0.tar.gz", hash = "sha256:e934acba3e3dc85f867703eca421847a9ab7b7679b11b5d5cfd096febbf8bde6", size = 207901, upload-time = "2023-01-11T18:38:07.893Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/42/327554649ed2dd5ce59d3f5da176c7be20f9352c7c6c51597293660b7b08/language_tags-1.2.0-py3-none-any.whl", hash = "sha256:d815604622242fdfbbfd747b40c31213617fd03734a267f2e39ee4bd73c88722", size = 213449, upload-time = "2023-01-11T18:38:05.692Z" }, -] - -[[package]] -name = "lxml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" }, - { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" }, - { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" }, - { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" }, - { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" }, - { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" }, - { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" }, - { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" }, - { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" }, - { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" }, - { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" }, - { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" }, - { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" }, - { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" }, - { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, - { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, - { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, - { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, - { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, - { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, - { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, - { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, - { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, - { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, - { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, - { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, - { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, - { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, - { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, - { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, - { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, - { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, - { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, - { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, - { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, - { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, - { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, - { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, - { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, - { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, - { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, - { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, - { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, - { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, - { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, - { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, - { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, - { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, - { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, - { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, - { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, - { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, - { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, - { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, - { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, - { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, - { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, - { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, - { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, - { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, - { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, - { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, - { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, - { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, - { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, - { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, - { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, - { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, - { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, - { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, - { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, - { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, - { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" }, - { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" }, - { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" }, - { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" }, - { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" }, -] - -[[package]] -name = "maxminddb" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/6e/6adbb0b2280a853e8b3344737fea5167e8a2a2ff67168555589b7278e2e8/maxminddb-3.0.0.tar.gz", hash = "sha256:9792b19625945dff146e2e3187f9e470b82330a912f7cea5581b8bd5af30da8b", size = 199784, upload-time = "2025-10-15T20:50:07.283Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/15/a3c156c609b59342799fd1c0e60e4a6f6e096e2a18107d88b61a39c76ab5/maxminddb-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8e34a0cd9a67f446a6b425b857ac7b63254b5ce64d0b38e013d0e531588b7a66", size = 53857, upload-time = "2025-10-15T20:48:38.447Z" }, - { url = "https://files.pythonhosted.org/packages/fa/42/8e367bfa2a9c8ffbe0f0b60a904d35b7cb9d6a0d3a3c8fd803a932b3226e/maxminddb-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4e1791a30ac50ec2bdd2203276bcb9da25ff1115fd6f152548b5110993297d14", size = 36118, upload-time = "2025-10-15T20:48:39.551Z" }, - { url = "https://files.pythonhosted.org/packages/47/83/367902eb1e01955d491251812af2226a7d2b2fa3893c1099b5690119ac44/maxminddb-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:722d4e5c5735f5ae2dcb0ccf972e890c41bccd15476fafe8c2a991f72c4a28d2", size = 36044, upload-time = "2025-10-15T20:48:40.927Z" }, - { url = "https://files.pythonhosted.org/packages/1d/3b/ae5ead6809bc26bd61bab0548d08983a7b2955159df092305eaa45909f6f/maxminddb-3.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c71ec72fdbec8be4d1e9b53294d59f04c7ae73ede6273efce7516995bcb5468", size = 99409, upload-time = "2025-10-15T20:48:42.775Z" }, - { url = "https://files.pythonhosted.org/packages/84/0e/3f4bd90ba3def4b490594866641585b93e3deeef4a09aacde921e223629a/maxminddb-3.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f62b55d15f796d14a58c84a853a8ae6d2728546f5c81a15a61aa45082afc21c8", size = 97114, upload-time = "2025-10-15T20:48:44.378Z" }, - { url = "https://files.pythonhosted.org/packages/2e/3d/49dea88a0931b5764e582e628a55ac2d5a64a8d97ece1853bf751cb43fd3/maxminddb-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78ab0b386ab51ea21d54ca42dee379dfd976d5e650ae0435848278b2aaf9d0fb", size = 96833, upload-time = "2025-10-15T20:48:45.916Z" }, - { url = "https://files.pythonhosted.org/packages/f0/ec/4419c88774ae183bdfb386882796c85675767339eddc41d86ec3df68f61f/maxminddb-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9167c80dac8a524af24af22b2093c819cb2ea11c2abe986b6b29be7fa7f6c88f", size = 95065, upload-time = "2025-10-15T20:48:47.096Z" }, - { url = "https://files.pythonhosted.org/packages/56/40/31fcbe0052848292d8ea17bc3779a8a5f83fd090338a83aeb8c71057ad97/maxminddb-3.0.0-cp311-cp311-win32.whl", hash = "sha256:deb2e6bc068db799eac025ab9d1cbf96cd9fbf636a3414a79518e05fe57ae5a3", size = 35341, upload-time = "2025-10-15T20:48:48.232Z" }, - { url = "https://files.pythonhosted.org/packages/26/53/48e939bc13be41337aa23c30a3b45122e610a8fa8419af44265ff309dfa2/maxminddb-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:87062aec30c57af1a6e1c007391f20f1af836a459486801f169af44bc244c9e7", size = 37132, upload-time = "2025-10-15T20:48:49.324Z" }, - { url = "https://files.pythonhosted.org/packages/ef/f0/df860ee2b8db38332416ceab8756a36d5b5ed82d3f1cccc8978cc716548a/maxminddb-3.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:612497302bf77d7c90586fc3a19b941e0c78b47f92df035e80550f044a849c96", size = 34258, upload-time = "2025-10-15T20:48:50.404Z" }, - { url = "https://files.pythonhosted.org/packages/31/df/dec231686a814f9e279afb39f3e27091770d970964bb94e7bfc1fdf01428/maxminddb-3.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bcf83c60a44ec5dfab9e5d3a0c2347ee429d31fa89f88aa283d8551fd5e2c37a", size = 54352, upload-time = "2025-10-15T20:48:51.488Z" }, - { url = "https://files.pythonhosted.org/packages/14/e3/efb6d621a8940371ecbf393f84fde01f0521116bc281c40124292a593198/maxminddb-3.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:56856d0fadab323fb5dc3fa69bc4cb975242133cab1df2c710779738dadda75d", size = 36328, upload-time = "2025-10-15T20:48:52.85Z" }, - { url = "https://files.pythonhosted.org/packages/51/e8/17cbe454829befb32fec83745141bb6f9ef0b593d53c4e333e938d83ed26/maxminddb-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1bd05d919787719fc1026d53b0e7462cf0c389534620e407676ecf61c2d289bb", size = 36174, upload-time = "2025-10-15T20:48:53.874Z" }, - { url = "https://files.pythonhosted.org/packages/ae/1d/5492205210570d851d5a74f5c9c01022993edc74296eb792c890318eff25/maxminddb-3.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29515dc3606d1d8fffdb4025dccf01c93d16651683e9c6d8611892a4c9f2566d", size = 101153, upload-time = "2025-10-15T20:48:55.013Z" }, - { url = "https://files.pythonhosted.org/packages/a6/07/f96b5e4fdfdd2cc7a9724f3fa40b6bc282c9d9bdcf85b1920a0dee50c00b/maxminddb-3.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52b5edc32894643c93279de2d889c0b98906277e7e91cbba709bc55f5500ecca", size = 99465, upload-time = "2025-10-15T20:48:56.198Z" }, - { url = "https://files.pythonhosted.org/packages/d7/30/ef2c167277292ce360bcd2a11e0fa9fe2e4e67e7c7b49fff2eab7caae787/maxminddb-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0a095ce04e404315f9d47a186a7d96b11a283430d811ba6b0530167233100b95", size = 98395, upload-time = "2025-10-15T20:48:57.489Z" }, - { url = "https://files.pythonhosted.org/packages/04/c9/71ce286a4ba12ec74b094d1a627d57a306349f4f23ce66d3ec2eca045e9f/maxminddb-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d3645a44c392d9ffdea4d2252d70b2910eee47d56b8305da0c0923a63e895d6", size = 97320, upload-time = "2025-10-15T20:48:58.7Z" }, - { url = "https://files.pythonhosted.org/packages/da/4a/3e3f24f876242dd53a8a95250669e2f08b8cd8bc4640e947c982efcdaca6/maxminddb-3.0.0-cp312-cp312-win32.whl", hash = "sha256:c0e6d54da5d85d38e674fee9b04b1ad9212c38cb57adcc7c86bb4ed71b2b6555", size = 35481, upload-time = "2025-10-15T20:49:00.755Z" }, - { url = "https://files.pythonhosted.org/packages/71/de/56feda63d5d8d896c2dcfa6ef9754a429fa2c5353fa5f0c32ed1f46fa004/maxminddb-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:4931ee0cbba030e1b729599e485aca438b668432ccd1eb73770c93bbc38f2b60", size = 37295, upload-time = "2025-10-15T20:49:01.993Z" }, - { url = "https://files.pythonhosted.org/packages/87/4a/87c86516dee431a9e6cdded7eb865b5b7fc7c73b17262a50c75e2da5c9b6/maxminddb-3.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:f55fb5c607dc4ddab7eba67da92d75921ef7d8e682ab47d21935566dc6990021", size = 34263, upload-time = "2025-10-15T20:49:03.237Z" }, - { url = "https://files.pythonhosted.org/packages/e6/4e/9c97eaea080d450ab63197a20ead71cc652f99f5ecb1e68fc0896db33ac8/maxminddb-3.0.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:f2162e6bee9643d86647518c891756ef5091afb1c2d522fc206d7c26187862eb", size = 37615, upload-time = "2025-10-15T20:49:04.728Z" }, - { url = "https://files.pythonhosted.org/packages/b4/a1/900f105a1562667e22566bbbfa3ca8ca6ea2b2e7e31ff30673459809be74/maxminddb-3.0.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:eace3ccb184546287d27fce54852751e935c00f9ba7b66fece09e7761503cd13", size = 38070, upload-time = "2025-10-15T20:49:06.138Z" }, - { url = "https://files.pythonhosted.org/packages/39/6b/247ef29d080be9b57efba90e33445e2cc028f8cf09beb5e697e5132c95dd/maxminddb-3.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:babf6c600361e5f9bc3e3873b900ab044f6cdc7c0f15c086e0b52d2e005ab949", size = 35365, upload-time = "2025-10-15T20:49:07.233Z" }, - { url = "https://files.pythonhosted.org/packages/7d/dd/ca6c4929e1a634507a99e8143c81ecc3f3f913e1a0c47b656b5a006538ec/maxminddb-3.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:024221e821f3385dd41f13e2d0ac5afca569b69a6d7755c8c960edaf31c0e47f", size = 35919, upload-time = "2025-10-15T20:49:08.3Z" }, - { url = "https://files.pythonhosted.org/packages/e6/41/a7faaf244114d47994fef85accd06dd906832cdcc5465ad27b48e0f11f2d/maxminddb-3.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bc838d9060e6e623b4bd055d498a2159a072d43beb3eeaefde5af39ac1b1b249", size = 54354, upload-time = "2025-10-15T20:49:09.417Z" }, - { url = "https://files.pythonhosted.org/packages/d6/c8/76b3c0ea1f180209496cb401892a4ad197ee23ac1f370da578fffa466418/maxminddb-3.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:29fd164067b2765752d5970aaef823a51d262a484c59e866c20dbf99f45453ac", size = 36312, upload-time = "2025-10-15T20:49:10.713Z" }, - { url = "https://files.pythonhosted.org/packages/b6/96/b2d5ab37458ec892d7d52b6a9e6aa9992354d61df20b9978bae60e35d17a/maxminddb-3.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6008ecba67b7024b80e3f28e276b736f2f984795cd4a6922ddffaba8038d6a60", size = 36174, upload-time = "2025-10-15T20:49:12.232Z" }, - { url = "https://files.pythonhosted.org/packages/ec/3d/c22a117c1c6ca42a62be9473f12d113e2eab72ac28c032a290d0fbbd488e/maxminddb-3.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:684fec138b463d1fc6fa88fd2967e25b3af0629eb0b5e6f3bbc017e64e2f68c6", size = 101205, upload-time = "2025-10-15T20:49:13.342Z" }, - { url = "https://files.pythonhosted.org/packages/df/e6/a170e6ae3492d8e334a6ce9e39668f2b8d0cb0a158804460b5d851315230/maxminddb-3.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2dd94deb1baa19f9fd17968b90b7c03589078a3000972948e3aecfa723300d1", size = 99495, upload-time = "2025-10-15T20:49:14.65Z" }, - { url = "https://files.pythonhosted.org/packages/7b/8b/a18aba0838a85bf9ff30f165d3cb5f52967858e89e54aa8a7509a674f253/maxminddb-3.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:880e233c00a4403bb6dd5e406f156be3c6a5a5b37b472102928014ab21c12b4b", size = 98402, upload-time = "2025-10-15T20:49:15.864Z" }, - { url = "https://files.pythonhosted.org/packages/2e/24/85d15613b6dbf2b683a5b9817640c3baac1931edf59a7465c54e0ad92084/maxminddb-3.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e8031353eaece26ee634bcba2cba3bb91092f52a69e5f5dbc5931d59f84b2de", size = 97303, upload-time = "2025-10-15T20:49:17.121Z" }, - { url = "https://files.pythonhosted.org/packages/1f/fb/23883b82abf92d0613375a9a9f4a4412ff8cc0596d124070832bf7f783a6/maxminddb-3.0.0-cp313-cp313-win32.whl", hash = "sha256:2d325fcdbf1ba356ac47304ba3fc8605b21b9bd09d0818b24f43ebecc71c5e29", size = 35481, upload-time = "2025-10-15T20:49:18.266Z" }, - { url = "https://files.pythonhosted.org/packages/4b/19/a498bf14a86e98475d4ca994988e8f072dccfd407d026403ad95725321de/maxminddb-3.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:3ad60671645bf88b853f126999cafd0e61ad668f210176ea24a8b5e99dd3e049", size = 37299, upload-time = "2025-10-15T20:49:19.488Z" }, - { url = "https://files.pythonhosted.org/packages/34/ca/36fecfbed3ef0175b569b07f968fb56d591a6effdaeda81f1247dc8034a4/maxminddb-3.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:4ca8989b0e389404f268c8650aeeadda34417f87baa405325b051a511f56c382", size = 34254, upload-time = "2025-10-15T20:49:20.533Z" }, - { url = "https://files.pythonhosted.org/packages/96/cb/eed553828b6bcd1bb8c3eb74818aee471c63aba6612128d73c20da122921/maxminddb-3.0.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:1a89feae4b7296f24a76467788dad73578bbf51e4cf9672e61ef1be1320dd3d6", size = 37412, upload-time = "2025-10-15T20:49:21.649Z" }, - { url = "https://files.pythonhosted.org/packages/65/3b/0dba6d1d078e2a0523bc0a89c0060b1366a2f9ee8f72f47c33a107e56fab/maxminddb-3.0.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e874238be93b6e6b3c6589795015edb9b935d2638806439ee65c66669e399b2d", size = 37876, upload-time = "2025-10-15T20:49:23.165Z" }, - { url = "https://files.pythonhosted.org/packages/a3/49/d4b0636aec3671c3aedf97013b24b6b310de62d6ab373b775a3a8dd594a9/maxminddb-3.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:a45fc20423952f84a73c008d40ea5b1d8c343a3c58d229a45d78a20093817a6f", size = 35334, upload-time = "2025-10-15T20:49:24.554Z" }, - { url = "https://files.pythonhosted.org/packages/37/d7/6ff7c7386365f639cff20fb1d0f4b6533b12706b4a0ae05cfdaf0b41f768/maxminddb-3.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d3796460179976fea3f99855bd75811af74f5659699584d4b7e80a7c66b52893", size = 35887, upload-time = "2025-10-15T20:49:25.614Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ee/e7d8942bbdf06a2082611d52e89ece4e6064195ddb18a7158b5f53e76bc7/maxminddb-3.0.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:7997f0b1ed0210b0790a1885e9bc38bed53fdcaaf37141cf8dd1a97894c8fa1b", size = 54335, upload-time = "2025-10-15T20:49:26.728Z" }, - { url = "https://files.pythonhosted.org/packages/77/e1/bd16679264463d2c340940b5b320cc97cd240a3a0b6c1811c88b82d292db/maxminddb-3.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:21f9d0c164f7c058f419cd9b1105f01606b2abf77b456e8d201700804667686f", size = 36528, upload-time = "2025-10-15T20:49:27.842Z" }, - { url = "https://files.pythonhosted.org/packages/6f/41/cf8a5ca1ede1fcd1b306d504a3949d52fd87124fcc2c2180afbcf714ff54/maxminddb-3.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e7fe7a2a6b2275736fd090d98e081146a1b81abf475d6d2dfd1b106d3792b208", size = 36147, upload-time = "2025-10-15T20:49:28.935Z" }, - { url = "https://files.pythonhosted.org/packages/4a/e0/cd48d467c34ac108fcee9e444dd537e27f04a945d787acd5614f1127dbe5/maxminddb-3.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aaa656d5cb2ea60f4e845669be6a759a25aa1f0cd67fbfef0e64759af28dbb7", size = 101010, upload-time = "2025-10-15T20:49:30.122Z" }, - { url = "https://files.pythonhosted.org/packages/4f/4b/323ec8abe811702bcea537a0aa5e83442f48f1974084bdb048b75424536c/maxminddb-3.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6b86dedb1ae681376bcc31bec37d7d674c86ff05687738ab333f18988f17c3a", size = 99249, upload-time = "2025-10-15T20:49:31.332Z" }, - { url = "https://files.pythonhosted.org/packages/d7/1a/520fcb6ad4185857fbba74cb1ee42b580492049c3730ce0687ac54dbe731/maxminddb-3.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fde874c2af4dd4488c423b4f4bc2ec9931e5e992f382feb43d07dc4e8dda5e24", size = 98418, upload-time = "2025-10-15T20:49:32.879Z" }, - { url = "https://files.pythonhosted.org/packages/af/20/8650798c8e0806a07e2534c562acf1e3d735ac7aaee0abab370f80c56977/maxminddb-3.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397a90b6db4563abe6d5fb08e112afd4481b8354fa64d289625c2e941b787860", size = 97092, upload-time = "2025-10-15T20:49:34.529Z" }, - { url = "https://files.pythonhosted.org/packages/97/c8/0b89ba3651519c021cf2258a5d0c1af306245f2f5e3e05618f28200270d2/maxminddb-3.0.0-cp314-cp314-win32.whl", hash = "sha256:38ba5ce9efb19f1bf5915f9dca8495f5eb63677cf7760ba7038850d3dcde6572", size = 36144, upload-time = "2025-10-15T20:49:35.702Z" }, - { url = "https://files.pythonhosted.org/packages/9e/9f/06b232bb67f13580ae36aa15ddac85d4b38203363801cdd72d70aaba1b56/maxminddb-3.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:9c8c62de3b10d1940abd0c18ed57879b6618e26b78a17ab4a576ac30f96a0e83", size = 38069, upload-time = "2025-10-15T20:49:37.141Z" }, - { url = "https://files.pythonhosted.org/packages/70/dd/a76d5b755bdbe24dd1e03f2ce1951d8d9e70fca80abc7498abbec4441f71/maxminddb-3.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:f0bfd8326a012cb2c8a282531ce900e6535dfc3e50d99c04e64453f782201bd0", size = 34804, upload-time = "2025-10-15T20:49:38.44Z" }, - { url = "https://files.pythonhosted.org/packages/f3/02/512f13edf16e8bf3e01cff958d58bcf023c9ab3ba3d5cda92e011a57f34e/maxminddb-3.0.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6a4330999ab1987f82d32ad969fddcace596dbf8a5c075104e88568f0c326f94", size = 58264, upload-time = "2025-10-15T20:49:39.891Z" }, - { url = "https://files.pythonhosted.org/packages/10/3a/0e8551e0a254489769ab5336bddf5898bead7f6dad17645f85473922a01d/maxminddb-3.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d22b9706c96872b5ee08a1085af9af986832e52c9cc399aa445f4d3d52f2475", size = 38622, upload-time = "2025-10-15T20:49:41.88Z" }, - { url = "https://files.pythonhosted.org/packages/46/85/1ee105d2870c62df68aa2c6c2b886910de8936d9a67d261e55b0dfc9be53/maxminddb-3.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:758f1b656c8bf7b5308d23bbcfe61918f1dd57394331e4300402fd2814e748f4", size = 38028, upload-time = "2025-10-15T20:49:43.326Z" }, - { url = "https://files.pythonhosted.org/packages/2c/5f/b01340be810e8a846db21e839a5d80305628765803fabb45aab31d4c96f6/maxminddb-3.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1efe051b84bd90c86571ae7e02479d09dcf6f84925702c2abe870f8cec4a443c", size = 117895, upload-time = "2025-10-15T20:49:45.209Z" }, - { url = "https://files.pythonhosted.org/packages/3e/34/06c029169335d3557904749f15e4a03361471869655693c8b83d4b64dd29/maxminddb-3.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a87b1d0409aea9903a5f9f2700abb798c46a115941f28ac23b7905fc3ce3967", size = 114609, upload-time = "2025-10-15T20:49:46.59Z" }, - { url = "https://files.pythonhosted.org/packages/18/e1/3dc1c8742e552be3f074943a6fc2e27a6cdaef559613f03b5158833994a4/maxminddb-3.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:029fce189a447d0c0a5729685ae1af3793de364f7301391cde5604282901c52f", size = 113977, upload-time = "2025-10-15T20:49:47.817Z" }, - { url = "https://files.pythonhosted.org/packages/96/f3/566927372c444dc35f54d17a3a608939586c9b3e5ce9d1282a27ed0e1dde/maxminddb-3.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0652a3858c6c6f70e94df1eb3e4755087b8278c4280c473fb533280ff2a4d281", size = 111818, upload-time = "2025-10-15T20:49:49.217Z" }, - { url = "https://files.pythonhosted.org/packages/c9/b6/6f97bbb6535cbf0e57cbfff3cde75d3b419d0e192b38d6f33050d6f97ec3/maxminddb-3.0.0-cp314-cp314t-win32.whl", hash = "sha256:b240375d51a91f98d050f3f4f1ed164c0c7e4fb7c55ef7767242ef3d56147853", size = 37395, upload-time = "2025-10-15T20:49:50.401Z" }, - { url = "https://files.pythonhosted.org/packages/96/41/5296294d494cfc111d000d2e85367a89abe5756f86d2669453c6e1a39334/maxminddb-3.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b23e77eaa343dccdf85aad422ac16b65ed2181abdc7678945d07e89187a0b15b", size = 39553, upload-time = "2025-10-15T20:49:51.464Z" }, - { url = "https://files.pythonhosted.org/packages/8c/34/7910eff964987e82ffa78b94a59e943e52cd2e51f40a1ffede0e9e6ecf86/maxminddb-3.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:d2306c0a50eeafc882fcfce0c99b44400ae95b9283187902236248c2cf904d2a", size = 35418, upload-time = "2025-10-15T20:49:52.528Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d9/fe6243736aed22a7fa877e4bce7f0a5d45e07115fc0da2b5c261d2d8dc19/maxminddb-3.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:aca2e80a32749e0484e8f0e885014efe0c43218acb6f44c6ca4cd8daa90c8f98", size = 34992, upload-time = "2025-10-15T20:50:00.902Z" }, - { url = "https://files.pythonhosted.org/packages/70/a6/1719d8e980edd735f2ad10e47fe0345a4598cb20658b53b8649e7adecec3/maxminddb-3.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0ea42502167190cc52e11ec9c26ff642ee276418f557304d1773b888563feb3f", size = 34643, upload-time = "2025-10-15T20:50:01.985Z" }, - { url = "https://files.pythonhosted.org/packages/93/01/fef9105639a0abedf2891b2492399417f4e2a166a1bad4f7aa6d262254b9/maxminddb-3.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71db302b3eafab7d421d482fcaa977bf94d696dde4db6aab9a9ffd3f4ea3c186", size = 39163, upload-time = "2025-10-15T20:50:03.074Z" }, - { url = "https://files.pythonhosted.org/packages/e6/88/dd27d7834a5db56e7ac6dca93fd5c4c5f2abd3be3eab0dae2c07023dcb20/maxminddb-3.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98eb4bd49301a9066c6ed5817867b8289af3dc41f60a59e29ee2460d80e348da", size = 37988, upload-time = "2025-10-15T20:50:04.499Z" }, - { url = "https://files.pythonhosted.org/packages/02/c5/fa52785a18300a0a5dfa36d4d640bbaf6dc7f41db102f86293156101bdd0/maxminddb-3.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2de7f8255157107e3ed03690d9dc0c278fd5da108b2f36b8b161e21c67175940", size = 37219, upload-time = "2025-10-15T20:50:05.667Z" }, -] - -[[package]] -name = "msgspec" -version = "0.20.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/9c/bfbd12955a49180cbd234c5d29ec6f74fe641698f0cd9df154a854fc8a15/msgspec-0.20.0.tar.gz", hash = "sha256:692349e588fde322875f8d3025ac01689fead5901e7fb18d6870a44519d62a29", size = 317862, upload-time = "2025-11-24T03:56:28.934Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/59/fdcb3af72f750a8de2bcf39d62ada70b5eb17b06d7f63860e0a679cb656b/msgspec-0.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:09e0efbf1ac641fedb1d5496c59507c2f0dc62a052189ee62c763e0aae217520", size = 193345, upload-time = "2025-11-24T03:55:20.613Z" }, - { url = "https://files.pythonhosted.org/packages/5a/15/3c225610da9f02505d37d69a77f4a2e7daae2a125f99d638df211ba84e59/msgspec-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23ee3787142e48f5ee746b2909ce1b76e2949fbe0f97f9f6e70879f06c218b54", size = 186867, upload-time = "2025-11-24T03:55:22.4Z" }, - { url = "https://files.pythonhosted.org/packages/81/36/13ab0c547e283bf172f45491edfdea0e2cecb26ae61e3a7b1ae6058b326d/msgspec-0.20.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:81f4ac6f0363407ac0465eff5c7d4d18f26870e00674f8fcb336d898a1e36854", size = 215351, upload-time = "2025-11-24T03:55:23.958Z" }, - { url = "https://files.pythonhosted.org/packages/6b/96/5c095b940de3aa6b43a71ec76275ac3537b21bd45c7499b5a17a429110fa/msgspec-0.20.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb4d873f24ae18cd1334f4e37a178ed46c9d186437733351267e0a269bdf7e53", size = 219896, upload-time = "2025-11-24T03:55:25.356Z" }, - { url = "https://files.pythonhosted.org/packages/98/7a/81a7b5f01af300761087b114dafa20fb97aed7184d33aab64d48874eb187/msgspec-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b92b8334427b8393b520c24ff53b70f326f79acf5f74adb94fd361bcff8a1d4e", size = 220389, upload-time = "2025-11-24T03:55:26.99Z" }, - { url = "https://files.pythonhosted.org/packages/70/c0/3d0cce27db9a9912421273d49eab79ce01ecd2fed1a2f1b74af9b445f33c/msgspec-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:562c44b047c05cc0384e006fae7a5e715740215c799429e0d7e3e5adf324285a", size = 223348, upload-time = "2025-11-24T03:55:28.311Z" }, - { url = "https://files.pythonhosted.org/packages/89/5e/406b7d578926b68790e390d83a1165a9bfc2d95612a1a9c1c4d5c72ea815/msgspec-0.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:d1dcc93a3ce3d3195985bfff18a48274d0b5ffbc96fa1c5b89da6f0d9af81b29", size = 188713, upload-time = "2025-11-24T03:55:29.553Z" }, - { url = "https://files.pythonhosted.org/packages/47/87/14fe2316624ceedf76a9e94d714d194cbcb699720b210ff189f89ca4efd7/msgspec-0.20.0-cp311-cp311-win_arm64.whl", hash = "sha256:aa387aa330d2e4bd69995f66ea8fdc87099ddeedf6fdb232993c6a67711e7520", size = 174229, upload-time = "2025-11-24T03:55:31.107Z" }, - { url = "https://files.pythonhosted.org/packages/d9/6f/1e25eee957e58e3afb2a44b94fa95e06cebc4c236193ed0de3012fff1e19/msgspec-0.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2aba22e2e302e9231e85edc24f27ba1f524d43c223ef5765bd8624c7df9ec0a5", size = 196391, upload-time = "2025-11-24T03:55:32.677Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ee/af51d090ada641d4b264992a486435ba3ef5b5634bc27e6eb002f71cef7d/msgspec-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:716284f898ab2547fedd72a93bb940375de9fbfe77538f05779632dc34afdfde", size = 188644, upload-time = "2025-11-24T03:55:33.934Z" }, - { url = "https://files.pythonhosted.org/packages/49/d6/9709ee093b7742362c2934bfb1bbe791a1e09bed3ea5d8a18ce552fbfd73/msgspec-0.20.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:558ed73315efa51b1538fa8f1d3b22c8c5ff6d9a2a62eff87d25829b94fc5054", size = 218852, upload-time = "2025-11-24T03:55:35.575Z" }, - { url = "https://files.pythonhosted.org/packages/5c/a2/488517a43ccf5a4b6b6eca6dd4ede0bd82b043d1539dd6bb908a19f8efd3/msgspec-0.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:509ac1362a1d53aa66798c9b9fd76872d7faa30fcf89b2fba3bcbfd559d56eb0", size = 224937, upload-time = "2025-11-24T03:55:36.859Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e8/49b832808aa23b85d4f090d1d2e48a4e3834871415031ed7c5fe48723156/msgspec-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1353c2c93423602e7dea1aa4c92f3391fdfc25ff40e0bacf81d34dbc68adb870", size = 222858, upload-time = "2025-11-24T03:55:38.187Z" }, - { url = "https://files.pythonhosted.org/packages/9f/56/1dc2fa53685dca9c3f243a6cbecd34e856858354e455b77f47ebd76cf5bf/msgspec-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb33b5eb5adb3c33d749684471c6a165468395d7aa02d8867c15103b81e1da3e", size = 227248, upload-time = "2025-11-24T03:55:39.496Z" }, - { url = "https://files.pythonhosted.org/packages/5a/51/aba940212c23b32eedce752896205912c2668472ed5b205fc33da28a6509/msgspec-0.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:fb1d934e435dd3a2b8cf4bbf47a8757100b4a1cfdc2afdf227541199885cdacb", size = 190024, upload-time = "2025-11-24T03:55:40.829Z" }, - { url = "https://files.pythonhosted.org/packages/41/ad/3b9f259d94f183daa9764fef33fdc7010f7ecffc29af977044fa47440a83/msgspec-0.20.0-cp312-cp312-win_arm64.whl", hash = "sha256:00648b1e19cf01b2be45444ba9dc961bd4c056ffb15706651e64e5d6ec6197b7", size = 175390, upload-time = "2025-11-24T03:55:42.05Z" }, - { url = "https://files.pythonhosted.org/packages/8a/d1/b902d38b6e5ba3bdddbec469bba388d647f960aeed7b5b3623a8debe8a76/msgspec-0.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c1ff8db03be7598b50dd4b4a478d6fe93faae3bd54f4f17aa004d0e46c14c46", size = 196463, upload-time = "2025-11-24T03:55:43.405Z" }, - { url = "https://files.pythonhosted.org/packages/57/b6/eff0305961a1d9447ec2b02f8c73c8946f22564d302a504185b730c9a761/msgspec-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f6532369ece217fd37c5ebcfd7e981f2615628c21121b7b2df9d3adcf2fd69b8", size = 188650, upload-time = "2025-11-24T03:55:44.761Z" }, - { url = "https://files.pythonhosted.org/packages/99/93/f2ec1ae1de51d3fdee998a1ede6b2c089453a2ee82b5c1b361ed9095064a/msgspec-0.20.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9a1697da2f85a751ac3cc6a97fceb8e937fc670947183fb2268edaf4016d1ee", size = 218834, upload-time = "2025-11-24T03:55:46.441Z" }, - { url = "https://files.pythonhosted.org/packages/28/83/36557b04cfdc317ed8a525c4993b23e43a8fbcddaddd78619112ca07138c/msgspec-0.20.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fac7e9c92eddcd24c19d9e5f6249760941485dff97802461ae7c995a2450111", size = 224917, upload-time = "2025-11-24T03:55:48.06Z" }, - { url = "https://files.pythonhosted.org/packages/8f/56/362037a1ed5be0b88aced59272442c4b40065c659700f4b195a7f4d0ac88/msgspec-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f953a66f2a3eb8d5ea64768445e2bb301d97609db052628c3e1bcb7d87192a9f", size = 222821, upload-time = "2025-11-24T03:55:49.388Z" }, - { url = "https://files.pythonhosted.org/packages/92/75/fa2370ec341cedf663731ab7042e177b3742645c5dd4f64dc96bd9f18a6b/msgspec-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:247af0313ae64a066d3aea7ba98840f6681ccbf5c90ba9c7d17f3e39dbba679c", size = 227227, upload-time = "2025-11-24T03:55:51.125Z" }, - { url = "https://files.pythonhosted.org/packages/f1/25/5e8080fe0117f799b1b68008dc29a65862077296b92550632de015128579/msgspec-0.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:67d5e4dfad52832017018d30a462604c80561aa62a9d548fc2bd4e430b66a352", size = 189966, upload-time = "2025-11-24T03:55:52.458Z" }, - { url = "https://files.pythonhosted.org/packages/79/b6/63363422153937d40e1cb349c5081338401f8529a5a4e216865decd981bf/msgspec-0.20.0-cp313-cp313-win_arm64.whl", hash = "sha256:91a52578226708b63a9a13de287b1ec3ed1123e4a088b198143860c087770458", size = 175378, upload-time = "2025-11-24T03:55:53.721Z" }, - { url = "https://files.pythonhosted.org/packages/bb/18/62dc13ab0260c7d741dda8dc7f481495b93ac9168cd887dda5929880eef8/msgspec-0.20.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:eead16538db1b3f7ec6e3ed1f6f7c5dec67e90f76e76b610e1ffb5671815633a", size = 196407, upload-time = "2025-11-24T03:55:55.001Z" }, - { url = "https://files.pythonhosted.org/packages/dd/1d/b9949e4ad6953e9f9a142c7997b2f7390c81e03e93570c7c33caf65d27e1/msgspec-0.20.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:703c3bb47bf47801627fb1438f106adbfa2998fe586696d1324586a375fca238", size = 188889, upload-time = "2025-11-24T03:55:56.311Z" }, - { url = "https://files.pythonhosted.org/packages/1e/19/f8bb2dc0f1bfe46cc7d2b6b61c5e9b5a46c62298e8f4d03bbe499c926180/msgspec-0.20.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6cdb227dc585fb109305cee0fd304c2896f02af93ecf50a9c84ee54ee67dbb42", size = 219691, upload-time = "2025-11-24T03:55:57.908Z" }, - { url = "https://files.pythonhosted.org/packages/b8/8e/6b17e43f6eb9369d9858ee32c97959fcd515628a1df376af96c11606cf70/msgspec-0.20.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27d35044dd8818ac1bd0fedb2feb4fbdff4e3508dd7c5d14316a12a2d96a0de0", size = 224918, upload-time = "2025-11-24T03:55:59.322Z" }, - { url = "https://files.pythonhosted.org/packages/1c/db/0e833a177db1a4484797adba7f429d4242585980b90882cc38709e1b62df/msgspec-0.20.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4296393a29ee42dd25947981c65506fd4ad39beaf816f614146fa0c5a6c91ae", size = 223436, upload-time = "2025-11-24T03:56:00.716Z" }, - { url = "https://files.pythonhosted.org/packages/c3/30/d2ee787f4c918fd2b123441d49a7707ae9015e0e8e1ab51aa7967a97b90e/msgspec-0.20.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:205fbdadd0d8d861d71c8f3399fe1a82a2caf4467bc8ff9a626df34c12176980", size = 227190, upload-time = "2025-11-24T03:56:02.371Z" }, - { url = "https://files.pythonhosted.org/packages/ff/37/9c4b58ff11d890d788e700b827db2366f4d11b3313bf136780da7017278b/msgspec-0.20.0-cp314-cp314-win_amd64.whl", hash = "sha256:7dfebc94fe7d3feec6bc6c9df4f7e9eccc1160bb5b811fbf3e3a56899e398a6b", size = 193950, upload-time = "2025-11-24T03:56:03.668Z" }, - { url = "https://files.pythonhosted.org/packages/e9/4e/cab707bf2fa57408e2934e5197fc3560079db34a1e3cd2675ff2e47e07de/msgspec-0.20.0-cp314-cp314-win_arm64.whl", hash = "sha256:2ad6ae36e4a602b24b4bf4eaf8ab5a441fec03e1f1b5931beca8ebda68f53fc0", size = 179018, upload-time = "2025-11-24T03:56:05.038Z" }, - { url = "https://files.pythonhosted.org/packages/4c/06/3da3fc9aaa55618a8f43eb9052453cfe01f82930bca3af8cea63a89f3a11/msgspec-0.20.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f84703e0e6ef025663dd1de828ca028774797b8155e070e795c548f76dde65d5", size = 200389, upload-time = "2025-11-24T03:56:06.375Z" }, - { url = "https://files.pythonhosted.org/packages/83/3b/cc4270a5ceab40dfe1d1745856951b0a24fd16ac8539a66ed3004a60c91e/msgspec-0.20.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7c83fc24dd09cf1275934ff300e3951b3adc5573f0657a643515cc16c7dee131", size = 193198, upload-time = "2025-11-24T03:56:07.742Z" }, - { url = "https://files.pythonhosted.org/packages/cd/ae/4c7905ac53830c8e3c06fdd60e3cdcfedc0bbc993872d1549b84ea21a1bd/msgspec-0.20.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f13ccb1c335a124e80c4562573b9b90f01ea9521a1a87f7576c2e281d547f56", size = 225973, upload-time = "2025-11-24T03:56:09.18Z" }, - { url = "https://files.pythonhosted.org/packages/d9/da/032abac1de4d0678d99eaeadb1323bd9d247f4711c012404ba77ed6f15ca/msgspec-0.20.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17c2b5ca19f19306fc83c96d85e606d2cc107e0caeea85066b5389f664e04846", size = 229509, upload-time = "2025-11-24T03:56:10.898Z" }, - { url = "https://files.pythonhosted.org/packages/69/52/fdc7bdb7057a166f309e0b44929e584319e625aaba4771b60912a9321ccd/msgspec-0.20.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d931709355edabf66c2dd1a756b2d658593e79882bc81aae5964969d5a291b63", size = 230434, upload-time = "2025-11-24T03:56:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/cb/fe/1dfd5f512b26b53043884e4f34710c73e294e7cc54278c3fe28380e42c37/msgspec-0.20.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:565f915d2e540e8a0c93a01ff67f50aebe1f7e22798c6a25873f9fda8d1325f8", size = 231758, upload-time = "2025-11-24T03:56:13.765Z" }, - { url = "https://files.pythonhosted.org/packages/97/f6/9ba7121b8e0c4e0beee49575d1dbc804e2e72467692f0428cf39ceba1ea5/msgspec-0.20.0-cp314-cp314t-win_amd64.whl", hash = "sha256:726f3e6c3c323f283f6021ebb6c8ccf58d7cd7baa67b93d73bfbe9a15c34ab8d", size = 206540, upload-time = "2025-11-24T03:56:15.029Z" }, - { url = "https://files.pythonhosted.org/packages/c8/3e/c5187de84bb2c2ca334ab163fcacf19a23ebb1d876c837f81a1b324a15bf/msgspec-0.20.0-cp314-cp314t-win_arm64.whl", hash = "sha256:93f23528edc51d9f686808a361728e903d6f2be55c901d6f5c92e44c6d546bfc", size = 183011, upload-time = "2025-11-24T03:56:16.442Z" }, -] - -[[package]] -name = "multidict" -version = "6.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604, upload-time = "2025-10-06T14:48:54.277Z" }, - { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715, upload-time = "2025-10-06T14:48:55.445Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332, upload-time = "2025-10-06T14:48:56.706Z" }, - { url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212, upload-time = "2025-10-06T14:48:58.042Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671, upload-time = "2025-10-06T14:49:00.004Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491, upload-time = "2025-10-06T14:49:01.393Z" }, - { url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322, upload-time = "2025-10-06T14:49:02.745Z" }, - { url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694, upload-time = "2025-10-06T14:49:04.15Z" }, - { url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715, upload-time = "2025-10-06T14:49:05.967Z" }, - { url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189, upload-time = "2025-10-06T14:49:07.37Z" }, - { url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845, upload-time = "2025-10-06T14:49:08.759Z" }, - { url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374, upload-time = "2025-10-06T14:49:10.574Z" }, - { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345, upload-time = "2025-10-06T14:49:12.331Z" }, - { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940, upload-time = "2025-10-06T14:49:13.821Z" }, - { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229, upload-time = "2025-10-06T14:49:15.603Z" }, - { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308, upload-time = "2025-10-06T14:49:16.871Z" }, - { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037, upload-time = "2025-10-06T14:49:18.457Z" }, - { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023, upload-time = "2025-10-06T14:49:19.648Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" }, - { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" }, - { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" }, - { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" }, - { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" }, - { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" }, - { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" }, - { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" }, - { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" }, - { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" }, - { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" }, - { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" }, - { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" }, - { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" }, - { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" }, - { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" }, - { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" }, - { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" }, - { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" }, - { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" }, - { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" }, - { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" }, - { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" }, - { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" }, - { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" }, - { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" }, - { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" }, - { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" }, - { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" }, - { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" }, - { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" }, - { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" }, - { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" }, - { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" }, - { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" }, - { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" }, - { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" }, - { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" }, - { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" }, - { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" }, - { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" }, - { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" }, - { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" }, - { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" }, - { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" }, - { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" }, - { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, - { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, - { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" }, - { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" }, - { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" }, - { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084, upload-time = "2025-10-06T14:50:56.369Z" }, - { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667, upload-time = "2025-10-06T14:50:57.991Z" }, - { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590, upload-time = "2025-10-06T14:50:59.589Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112, upload-time = "2025-10-06T14:51:01.183Z" }, - { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194, upload-time = "2025-10-06T14:51:02.794Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510, upload-time = "2025-10-06T14:51:04.724Z" }, - { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395, upload-time = "2025-10-06T14:51:06.306Z" }, - { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520, upload-time = "2025-10-06T14:51:08.091Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479, upload-time = "2025-10-06T14:51:10.365Z" }, - { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" }, - { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" }, - { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" }, - { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" }, - { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" }, - { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" }, - { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" }, - { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" }, - { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324, upload-time = "2025-10-06T14:51:25.822Z" }, - { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" }, - { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" }, - { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" }, - { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" }, - { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" }, - { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" }, - { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" }, - { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272, upload-time = "2025-10-06T14:51:45.265Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" }, - { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" }, - { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" }, - { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, -] - [[package]] name = "ndjson" version = "0.3.1" @@ -1084,155 +365,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ad/b7/bc0cdbc2cc3a66fcac82c79912e135a0110b37b790a14c477f18e18d90cd/nodejs_wheel_binaries-24.11.1-py2.py3-none-win_arm64.whl", hash = "sha256:376b9ea1c4bc1207878975dfeb604f7aa5668c260c6154dcd2af9d42f7734116", size = 39026497, upload-time = "2025-11-18T18:21:54.634Z" }, ] -[[package]] -name = "numpy" -version = "2.3.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324, upload-time = "2025-11-16T22:49:22.582Z" }, - { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872, upload-time = "2025-11-16T22:49:25.408Z" }, - { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148, upload-time = "2025-11-16T22:49:27.549Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282, upload-time = "2025-11-16T22:49:30.964Z" }, - { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903, upload-time = "2025-11-16T22:49:34.191Z" }, - { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672, upload-time = "2025-11-16T22:49:37.2Z" }, - { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896, upload-time = "2025-11-16T22:49:39.727Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608, upload-time = "2025-11-16T22:49:42.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442, upload-time = "2025-11-16T22:49:43.99Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555, upload-time = "2025-11-16T22:49:47.092Z" }, - { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, - { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, - { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, - { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, - { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, - { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, - { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, - { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, - { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, - { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" }, - { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" }, - { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" }, - { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" }, - { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" }, - { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" }, - { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" }, - { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" }, - { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" }, - { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" }, - { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" }, - { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" }, - { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" }, - { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" }, - { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" }, - { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" }, - { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" }, - { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" }, - { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" }, - { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" }, - { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" }, - { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" }, - { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" }, - { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" }, - { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" }, - { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" }, - { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" }, - { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" }, - { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" }, - { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" }, - { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" }, - { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" }, - { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" }, - { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" }, - { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" }, - { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" }, - { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" }, - { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" }, - { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" }, - { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" }, - { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, - { url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689, upload-time = "2025-11-16T22:52:23.247Z" }, - { url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053, upload-time = "2025-11-16T22:52:26.367Z" }, - { url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635, upload-time = "2025-11-16T22:52:29.266Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770, upload-time = "2025-11-16T22:52:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768, upload-time = "2025-11-16T22:52:33.593Z" }, - { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263, upload-time = "2025-11-16T22:52:36.369Z" }, - { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" }, -] - -[[package]] -name = "orjson" -version = "3.11.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/b8/333fdb27840f3bf04022d21b654a35f58e15407183aeb16f3b41aa053446/orjson-3.11.5.tar.gz", hash = "sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5", size = 5972347, upload-time = "2025-12-06T15:55:39.458Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/68/6b3659daec3a81aed5ab47700adb1a577c76a5452d35b91c88efee89987f/orjson-3.11.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9c8494625ad60a923af6b2b0bd74107146efe9b55099e20d7740d995f338fcd8", size = 245318, upload-time = "2025-12-06T15:54:02.355Z" }, - { url = "https://files.pythonhosted.org/packages/e9/00/92db122261425f61803ccf0830699ea5567439d966cbc35856fe711bfe6b/orjson-3.11.5-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:7bb2ce0b82bc9fd1168a513ddae7a857994b780b2945a8c51db4ab1c4b751ebc", size = 129491, upload-time = "2025-12-06T15:54:03.877Z" }, - { url = "https://files.pythonhosted.org/packages/94/4f/ffdcb18356518809d944e1e1f77589845c278a1ebbb5a8297dfefcc4b4cb/orjson-3.11.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67394d3becd50b954c4ecd24ac90b5051ee7c903d167459f93e77fc6f5b4c968", size = 132167, upload-time = "2025-12-06T15:54:04.944Z" }, - { url = "https://files.pythonhosted.org/packages/97/c6/0a8caff96f4503f4f7dd44e40e90f4d14acf80d3b7a97cb88747bb712d3e/orjson-3.11.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:298d2451f375e5f17b897794bcc3e7b821c0f32b4788b9bcae47ada24d7f3cf7", size = 130516, upload-time = "2025-12-06T15:54:06.274Z" }, - { url = "https://files.pythonhosted.org/packages/4d/63/43d4dc9bd9954bff7052f700fdb501067f6fb134a003ddcea2a0bb3854ed/orjson-3.11.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa5e4244063db8e1d87e0f54c3f7522f14b2dc937e65d5241ef0076a096409fd", size = 135695, upload-time = "2025-12-06T15:54:07.702Z" }, - { url = "https://files.pythonhosted.org/packages/87/6f/27e2e76d110919cb7fcb72b26166ee676480a701bcf8fc53ac5d0edce32f/orjson-3.11.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1db2088b490761976c1b2e956d5d4e6409f3732e9d79cfa69f876c5248d1baf9", size = 139664, upload-time = "2025-12-06T15:54:08.828Z" }, - { url = "https://files.pythonhosted.org/packages/d4/f8/5966153a5f1be49b5fbb8ca619a529fde7bc71aa0a376f2bb83fed248bcd/orjson-3.11.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2ed66358f32c24e10ceea518e16eb3549e34f33a9d51f99ce23b0251776a1ef", size = 137289, upload-time = "2025-12-06T15:54:09.898Z" }, - { url = "https://files.pythonhosted.org/packages/a7/34/8acb12ff0299385c8bbcbb19fbe40030f23f15a6de57a9c587ebf71483fb/orjson-3.11.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2021afda46c1ed64d74b555065dbd4c2558d510d8cec5ea6a53001b3e5e82a9", size = 138784, upload-time = "2025-12-06T15:54:11.022Z" }, - { url = "https://files.pythonhosted.org/packages/ee/27/910421ea6e34a527f73d8f4ee7bdffa48357ff79c7b8d6eb6f7b82dd1176/orjson-3.11.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b42ffbed9128e547a1647a3e50bc88ab28ae9daa61713962e0d3dd35e820c125", size = 141322, upload-time = "2025-12-06T15:54:12.427Z" }, - { url = "https://files.pythonhosted.org/packages/87/a3/4b703edd1a05555d4bb1753d6ce44e1a05b7a6d7c164d5b332c795c63d70/orjson-3.11.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8d5f16195bb671a5dd3d1dbea758918bada8f6cc27de72bd64adfbd748770814", size = 413612, upload-time = "2025-12-06T15:54:13.858Z" }, - { url = "https://files.pythonhosted.org/packages/1b/36/034177f11d7eeea16d3d2c42a1883b0373978e08bc9dad387f5074c786d8/orjson-3.11.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c0e5d9f7a0227df2927d343a6e3859bebf9208b427c79bd31949abcc2fa32fa5", size = 150993, upload-time = "2025-12-06T15:54:15.189Z" }, - { url = "https://files.pythonhosted.org/packages/44/2f/ea8b24ee046a50a7d141c0227c4496b1180b215e728e3b640684f0ea448d/orjson-3.11.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23d04c4543e78f724c4dfe656b3791b5f98e4c9253e13b2636f1af5d90e4a880", size = 141774, upload-time = "2025-12-06T15:54:16.451Z" }, - { url = "https://files.pythonhosted.org/packages/8a/12/cc440554bf8200eb23348a5744a575a342497b65261cd65ef3b28332510a/orjson-3.11.5-cp311-cp311-win32.whl", hash = "sha256:c404603df4865f8e0afe981aa3c4b62b406e6d06049564d58934860b62b7f91d", size = 135109, upload-time = "2025-12-06T15:54:17.73Z" }, - { url = "https://files.pythonhosted.org/packages/a3/83/e0c5aa06ba73a6760134b169f11fb970caa1525fa4461f94d76e692299d9/orjson-3.11.5-cp311-cp311-win_amd64.whl", hash = "sha256:9645ef655735a74da4990c24ffbd6894828fbfa117bc97c1edd98c282ecb52e1", size = 133193, upload-time = "2025-12-06T15:54:19.426Z" }, - { url = "https://files.pythonhosted.org/packages/cb/35/5b77eaebc60d735e832c5b1a20b155667645d123f09d471db0a78280fb49/orjson-3.11.5-cp311-cp311-win_arm64.whl", hash = "sha256:1cbf2735722623fcdee8e712cbaaab9e372bbcb0c7924ad711b261c2eccf4a5c", size = 126830, upload-time = "2025-12-06T15:54:20.836Z" }, - { url = "https://files.pythonhosted.org/packages/ef/a4/8052a029029b096a78955eadd68ab594ce2197e24ec50e6b6d2ab3f4e33b/orjson-3.11.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:334e5b4bff9ad101237c2d799d9fd45737752929753bf4faf4b207335a416b7d", size = 245347, upload-time = "2025-12-06T15:54:22.061Z" }, - { url = "https://files.pythonhosted.org/packages/64/67/574a7732bd9d9d79ac620c8790b4cfe0717a3d5a6eb2b539e6e8995e24a0/orjson-3.11.5-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:ff770589960a86eae279f5d8aa536196ebda8273a2a07db2a54e82b93bc86626", size = 129435, upload-time = "2025-12-06T15:54:23.615Z" }, - { url = "https://files.pythonhosted.org/packages/52/8d/544e77d7a29d90cf4d9eecd0ae801c688e7f3d1adfa2ebae5e1e94d38ab9/orjson-3.11.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed24250e55efbcb0b35bed7caaec8cedf858ab2f9f2201f17b8938c618c8ca6f", size = 132074, upload-time = "2025-12-06T15:54:24.694Z" }, - { url = "https://files.pythonhosted.org/packages/6e/57/b9f5b5b6fbff9c26f77e785baf56ae8460ef74acdb3eae4931c25b8f5ba9/orjson-3.11.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a66d7769e98a08a12a139049aac2f0ca3adae989817f8c43337455fbc7669b85", size = 130520, upload-time = "2025-12-06T15:54:26.185Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6d/d34970bf9eb33f9ec7c979a262cad86076814859e54eb9a059a52f6dc13d/orjson-3.11.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86cfc555bfd5794d24c6a1903e558b50644e5e68e6471d66502ce5cb5fdef3f9", size = 136209, upload-time = "2025-12-06T15:54:27.264Z" }, - { url = "https://files.pythonhosted.org/packages/e7/39/bc373b63cc0e117a105ea12e57280f83ae52fdee426890d57412432d63b3/orjson-3.11.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a230065027bc2a025e944f9d4714976a81e7ecfa940923283bca7bbc1f10f626", size = 139837, upload-time = "2025-12-06T15:54:28.75Z" }, - { url = "https://files.pythonhosted.org/packages/cb/aa/7c4818c8d7d324da220f4f1af55c343956003aa4d1ce1857bdc1d396ba69/orjson-3.11.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b29d36b60e606df01959c4b982729c8845c69d1963f88686608be9ced96dbfaa", size = 137307, upload-time = "2025-12-06T15:54:29.856Z" }, - { url = "https://files.pythonhosted.org/packages/46/bf/0993b5a056759ba65145effe3a79dd5a939d4a070eaa5da2ee3180fbb13f/orjson-3.11.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c74099c6b230d4261fdc3169d50efc09abf38ace1a42ea2f9994b1d79153d477", size = 139020, upload-time = "2025-12-06T15:54:31.024Z" }, - { url = "https://files.pythonhosted.org/packages/65/e8/83a6c95db3039e504eda60fc388f9faedbb4f6472f5aba7084e06552d9aa/orjson-3.11.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e697d06ad57dd0c7a737771d470eedc18e68dfdefcdd3b7de7f33dfda5b6212e", size = 141099, upload-time = "2025-12-06T15:54:32.196Z" }, - { url = "https://files.pythonhosted.org/packages/b9/b4/24fdc024abfce31c2f6812973b0a693688037ece5dc64b7a60c1ce69e2f2/orjson-3.11.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e08ca8a6c851e95aaecc32bc44a5aa75d0ad26af8cdac7c77e4ed93acf3d5b69", size = 413540, upload-time = "2025-12-06T15:54:33.361Z" }, - { url = "https://files.pythonhosted.org/packages/d9/37/01c0ec95d55ed0c11e4cae3e10427e479bba40c77312b63e1f9665e0737d/orjson-3.11.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e8b5f96c05fce7d0218df3fdfeb962d6b8cfff7e3e20264306b46dd8b217c0f3", size = 151530, upload-time = "2025-12-06T15:54:34.6Z" }, - { url = "https://files.pythonhosted.org/packages/f9/d4/f9ebc57182705bb4bbe63f5bbe14af43722a2533135e1d2fb7affa0c355d/orjson-3.11.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddbfdb5099b3e6ba6d6ea818f61997bb66de14b411357d24c4612cf1ebad08ca", size = 141863, upload-time = "2025-12-06T15:54:35.801Z" }, - { url = "https://files.pythonhosted.org/packages/0d/04/02102b8d19fdcb009d72d622bb5781e8f3fae1646bf3e18c53d1bc8115b5/orjson-3.11.5-cp312-cp312-win32.whl", hash = "sha256:9172578c4eb09dbfcf1657d43198de59b6cef4054de385365060ed50c458ac98", size = 135255, upload-time = "2025-12-06T15:54:37.209Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fb/f05646c43d5450492cb387de5549f6de90a71001682c17882d9f66476af5/orjson-3.11.5-cp312-cp312-win_amd64.whl", hash = "sha256:2b91126e7b470ff2e75746f6f6ee32b9ab67b7a93c8ba1d15d3a0caaf16ec875", size = 133252, upload-time = "2025-12-06T15:54:38.401Z" }, - { url = "https://files.pythonhosted.org/packages/dc/a6/7b8c0b26ba18c793533ac1cd145e131e46fcf43952aa94c109b5b913c1f0/orjson-3.11.5-cp312-cp312-win_arm64.whl", hash = "sha256:acbc5fac7e06777555b0722b8ad5f574739e99ffe99467ed63da98f97f9ca0fe", size = 126777, upload-time = "2025-12-06T15:54:39.515Z" }, - { url = "https://files.pythonhosted.org/packages/10/43/61a77040ce59f1569edf38f0b9faadc90c8cf7e9bec2e0df51d0132c6bb7/orjson-3.11.5-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3b01799262081a4c47c035dd77c1301d40f568f77cc7ec1bb7db5d63b0a01629", size = 245271, upload-time = "2025-12-06T15:54:40.878Z" }, - { url = "https://files.pythonhosted.org/packages/55/f9/0f79be617388227866d50edd2fd320cb8fb94dc1501184bb1620981a0aba/orjson-3.11.5-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:61de247948108484779f57a9f406e4c84d636fa5a59e411e6352484985e8a7c3", size = 129422, upload-time = "2025-12-06T15:54:42.403Z" }, - { url = "https://files.pythonhosted.org/packages/77/42/f1bf1549b432d4a78bfa95735b79b5dac75b65b5bb815bba86ad406ead0a/orjson-3.11.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:894aea2e63d4f24a7f04a1908307c738d0dce992e9249e744b8f4e8dd9197f39", size = 132060, upload-time = "2025-12-06T15:54:43.531Z" }, - { url = "https://files.pythonhosted.org/packages/25/49/825aa6b929f1a6ed244c78acd7b22c1481fd7e5fda047dc8bf4c1a807eb6/orjson-3.11.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ddc21521598dbe369d83d4d40338e23d4101dad21dae0e79fa20465dbace019f", size = 130391, upload-time = "2025-12-06T15:54:45.059Z" }, - { url = "https://files.pythonhosted.org/packages/42/ec/de55391858b49e16e1aa8f0bbbb7e5997b7345d8e984a2dec3746d13065b/orjson-3.11.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cce16ae2f5fb2c53c3eafdd1706cb7b6530a67cc1c17abe8ec747f5cd7c0c51", size = 135964, upload-time = "2025-12-06T15:54:46.576Z" }, - { url = "https://files.pythonhosted.org/packages/1c/40/820bc63121d2d28818556a2d0a09384a9f0262407cf9fa305e091a8048df/orjson-3.11.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e46c762d9f0e1cfb4ccc8515de7f349abbc95b59cb5a2bd68df5973fdef913f8", size = 139817, upload-time = "2025-12-06T15:54:48.084Z" }, - { url = "https://files.pythonhosted.org/packages/09/c7/3a445ca9a84a0d59d26365fd8898ff52bdfcdcb825bcc6519830371d2364/orjson-3.11.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7345c759276b798ccd6d77a87136029e71e66a8bbf2d2755cbdde1d82e78706", size = 137336, upload-time = "2025-12-06T15:54:49.426Z" }, - { url = "https://files.pythonhosted.org/packages/9a/b3/dc0d3771f2e5d1f13368f56b339c6782f955c6a20b50465a91acb79fe961/orjson-3.11.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75bc2e59e6a2ac1dd28901d07115abdebc4563b5b07dd612bf64260a201b1c7f", size = 138993, upload-time = "2025-12-06T15:54:50.939Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a2/65267e959de6abe23444659b6e19c888f242bf7725ff927e2292776f6b89/orjson-3.11.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:54aae9b654554c3b4edd61896b978568c6daa16af96fa4681c9b5babd469f863", size = 141070, upload-time = "2025-12-06T15:54:52.414Z" }, - { url = "https://files.pythonhosted.org/packages/63/c9/da44a321b288727a322c6ab17e1754195708786a04f4f9d2220a5076a649/orjson-3.11.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4bdd8d164a871c4ec773f9de0f6fe8769c2d6727879c37a9666ba4183b7f8228", size = 413505, upload-time = "2025-12-06T15:54:53.67Z" }, - { url = "https://files.pythonhosted.org/packages/7f/17/68dc14fa7000eefb3d4d6d7326a190c99bb65e319f02747ef3ebf2452f12/orjson-3.11.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a261fef929bcf98a60713bf5e95ad067cea16ae345d9a35034e73c3990e927d2", size = 151342, upload-time = "2025-12-06T15:54:55.113Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c5/ccee774b67225bed630a57478529fc026eda33d94fe4c0eac8fe58d4aa52/orjson-3.11.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c028a394c766693c5c9909dec76b24f37e6a1b91999e8d0c0d5feecbe93c3e05", size = 141823, upload-time = "2025-12-06T15:54:56.331Z" }, - { url = "https://files.pythonhosted.org/packages/67/80/5d00e4155d0cd7390ae2087130637671da713959bb558db9bac5e6f6b042/orjson-3.11.5-cp313-cp313-win32.whl", hash = "sha256:2cc79aaad1dfabe1bd2d50ee09814a1253164b3da4c00a78c458d82d04b3bdef", size = 135236, upload-time = "2025-12-06T15:54:57.507Z" }, - { url = "https://files.pythonhosted.org/packages/95/fe/792cc06a84808dbdc20ac6eab6811c53091b42f8e51ecebf14b540e9cfe4/orjson-3.11.5-cp313-cp313-win_amd64.whl", hash = "sha256:ff7877d376add4e16b274e35a3f58b7f37b362abf4aa31863dadacdd20e3a583", size = 133167, upload-time = "2025-12-06T15:54:58.71Z" }, - { url = "https://files.pythonhosted.org/packages/46/2c/d158bd8b50e3b1cfdcf406a7e463f6ffe3f0d167b99634717acdaf5e299f/orjson-3.11.5-cp313-cp313-win_arm64.whl", hash = "sha256:59ac72ea775c88b163ba8d21b0177628bd015c5dd060647bbab6e22da3aad287", size = 126712, upload-time = "2025-12-06T15:54:59.892Z" }, - { url = "https://files.pythonhosted.org/packages/c2/60/77d7b839e317ead7bb225d55bb50f7ea75f47afc489c81199befc5435b50/orjson-3.11.5-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e446a8ea0a4c366ceafc7d97067bfd55292969143b57e3c846d87fc701e797a0", size = 245252, upload-time = "2025-12-06T15:55:01.127Z" }, - { url = "https://files.pythonhosted.org/packages/f1/aa/d4639163b400f8044cef0fb9aa51b0337be0da3a27187a20d1166e742370/orjson-3.11.5-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:53deb5addae9c22bbe3739298f5f2196afa881ea75944e7720681c7080909a81", size = 129419, upload-time = "2025-12-06T15:55:02.723Z" }, - { url = "https://files.pythonhosted.org/packages/30/94/9eabf94f2e11c671111139edf5ec410d2f21e6feee717804f7e8872d883f/orjson-3.11.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd00d49d6063d2b8791da5d4f9d20539c5951f965e45ccf4e96d33505ce68f", size = 132050, upload-time = "2025-12-06T15:55:03.918Z" }, - { url = "https://files.pythonhosted.org/packages/3d/c8/ca10f5c5322f341ea9a9f1097e140be17a88f88d1cfdd29df522970d9744/orjson-3.11.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fd15f9fc8c203aeceff4fda211157fad114dde66e92e24097b3647a08f4ee9e", size = 130370, upload-time = "2025-12-06T15:55:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/25/d4/e96824476d361ee2edd5c6290ceb8d7edf88d81148a6ce172fc00278ca7f/orjson-3.11.5-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df95000fbe6777bf9820ae82ab7578e8662051bb5f83d71a28992f539d2cda7", size = 136012, upload-time = "2025-12-06T15:55:06.402Z" }, - { url = "https://files.pythonhosted.org/packages/85/8e/9bc3423308c425c588903f2d103cfcfe2539e07a25d6522900645a6f257f/orjson-3.11.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a8d676748fca47ade5bc3da7430ed7767afe51b2f8100e3cd65e151c0eaceb", size = 139809, upload-time = "2025-12-06T15:55:07.656Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3c/b404e94e0b02a232b957c54643ce68d0268dacb67ac33ffdee24008c8b27/orjson-3.11.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa0f513be38b40234c77975e68805506cad5d57b3dfd8fe3baa7f4f4051e15b4", size = 137332, upload-time = "2025-12-06T15:55:08.961Z" }, - { url = "https://files.pythonhosted.org/packages/51/30/cc2d69d5ce0ad9b84811cdf4a0cd5362ac27205a921da524ff42f26d65e0/orjson-3.11.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1863e75b92891f553b7922ce4ee10ed06db061e104f2b7815de80cdcb135ad", size = 138983, upload-time = "2025-12-06T15:55:10.595Z" }, - { url = "https://files.pythonhosted.org/packages/0e/87/de3223944a3e297d4707d2fe3b1ffb71437550e165eaf0ca8bbe43ccbcb1/orjson-3.11.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4be86b58e9ea262617b8ca6251a2f0d63cc132a6da4b5fcc8e0a4128782c829", size = 141069, upload-time = "2025-12-06T15:55:11.832Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/81d5087ae74be33bcae3ff2d80f5ccaa4a8fedc6d39bf65a427a95b8977f/orjson-3.11.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b923c1c13fa02084eb38c9c065afd860a5cff58026813319a06949c3af5732ac", size = 413491, upload-time = "2025-12-06T15:55:13.314Z" }, - { url = "https://files.pythonhosted.org/packages/d0/6f/f6058c21e2fc1efaf918986dbc2da5cd38044f1a2d4b7b91ad17c4acf786/orjson-3.11.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1b6bd351202b2cd987f35a13b5e16471cf4d952b42a73c391cc537974c43ef6d", size = 151375, upload-time = "2025-12-06T15:55:14.715Z" }, - { url = "https://files.pythonhosted.org/packages/54/92/c6921f17d45e110892899a7a563a925b2273d929959ce2ad89e2525b885b/orjson-3.11.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb150d529637d541e6af06bbe3d02f5498d628b7f98267ff87647584293ab439", size = 141850, upload-time = "2025-12-06T15:55:15.94Z" }, - { url = "https://files.pythonhosted.org/packages/88/86/cdecb0140a05e1a477b81f24739da93b25070ee01ce7f7242f44a6437594/orjson-3.11.5-cp314-cp314-win32.whl", hash = "sha256:9cc1e55c884921434a84a0c3dd2699eb9f92e7b441d7f53f3941079ec6ce7499", size = 135278, upload-time = "2025-12-06T15:55:17.202Z" }, - { url = "https://files.pythonhosted.org/packages/e4/97/b638d69b1e947d24f6109216997e38922d54dcdcdb1b11c18d7efd2d3c59/orjson-3.11.5-cp314-cp314-win_amd64.whl", hash = "sha256:a4f3cb2d874e03bc7767c8f88adaa1a9a05cecea3712649c3b58589ec7317310", size = 133170, upload-time = "2025-12-06T15:55:18.468Z" }, - { url = "https://files.pythonhosted.org/packages/8f/dd/f4fff4a6fe601b4f8f3ba3aa6da8ac33d17d124491a3b804c662a70e1636/orjson-3.11.5-cp314-cp314-win_arm64.whl", hash = "sha256:38b22f476c351f9a1c43e5b07d8b5a02eb24a6ab8e75f700f7d479d4568346a5", size = 126713, upload-time = "2025-12-06T15:55:19.738Z" }, -] - [[package]] name = "packaging" version = "25.0" @@ -1242,25 +374,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] -[[package]] -name = "patchright" -version = "1.56.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "greenlet" }, - { name = "pyee" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/d6/ac03353f1b3138541356a044c2d7d3e01b73f9555c7281dab3423c5d44da/patchright-1.56.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:9df1e0b2c35a298ad2807044699476d3e872f76117c319077b68d7b8ac8bffae", size = 40595069, upload-time = "2025-11-11T18:59:20.801Z" }, - { url = "https://files.pythonhosted.org/packages/a8/f2/e33cd3e6916ed959f5b9be1182363e19771be81541a77b2c5615a6434cc8/patchright-1.56.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e0d8e86d743f4da99f83e9962f9317bd3aaabe4e9db86d3e12cb1d0f39e2f469", size = 39383869, upload-time = "2025-11-11T18:59:23.829Z" }, - { url = "https://files.pythonhosted.org/packages/5f/49/510b8fb761a7e6a2cbd8381f2254fd9442055e27961c0565944d5634ccb6/patchright-1.56.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:e939a9651ebad6ed5afb84e6cf8b874fd80e4b03a912509dc5159f0fa2d4a752", size = 40595069, upload-time = "2025-11-11T18:59:26.9Z" }, - { url = "https://files.pythonhosted.org/packages/e6/ad/17f72b906a1d3d9750b26964d4481340defbfc02f039c888756061f41a33/patchright-1.56.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:cef0f31334dece118fa8dbe20e0c0ac97ea539a2a3e7dd007a67d05b4c3e0512", size = 46246766, upload-time = "2025-11-11T18:59:29.985Z" }, - { url = "https://files.pythonhosted.org/packages/53/b4/6546fb868f464c888cee8e95d5b0cc4e276afc5e531bc3ca737e2a910f77/patchright-1.56.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02329457d4f9e92b49209adceec33097042b6a6f03de31185a2f99721aeded6a", size = 46094291, upload-time = "2025-11-11T18:59:32.883Z" }, - { url = "https://files.pythonhosted.org/packages/11/4b/a43f86570cb72f732723c19ba71f1c02200ed1cc762d71daa2c35718a529/patchright-1.56.0-py3-none-win32.whl", hash = "sha256:5502899cd8764ccc7a18c3cb47a76e49b25ec4eae155ec5ebea9e5e0022cc811", size = 35623487, upload-time = "2025-11-11T18:59:36.156Z" }, - { url = "https://files.pythonhosted.org/packages/95/8d/37cc3127c6085ffa6482832a07f84587f2f04dbbaf194cc395bb2afa811c/patchright-1.56.0-py3-none-win_amd64.whl", hash = "sha256:0fe85d8f6b541689185115f3ce69e87a7f5dd964ec6aaeac635d4db5a906aec1", size = 35623491, upload-time = "2025-11-11T18:59:38.853Z" }, - { url = "https://files.pythonhosted.org/packages/c5/37/0b79ddfc6a2ba16e9f258670ea40a74c791d925f1847cf4ad0e739bc7506/patchright-1.56.0-py3-none-win_arm64.whl", hash = "sha256:8bbf1c753e7b07b889a3ff43109ce3cea457605ea6b0477e65af4d9a2c5585cf", size = 31232602, upload-time = "2025-11-11T18:59:41.537Z" }, -] - [[package]] name = "platformdirs" version = "4.5.1" @@ -1270,25 +383,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, ] -[[package]] -name = "playwright" -version = "1.56.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "greenlet" }, - { name = "pyee" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/31/a5362cee43f844509f1f10d8a27c9cc0e2f7bdce5353d304d93b2151c1b1/playwright-1.56.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:b33eb89c516cbc6723f2e3523bada4a4eb0984a9c411325c02d7016a5d625e9c", size = 40611424, upload-time = "2025-11-11T18:39:10.175Z" }, - { url = "https://files.pythonhosted.org/packages/ef/95/347eef596d8778fb53590dc326c344d427fa19ba3d42b646fce2a4572eb3/playwright-1.56.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b228b3395212b9472a4ee5f1afe40d376eef9568eb039fcb3e563de8f4f4657b", size = 39400228, upload-time = "2025-11-11T18:39:13.915Z" }, - { url = "https://files.pythonhosted.org/packages/b9/54/6ad97b08b2ca1dfcb4fbde4536c4f45c0d9d8b1857a2d20e7bbfdf43bf15/playwright-1.56.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:0ef7e6fd653267798a8a968ff7aa2dcac14398b7dd7440ef57524e01e0fbbd65", size = 40611424, upload-time = "2025-11-11T18:39:17.093Z" }, - { url = "https://files.pythonhosted.org/packages/e4/76/6d409e37e82cdd5dda3df1ab958130ae32b46e42458bd4fc93d7eb8749cb/playwright-1.56.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:404be089b49d94bc4c1fe0dfb07664bda5ffe87789034a03bffb884489bdfb5c", size = 46263122, upload-time = "2025-11-11T18:39:20.619Z" }, - { url = "https://files.pythonhosted.org/packages/4f/84/fb292cc5d45f3252e255ea39066cd1d2385c61c6c1596548dfbf59c88605/playwright-1.56.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64cda7cf4e51c0d35dab55190841bfcdfb5871685ec22cb722cd0ad2df183e34", size = 46110645, upload-time = "2025-11-11T18:39:24.005Z" }, - { url = "https://files.pythonhosted.org/packages/61/bd/8c02c3388ae14edc374ac9f22cbe4e14826c6a51b2d8eaf86e89fabee264/playwright-1.56.0-py3-none-win32.whl", hash = "sha256:d87b79bcb082092d916a332c27ec9732e0418c319755d235d93cc6be13bdd721", size = 35639837, upload-time = "2025-11-11T18:39:27.174Z" }, - { url = "https://files.pythonhosted.org/packages/64/27/f13b538fbc6b7a00152f4379054a49f6abc0bf55ac86f677ae54bc49fb82/playwright-1.56.0-py3-none-win_amd64.whl", hash = "sha256:3c7fc49bb9e673489bf2622855f9486d41c5101bbed964638552b864c4591f94", size = 35639843, upload-time = "2025-11-11T18:39:30.851Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c7/3ee8b556107995846576b4fe42a08ed49b8677619421f2afacf6ee421138/playwright-1.56.0-py3-none-win_arm64.whl", hash = "sha256:2745490ae8dd58d27e5ea4d9aa28402e8e2991eb84fb4b2fd5fbde2106716f6f", size = 31248959, upload-time = "2025-11-11T18:39:33.998Z" }, -] - [[package]] name = "pluggy" version = "1.6.0" @@ -1314,105 +408,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429, upload-time = "2025-11-22T21:02:40.836Z" }, ] -[[package]] -name = "propcache" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, - { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, - { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, - { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, - { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, - { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, - { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, - { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, - { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, - { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, - { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, - { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, - { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, - { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, - { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, - { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, - { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, - { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, - { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, - { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, - { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, - { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, - { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, - { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, - { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, - { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, - { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, - { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, - { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, - { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, - { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, - { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, - { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, - { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, - { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, - { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, - { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, - { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, - { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, - { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, - { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, - { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, - { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, - { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, - { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, - { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, - { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, - { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, - { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, - { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, - { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, - { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, - { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, - { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, - { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, - { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, - { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, - { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, - { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, - { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, - { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, - { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, - { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, -] - [[package]] name = "pycparser" version = "2.23" @@ -1534,18 +529,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] -[[package]] -name = "pyee" -version = "13.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/95/03/1fd98d5841cd7964a27d729ccf2199602fe05eb7a405c1462eb7277945ed/pyee-13.0.0.tar.gz", hash = "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37", size = 31250, upload-time = "2025-03-17T18:53:15.955Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/4d/b9add7c84060d4c1906abe9a7e5359f2a60f7a9a4f67268b2766673427d8/pyee-13.0.0-py3-none-any.whl", hash = "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498", size = 15730, upload-time = "2025-03-17T18:53:14.532Z" }, -] - [[package]] name = "pygments" version = "2.19.2" @@ -1555,46 +538,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] -[[package]] -name = "pyobjc-core" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/df/d2b290708e9da86d6e7a9a2a2022b91915cf2e712a5a82e306cb6ee99792/pyobjc_core-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c918ebca280925e7fcb14c5c43ce12dcb9574a33cccb889be7c8c17f3bcce8b6", size = 671263, upload-time = "2025-11-14T09:31:35.231Z" }, - { url = "https://files.pythonhosted.org/packages/64/5a/6b15e499de73050f4a2c88fff664ae154307d25dc04da8fb38998a428358/pyobjc_core-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:818bcc6723561f207e5b5453efe9703f34bc8781d11ce9b8be286bb415eb4962", size = 678335, upload-time = "2025-11-14T09:32:20.107Z" }, - { url = "https://files.pythonhosted.org/packages/f4/d2/29e5e536adc07bc3d33dd09f3f7cf844bf7b4981820dc2a91dd810f3c782/pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:01c0cf500596f03e21c23aef9b5f326b9fb1f8f118cf0d8b66749b6cf4cbb37a", size = 677370, upload-time = "2025-11-14T09:33:05.273Z" }, - { url = "https://files.pythonhosted.org/packages/1b/f0/4b4ed8924cd04e425f2a07269943018d43949afad1c348c3ed4d9d032787/pyobjc_core-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:177aaca84bb369a483e4961186704f64b2697708046745f8167e818d968c88fc", size = 719586, upload-time = "2025-11-14T09:33:53.302Z" }, - { url = "https://files.pythonhosted.org/packages/25/98/9f4ed07162de69603144ff480be35cd021808faa7f730d082b92f7ebf2b5/pyobjc_core-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:844515f5d86395b979d02152576e7dee9cc679acc0b32dc626ef5bda315eaa43", size = 670164, upload-time = "2025-11-14T09:34:37.458Z" }, - { url = "https://files.pythonhosted.org/packages/62/50/dc076965c96c7f0de25c0a32b7f8aa98133ed244deaeeacfc758783f1f30/pyobjc_core-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:453b191df1a4b80e756445b935491b974714456ae2cbae816840bd96f86db882", size = 712204, upload-time = "2025-11-14T09:35:24.148Z" }, -] - -[[package]] -name = "pyobjc-framework-cocoa" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/07/5760735c0fffc65107e648eaf7e0991f46da442ac4493501be5380e6d9d4/pyobjc_framework_cocoa-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f52228bcf38da64b77328787967d464e28b981492b33a7675585141e1b0a01e6", size = 383812, upload-time = "2025-11-14T09:40:53.169Z" }, - { url = "https://files.pythonhosted.org/packages/95/bf/ee4f27ec3920d5c6fc63c63e797c5b2cc4e20fe439217085d01ea5b63856/pyobjc_framework_cocoa-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:547c182837214b7ec4796dac5aee3aa25abc665757b75d7f44f83c994bcb0858", size = 384590, upload-time = "2025-11-14T09:41:17.336Z" }, - { url = "https://files.pythonhosted.org/packages/ad/31/0c2e734165abb46215797bd830c4bdcb780b699854b15f2b6240515edcc6/pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a3dcd491cacc2f5a197142b3c556d8aafa3963011110102a093349017705118", size = 384689, upload-time = "2025-11-14T09:41:41.478Z" }, - { url = "https://files.pythonhosted.org/packages/23/3b/b9f61be7b9f9b4e0a6db18b3c35c4c4d589f2d04e963e2174d38c6555a92/pyobjc_framework_cocoa-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:914b74328c22d8ca261d78c23ef2befc29776e0b85555973927b338c5734ca44", size = 388843, upload-time = "2025-11-14T09:42:05.719Z" }, - { url = "https://files.pythonhosted.org/packages/59/bb/f777cc9e775fc7dae77b569254570fe46eb842516b3e4fe383ab49eab598/pyobjc_framework_cocoa-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:03342a60fc0015bcdf9b93ac0b4f457d3938e9ef761b28df9564c91a14f0129a", size = 384932, upload-time = "2025-11-14T09:42:29.771Z" }, - { url = "https://files.pythonhosted.org/packages/58/27/b457b7b37089cad692c8aada90119162dfb4c4a16f513b79a8b2b022b33b/pyobjc_framework_cocoa-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6ba1dc1bfa4da42d04e93d2363491275fb2e2be5c20790e561c8a9e09b8cf2cc", size = 388970, upload-time = "2025-11-14T09:42:53.964Z" }, -] - -[[package]] -name = "pysocks" -version = "1.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" }, -] - [[package]] name = "pytest" version = "9.0.2" @@ -1693,18 +636,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] -[[package]] -name = "requests-file" -version = "3.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3c/f8/5dc70102e4d337063452c82e1f0d95e39abfe67aa222ed8a5ddeb9df8de8/requests_file-3.0.1.tar.gz", hash = "sha256:f14243d7796c588f3521bd423c5dea2ee4cc730e54a3cac9574d78aca1272576", size = 6967, upload-time = "2025-10-20T18:56:42.279Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/d5/de8f089119205a09da657ed4784c584ede8381a0ce6821212a6d4ca47054/requests_file-3.0.1-py2.py3-none-any.whl", hash = "sha256:d0f5eb94353986d998f80ac63c7f146a307728be051d4d1cd390dbdb59c10fa2", size = 4514, upload-time = "2025-10-20T18:56:41.184Z" }, -] - [[package]] name = "ruff" version = "0.14.8" @@ -1743,8 +674,6 @@ dependencies = [ { name = "ndjson" }, { name = "pydantic" }, { name = "requests" }, - { name = "scrapling", extra = ["fetchers"] }, - { name = "types-requests" }, ] [package.dev-dependencies] @@ -1768,8 +697,6 @@ requires-dist = [ { name = "ndjson", specifier = ">=0.3.1" }, { name = "pydantic", specifier = ">=2.11.10" }, { name = "requests", specifier = ">=2.32.5" }, - { name = "scrapling", extras = ["fetchers"], specifier = ">=0.3.5" }, - { name = "types-requests", specifier = ">=2.32.4.20250913" }, ] [package.metadata.requires-dev] @@ -1784,45 +711,6 @@ dev = [ { name = "types-requests", specifier = ">=2.32.4.20250913" }, ] -[[package]] -name = "scrapling" -version = "0.3.11" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cssselect" }, - { name = "lxml" }, - { name = "orjson" }, - { name = "tldextract" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c4/ad/dfe0efce82fcba28a7bec36d26ab3eac99d7440eb85b01ab98c5bfc7c1d7/scrapling-0.3.11.tar.gz", hash = "sha256:e3c16b03e02ec302bc451eab943146968cfc8622c317a2edf3ba839dd5ac89c9", size = 98939, upload-time = "2025-12-03T01:53:25.549Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/7f/085b53835f0516360fc0f9f26d829daa4d2e2c55375fa691e226025e5464/scrapling-0.3.11-py3-none-any.whl", hash = "sha256:0cf4b64e3a804e3ea7f21ec328de33175dbdc485fee48840fdbac634862b093b", size = 106953, upload-time = "2025-12-03T01:53:24.292Z" }, -] - -[package.optional-dependencies] -fetchers = [ - { name = "camoufox" }, - { name = "click" }, - { name = "curl-cffi" }, - { name = "geoip2" }, - { name = "msgspec" }, - { name = "patchright" }, - { name = "playwright" }, -] - -[[package]] -name = "screeninfo" -version = "0.8.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cython", marker = "sys_platform == 'darwin'" }, - { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ec/bb/e69e5e628d43f118e0af4fc063c20058faa8635c95a1296764acc8167e27/screeninfo-0.8.1.tar.gz", hash = "sha256:9983076bcc7e34402a1a9e4d7dabf3729411fd2abb3f3b4be7eba73519cd2ed1", size = 10666, upload-time = "2022-09-09T11:35:23.419Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/bf/c5205d480307bef660e56544b9e3d7ff687da776abb30c9cb3f330887570/screeninfo-0.8.1-py3-none-any.whl", hash = "sha256:e97d6b173856edcfa3bd282f81deb528188aff14b11ec3e195584e7641be733c", size = 12907, upload-time = "2022-09-09T11:35:21.351Z" }, -] - [[package]] name = "soupsieve" version = "2.8" @@ -1832,33 +720,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, ] -[[package]] -name = "tldextract" -version = "5.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "filelock" }, - { name = "idna" }, - { name = "requests" }, - { name = "requests-file" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/97/78/182641ea38e3cfd56e9c7b3c0d48a53d432eea755003aa544af96403d4ac/tldextract-5.3.0.tar.gz", hash = "sha256:b3d2b70a1594a0ecfa6967d57251527d58e00bb5a91a74387baa0d87a0678609", size = 128502, upload-time = "2025-04-22T06:19:37.491Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/67/7c/ea488ef48f2f544566947ced88541bc45fae9e0e422b2edbf165ee07da99/tldextract-5.3.0-py3-none-any.whl", hash = "sha256:f70f31d10b55c83993f55e91ecb7c5d84532a8972f22ec578ecfbe5ea2292db2", size = 107384, upload-time = "2025-04-22T06:19:36.304Z" }, -] - -[[package]] -name = "tqdm" -version = "4.67.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, -] - [[package]] name = "ty" version = "0.0.1a32" @@ -1950,26 +811,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] -[[package]] -name = "ua-parser" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ua-parser-builtins" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/70/0e/ed98be735bc89d5040e0c60f5620d0b8c04e9e7da99ed1459e8050e90a77/ua_parser-1.0.1.tar.gz", hash = "sha256:f9d92bf19d4329019cef91707aecc23c6d65143ad7e29a233f0580fb0d15547d", size = 728106, upload-time = "2025-02-01T14:13:32.508Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/37/be6dfbfa45719aa82c008fb4772cfe5c46db765a2ca4b6f524a1fdfee4d7/ua_parser-1.0.1-py3-none-any.whl", hash = "sha256:b059f2cb0935addea7e551251cbbf42e9a8872f86134163bc1a4f79e0945ffea", size = 31410, upload-time = "2025-02-01T14:13:28.458Z" }, -] - -[[package]] -name = "ua-parser-builtins" -version = "0.18.0.post1" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/d3/13adff37f15489c784cc7669c35a6c3bf94b87540229eedf52ef2a1d0175/ua_parser_builtins-0.18.0.post1-py3-none-any.whl", hash = "sha256:eb4f93504040c3a990a6b0742a2afd540d87d7f9f05fd66e94c101db1564674d", size = 86077, upload-time = "2024-12-05T18:44:36.732Z" }, -] - [[package]] name = "urllib3" version = "2.6.0" @@ -1992,113 +833,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846 wheels = [ { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, ] - -[[package]] -name = "yarl" -version = "1.22.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "multidict" }, - { name = "propcache" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, - { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, - { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, - { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, - { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, - { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, - { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, - { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, - { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, - { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, - { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, - { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, - { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, - { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, - { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, - { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, - { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, - { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, - { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, - { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, - { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, - { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, - { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, - { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, - { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, - { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, - { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, - { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, - { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, - { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, - { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, - { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, - { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, - { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, - { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, - { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, - { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, - { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, - { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, - { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, - { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, - { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, - { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, - { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, - { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, - { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, - { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, - { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, - { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, - { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, - { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, - { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, - { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, - { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, - { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, - { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, - { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, - { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, - { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, - { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, - { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, - { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, - { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, - { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, - { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, - { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, - { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, - { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, - { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, - { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, - { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, - { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, - { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, - { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, - { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, - { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, - { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, - { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, - { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, - { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, - { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, - { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, - { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, - { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, -] From 2148d9bd0700464aef997cc46470ad5492f00651 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Wed, 18 Feb 2026 13:49:42 -0500 Subject: [PATCH 12/86] feat(nix): add health --- lua/cp/health.lua | 12 +++++++++--- lua/cp/utils.lua | 7 +++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lua/cp/health.lua b/lua/cp/health.lua index 6a3913f..d3a3fea 100644 --- a/lua/cp/health.lua +++ b/lua/cp/health.lua @@ -19,11 +19,13 @@ local function check() end if utils.is_nix_build() then - vim.health.ok('Nix-built Python environment detected') + local source = utils.is_nix_discovered() and 'runtime discovery' or 'flake install' + vim.health.ok('Nix Python environment detected (' .. source .. ')') local py = utils.get_nix_python() + vim.health.info('Python: ' .. py) local r = vim.system({ py, '--version' }, { text = true }):wait() if r.code == 0 then - vim.health.info('Python: ' .. r.stdout:gsub('\n', '')) + vim.health.info('Python version: ' .. r.stdout:gsub('\n', '')) end else if vim.fn.executable('uv') == 1 then @@ -36,6 +38,10 @@ local function check() vim.health.warn('uv not found (install https://docs.astral.sh/uv/ for scraping)') end + if vim.fn.executable('nix') == 1 then + vim.health.info('nix available but Python environment not resolved via nix') + end + local plugin_path = utils.get_plugin_path() local venv_dir = plugin_path .. '/.venv' if vim.fn.isdirectory(venv_dir) == 1 then @@ -52,7 +58,7 @@ local function check() vim.health.error('GNU time not found: ' .. (time_cap.reason or '')) end - local timeout_cap = utils.time_capability() + local timeout_cap = utils.timeout_capability() if timeout_cap.ok then vim.health.ok('GNU timeout found: ' .. timeout_cap.path) else diff --git a/lua/cp/utils.lua b/lua/cp/utils.lua index 0ac4916..0031216 100644 --- a/lua/cp/utils.lua +++ b/lua/cp/utils.lua @@ -3,6 +3,7 @@ local M = {} local logger = require('cp.log') local _nix_python = nil +local _nix_discovered = false local uname = vim.loop.os_uname() @@ -89,6 +90,10 @@ function M.get_nix_python() return _nix_python end +function M.is_nix_discovered() + return _nix_discovered +end + function M.get_python_cmd(module, plugin_path) if _nix_python then return { _nix_python, '-m', 'scrapers.' .. module } @@ -113,6 +118,7 @@ local function discover_nix_python() end local plugin_path = M.get_plugin_path() + logger.log('Building Python environment with nix...', nil, true) local result = vim .system( { 'nix', 'build', plugin_path .. '#pythonEnv', '--no-link', '--print-out-paths' }, @@ -141,6 +147,7 @@ local function discover_nix_python() end _nix_python = python_path + _nix_discovered = true return true end From 0f9715298eb52adc27bedc3bed0b1cf1a311389c Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Wed, 18 Feb 2026 14:02:13 -0500 Subject: [PATCH 13/86] fix(ci): remove deprecated setups --- lua/cp/utils.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lua/cp/utils.lua b/lua/cp/utils.lua index 0031216..39e2876 100644 --- a/lua/cp/utils.lua +++ b/lua/cp/utils.lua @@ -82,18 +82,24 @@ function M.get_plugin_path() return vim.fn.fnamemodify(plugin_path, ':h:h:h') end +---@return boolean function M.is_nix_build() return _nix_python ~= nil end +---@return string|nil function M.get_nix_python() return _nix_python end +---@return boolean function M.is_nix_discovered() return _nix_discovered end +---@param module string +---@param plugin_path string +---@return string[] function M.get_python_cmd(module, plugin_path) if _nix_python then return { _nix_python, '-m', 'scrapers.' .. module } @@ -103,6 +109,7 @@ end local python_env_setup = false +---@return boolean local function discover_nix_python() local cache_dir = vim.fn.stdpath('cache') .. '/cp-nvim' local cache_file = cache_dir .. '/nix-python' From e02a29bd4029a38a26d5892c00a6b5dc4b0bc115 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Wed, 18 Feb 2026 14:04:36 -0500 Subject: [PATCH 14/86] fix(ci): remove duplicate workflows --- .github/workflows/ci.yaml | 112 ------------------------------------ .github/workflows/test.yaml | 4 +- 2 files changed, 1 insertion(+), 115 deletions(-) delete mode 100644 .github/workflows/ci.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index bf6bcea..0000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,112 +0,0 @@ -name: ci -on: - workflow_call: - pull_request: - branches: [main] - push: - branches: [main] - -jobs: - changes: - runs-on: ubuntu-latest - outputs: - lua: ${{ steps.changes.outputs.lua }} - python: ${{ steps.changes.outputs.python }} - steps: - - uses: actions/checkout@v4 - - uses: dorny/paths-filter@v3 - id: changes - with: - filters: | - lua: - - 'lua/**' - - 'spec/**' - - 'plugin/**' - - 'after/**' - - 'ftdetect/**' - - '*.lua' - - '.luarc.json' - - 'stylua.toml' - - 'selene.toml' - python: - - 'scripts/**' - - 'scrapers/**' - - 'tests/**' - - 'pyproject.toml' - - 'uv.lock' - - lua-format: - runs-on: ubuntu-latest - needs: changes - if: ${{ needs.changes.outputs.lua == 'true' }} - steps: - - uses: actions/checkout@v4 - - uses: JohnnyMorganz/stylua-action@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - version: 2.1.0 - args: --check . - - lua-lint: - runs-on: ubuntu-latest - needs: changes - if: ${{ needs.changes.outputs.lua == 'true' }} - steps: - - uses: actions/checkout@v4 - - uses: NTBBloodbath/selene-action@v1.0.0 - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --display-style quiet . - - lua-typecheck: - runs-on: ubuntu-latest - needs: changes - if: ${{ needs.changes.outputs.lua == 'true' }} - steps: - - uses: actions/checkout@v4 - - uses: mrcjkb/lua-typecheck-action@v0 - with: - checklevel: Warning - directories: lua - configpath: .luarc.json - - python-format: - runs-on: ubuntu-latest - needs: changes - if: ${{ needs.changes.outputs.python == 'true' }} - steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v4 - - run: uv tool install ruff - - run: ruff format --check . - - python-lint: - runs-on: ubuntu-latest - needs: changes - if: ${{ needs.changes.outputs.python == 'true' }} - steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v4 - - run: uv tool install ruff - - run: ruff check . - - python-typecheck: - runs-on: ubuntu-latest - needs: changes - if: ${{ needs.changes.outputs.python == 'true' }} - steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v4 - - run: uv sync --dev - - run: uvx ty check . - - python-test: - runs-on: ubuntu-latest - needs: changes - if: ${{ needs.changes.outputs.python == 'true' }} - steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v4 - - run: uv sync --dev - - run: uv run camoufox fetch - - run: uv run pytest tests/ -v diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ed7be0c..34bd84f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -44,9 +44,7 @@ jobs: - uses: actions/checkout@v4 - name: Install uv uses: astral-sh/setup-uv@v4 - - name: Install dependencies with pytest + - name: Install dependencies run: uv sync --dev - - name: Fetch camoufox data - run: uv run camoufox fetch - name: Run Python tests run: uv run pytest tests/ -v From b6f3398bbce99e6cc9f1ff277236bca937f3df4d Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Wed, 18 Feb 2026 14:07:53 -0500 Subject: [PATCH 15/86] fix(ci): formatting and typing --- lua/cp/scraper.lua | 7 ++++++- scrapers/codeforces.py | 2 +- tests/test_scrapers.py | 24 +++++++++--------------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/lua/cp/scraper.lua b/lua/cp/scraper.lua index 4771cc1..10fc7b7 100644 --- a/lua/cp/scraper.lua +++ b/lua/cp/scraper.lua @@ -44,7 +44,12 @@ local function run_scraper(platform, subcommand, args, opts) local handle handle = uv.spawn( cmd[1], - { args = vim.list_slice(cmd, 2), stdio = { nil, stdout, stderr }, env = env, cwd = plugin_path }, + { + args = vim.list_slice(cmd, 2), + stdio = { nil, stdout, stderr }, + env = env, + cwd = plugin_path, + }, function(code, signal) if buf ~= '' and opts.on_event then local ok_tail, ev_tail = pcall(vim.json.decode, buf) diff --git a/scrapers/codeforces.py b/scrapers/codeforces.py index 1d01b8d..a67489f 100644 --- a/scrapers/codeforces.py +++ b/scrapers/codeforces.py @@ -78,7 +78,7 @@ def _extract_title(block: Tag) -> tuple[str, str]: def _extract_samples(block: Tag) -> tuple[list[TestCase], bool]: st = block.find("div", class_="sample-test") - if not st: + if not isinstance(st, Tag): return [], False input_pres: list[Tag] = [ diff --git a/tests/test_scrapers.py b/tests/test_scrapers.py index 75f3cb0..8ce468f 100644 --- a/tests/test_scrapers.py +++ b/tests/test_scrapers.py @@ -6,11 +6,6 @@ from scrapers.models import ( TestsResult, ) -MODEL_FOR_MODE = { - "metadata": MetadataResult, - "contests": ContestListResult, -} - MATRIX = { "cses": { "metadata": ("introductory_problems",), @@ -43,17 +38,16 @@ def test_scraper_offline_fixture_matrix(run_scraper_offline, scraper, mode): assert rc in (0, 1), f"Bad exit code {rc}" assert objs, f"No JSON output for {scraper}:{mode}" - if mode in ("metadata", "contests"): - Model = MODEL_FOR_MODE[mode] - model = Model.model_validate(objs[-1]) - assert model is not None + if mode == "metadata": + model = MetadataResult.model_validate(objs[-1]) assert model.success is True - if mode == "metadata": - assert model.url - assert len(model.problems) >= 1 - assert all(isinstance(p.id, str) and p.id for p in model.problems) - else: - assert len(model.contests) >= 1 + assert model.url + assert len(model.problems) >= 1 + assert all(isinstance(p.id, str) and p.id for p in model.problems) + elif mode == "contests": + model = ContestListResult.model_validate(objs[-1]) + assert model.success is True + assert len(model.contests) >= 1 else: assert len(objs) >= 1, "No test objects returned" validated_any = False From c192afc5d7d8ae4b852471432be02390b955d874 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Wed, 18 Feb 2026 14:09:18 -0500 Subject: [PATCH 16/86] fix(ci): format --- lua/cp/scraper.lua | 52 +++++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/lua/cp/scraper.lua b/lua/cp/scraper.lua index 10fc7b7..d64a765 100644 --- a/lua/cp/scraper.lua +++ b/lua/cp/scraper.lua @@ -42,36 +42,32 @@ local function run_scraper(platform, subcommand, args, opts) local buf = '' local handle - handle = uv.spawn( - cmd[1], - { - args = vim.list_slice(cmd, 2), - stdio = { nil, stdout, stderr }, - env = env, - cwd = plugin_path, - }, - function(code, signal) - if buf ~= '' and opts.on_event then - local ok_tail, ev_tail = pcall(vim.json.decode, buf) - if ok_tail then - opts.on_event(ev_tail) - end - buf = '' - end - if opts.on_exit then - opts.on_exit({ success = (code == 0), code = code, signal = signal }) - end - if not stdout:is_closing() then - stdout:close() - end - if not stderr:is_closing() then - stderr:close() - end - if handle and not handle:is_closing() then - handle:close() + handle = uv.spawn(cmd[1], { + args = vim.list_slice(cmd, 2), + stdio = { nil, stdout, stderr }, + env = env, + cwd = plugin_path, + }, function(code, signal) + if buf ~= '' and opts.on_event then + local ok_tail, ev_tail = pcall(vim.json.decode, buf) + if ok_tail then + opts.on_event(ev_tail) end + buf = '' end - ) + if opts.on_exit then + opts.on_exit({ success = (code == 0), code = code, signal = signal }) + end + if not stdout:is_closing() then + stdout:close() + end + if not stderr:is_closing() then + stderr:close() + end + if handle and not handle:is_closing() then + handle:close() + end + end) if not handle then logger.log('Failed to start scraper process', vim.log.levels.ERROR) From 6045042dfb6c39a63549167705e6909dd062b42d Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Wed, 18 Feb 2026 17:24:26 -0500 Subject: [PATCH 17/86] fix: surface runtime check failures as clean notifications Problem: when required dependencies (GNU time/timeout, Python env) are missing, config.setup() throws a raw error() that surfaces as a Lua traceback. On macOS without coreutils the message is also redundant ("GNU time not found: GNU time not found") and offers no install hint. Solution: wrap config.setup() in pcall inside ensure_initialized(), strip the Lua source-location prefix, and emit a vim.notify at ERROR level. Add Darwin-specific install guidance to the GNU time/timeout not-found messages. Pass capability reasons directly instead of wrapping them in a redundant outer message. --- lua/cp/init.lua | 16 ++++++++++++---- lua/cp/utils.lua | 16 ++++++++++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/lua/cp/init.lua b/lua/cp/init.lua index 3cb36c1..088272a 100644 --- a/lua/cp/init.lua +++ b/lua/cp/init.lua @@ -15,17 +15,25 @@ local initialized = false local function ensure_initialized() if initialized then - return + return true end local user_config = vim.g.cp or {} - local config = config_module.setup(user_config) - config_module.set_current_config(config) + local ok, result = pcall(config_module.setup, user_config) + if not ok then + local msg = tostring(result):gsub('^.+:%d+: ', '') + vim.notify(msg, vim.log.levels.ERROR) + return false + end + config_module.set_current_config(result) initialized = true + return true end ---@return nil function M.handle_command(opts) - ensure_initialized() + if not ensure_initialized() then + return + end local commands = require('cp.commands') commands.handle_command(opts) end diff --git a/lua/cp/utils.lua b/lua/cp/utils.lua index 39e2876..8539f99 100644 --- a/lua/cp/utils.lua +++ b/lua/cp/utils.lua @@ -60,7 +60,11 @@ local function find_gnu_time() _time_cached = true _time_path = nil - _time_reason = 'GNU time not found' + if uname and uname.sysname == 'Darwin' then + _time_reason = 'GNU time not found (install via: brew install coreutils)' + else + _time_reason = 'GNU time not found' + end return _time_path, _time_reason end @@ -251,12 +255,12 @@ function M.check_required_runtime() local time = M.time_capability() if not time.ok then - return false, 'GNU time not found: ' .. (time.reason or '') + return false, time.reason end local timeout = M.timeout_capability() if not timeout.ok then - return false, 'GNU timeout not found: ' .. (timeout.reason or '') + return false, timeout.reason end if not M.setup_python_env() then @@ -310,7 +314,11 @@ local function find_gnu_timeout() _timeout_cached = true _timeout_path = nil - _timeout_reason = 'GNU timeout not found' + if uname and uname.sysname == 'Darwin' then + _timeout_reason = 'GNU timeout not found (install via: brew install coreutils)' + else + _timeout_reason = 'GNU timeout not found' + end return _timeout_path, _timeout_reason end From 06f72bbe2b7150506c8a85cfb0e83bc166a429ce Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Wed, 18 Feb 2026 17:27:01 -0500 Subject: [PATCH 18/86] fix: only show user-configured platforms in picker Problem: tbl_deep_extend merges user platforms on top of defaults, so all four default platforms survive even when the user only configures a subset. The picker then shows platforms the user never intended to use. Solution: before the deep merge, prune any default platform not present in the user's platforms table. This preserves per-platform default filling (the user doesn't have to re-specify every field) while ensuring only explicitly configured platforms appear. --- lua/cp/config.lua | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lua/cp/config.lua b/lua/cp/config.lua index dec8878..4ed762b 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -292,7 +292,15 @@ end ---@return cp.Config function M.setup(user_config) vim.validate({ user_config = { user_config, { 'table', 'nil' }, true } }) - local cfg = vim.tbl_deep_extend('force', vim.deepcopy(M.defaults), user_config or {}) + local defaults = vim.deepcopy(M.defaults) + if user_config and user_config.platforms then + for plat in pairs(defaults.platforms) do + if not user_config.platforms[plat] then + defaults.platforms[plat] = nil + end + end + end + local cfg = vim.tbl_deep_extend('force', defaults, user_config or {}) if not next(cfg.languages) then error('[cp.nvim] At least one language must be configured') From 976838d981cb2b0779474671cacc94d40879bedb Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Wed, 18 Feb 2026 17:30:16 -0500 Subject: [PATCH 19/86] fix: always run uv sync to recover from partial installs Problem: setup_python_env() skips uv sync when .venv/ exists. If a previous sync was interrupted (e.g. network timeout), the directory exists but is broken, and every subsequent session silently uses a corrupt environment. Solution: remove the isdirectory guard and always run uv sync. It is idempotent and near-instant when dependencies are already installed, so the only cost is one subprocess call per session. --- lua/cp/utils.lua | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/lua/cp/utils.lua b/lua/cp/utils.lua index 8539f99..fd4cf71 100644 --- a/lua/cp/utils.lua +++ b/lua/cp/utils.lua @@ -175,22 +175,17 @@ function M.setup_python_env() if vim.fn.executable('uv') == 1 then local plugin_path = M.get_plugin_path() - local venv_dir = plugin_path .. '/.venv' - if vim.fn.isdirectory(venv_dir) == 0 then - logger.log('Setting up Python environment for scrapers...') - local env = vim.fn.environ() - env.VIRTUAL_ENV = '' - env.PYTHONPATH = '' - env.CONDA_PREFIX = '' - local result = vim - .system({ 'uv', 'sync' }, { cwd = plugin_path, text = true, env = env }) - :wait() - if result.code ~= 0 then - logger.log('Failed to setup Python environment: ' .. result.stderr, vim.log.levels.ERROR) - return false - end - logger.log('Python environment setup complete.') + local env = vim.fn.environ() + env.VIRTUAL_ENV = '' + env.PYTHONPATH = '' + env.CONDA_PREFIX = '' + local result = vim + .system({ 'uv', 'sync' }, { cwd = plugin_path, text = true, env = env }) + :wait() + if result.code ~= 0 then + logger.log('Failed to setup Python environment: ' .. result.stderr, vim.log.levels.ERROR) + return false end python_env_setup = true From 622620f6d087fd442a7885943ea0ba18a22e89ed Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Wed, 18 Feb 2026 17:33:44 -0500 Subject: [PATCH 20/86] feat: add debug logging to python env, scraper, and runner Problem: with debug = true, there is not enough diagnostic output to troubleshoot environment or execution issues. The resolved python path, scraper commands, and compile/run shell commands are not logged. Solution: add logger.log calls at key decision points: python env resolution (nix vs uv vs discovery), uv sync stderr output, scraper subprocess commands, and compile/run shell strings. All gated behind the existing debug flag so they only appear when debug = true. --- lua/cp/runner/execute.lua | 2 ++ lua/cp/scraper.lua | 2 ++ lua/cp/utils.lua | 6 ++++++ 3 files changed, 10 insertions(+) diff --git a/lua/cp/runner/execute.lua b/lua/cp/runner/execute.lua index 76d055a..9e40b4f 100644 --- a/lua/cp/runner/execute.lua +++ b/lua/cp/runner/execute.lua @@ -43,6 +43,7 @@ end function M.compile(compile_cmd, substitutions, on_complete) local cmd = substitute_template(compile_cmd, substitutions) local sh = table.concat(cmd, ' ') .. ' 2>&1' + logger.log('compile: ' .. sh) local t0 = vim.uv.hrtime() vim.system({ 'sh', '-c', sh }, { text = false }, function(r) @@ -119,6 +120,7 @@ function M.run(cmd, stdin, timeout_ms, memory_mb, on_complete) local sec = math.ceil(timeout_ms / 1000) local timeout_prefix = ('%s -k 1s %ds '):format(timeout_bin, sec) local sh = prefix .. timeout_prefix .. ('%s -v sh -c %q 2>&1'):format(time_bin, prog) + logger.log('run: ' .. sh) local t0 = vim.uv.hrtime() vim.system({ 'sh', '-c', sh }, { stdin = stdin, text = true }, function(r) diff --git a/lua/cp/scraper.lua b/lua/cp/scraper.lua index d64a765..d2613cf 100644 --- a/lua/cp/scraper.lua +++ b/lua/cp/scraper.lua @@ -30,6 +30,8 @@ local function run_scraper(platform, subcommand, args, opts) vim.list_extend(cmd, { subcommand }) vim.list_extend(cmd, args) + logger.log('scraper cmd: ' .. table.concat(cmd, ' ')) + local env = vim.fn.environ() env.VIRTUAL_ENV = '' env.PYTHONPATH = '' diff --git a/lua/cp/utils.lua b/lua/cp/utils.lua index fd4cf71..05a715d 100644 --- a/lua/cp/utils.lua +++ b/lua/cp/utils.lua @@ -169,12 +169,14 @@ function M.setup_python_env() end if _nix_python then + logger.log('Python env: nix (python=' .. _nix_python .. ')') python_env_setup = true return true end if vim.fn.executable('uv') == 1 then local plugin_path = M.get_plugin_path() + logger.log('Python env: uv sync (dir=' .. plugin_path .. ')') local env = vim.fn.environ() env.VIRTUAL_ENV = '' @@ -187,12 +189,16 @@ function M.setup_python_env() logger.log('Failed to setup Python environment: ' .. result.stderr, vim.log.levels.ERROR) return false end + if result.stderr and result.stderr ~= '' then + logger.log('uv sync stderr: ' .. result.stderr:gsub('%s+$', '')) + end python_env_setup = true return true end if vim.fn.executable('nix') == 1 then + logger.log('Python env: nix discovery') if discover_nix_python() then python_env_setup = true return true From 49e4233b3f7b289c30fa5bd69e454184c9735a8b Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Wed, 18 Feb 2026 17:48:06 -0500 Subject: [PATCH 21/86] fix: decouple python env setup from config init Problem: setup_python_env() is called from check_required_runtime() during config.setup(), which runs on the very first :CP command. The uv sync and nix build calls use vim.system():wait(), blocking the Neovim event loop. During the block the UI is frozen and vim.schedule-based log messages never render, so the user sees an unresponsive editor with no feedback. Solution: remove setup_python_env() from check_required_runtime() so config init is instant. Call it lazily from run_scraper() instead, only when a scraper subprocess is actually needed. Use vim.notify + vim.cmd.redraw() before blocking calls so the notification renders immediately via a forced screen repaint, rather than being queued behind vim.schedule. --- lua/cp/scraper.lua | 9 +++++++++ lua/cp/utils.lua | 9 ++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/lua/cp/scraper.lua b/lua/cp/scraper.lua index d2613cf..21cd697 100644 --- a/lua/cp/scraper.lua +++ b/lua/cp/scraper.lua @@ -25,6 +25,15 @@ end ---@param args string[] ---@param opts { sync?: boolean, ndjson?: boolean, on_event?: fun(ev: table), on_exit?: fun(result: table) } local function run_scraper(platform, subcommand, args, opts) + if not utils.setup_python_env() then + local msg = 'no Python environment available (install uv or nix)' + logger.log(msg, vim.log.levels.ERROR) + if opts and opts.on_exit then + opts.on_exit({ success = false, error = msg }) + end + return { success = false, error = msg } + end + local plugin_path = utils.get_plugin_path() local cmd = utils.get_python_cmd(platform, plugin_path) vim.list_extend(cmd, { subcommand }) diff --git a/lua/cp/utils.lua b/lua/cp/utils.lua index 05a715d..b120a6f 100644 --- a/lua/cp/utils.lua +++ b/lua/cp/utils.lua @@ -129,7 +129,8 @@ local function discover_nix_python() end local plugin_path = M.get_plugin_path() - logger.log('Building Python environment with nix...', nil, true) + vim.notify('[cp.nvim] Building Python environment with nix...', vim.log.levels.INFO) + vim.cmd.redraw() local result = vim .system( { 'nix', 'build', plugin_path .. '#pythonEnv', '--no-link', '--print-out-paths' }, @@ -177,6 +178,8 @@ function M.setup_python_env() if vim.fn.executable('uv') == 1 then local plugin_path = M.get_plugin_path() logger.log('Python env: uv sync (dir=' .. plugin_path .. ')') + vim.notify('[cp.nvim] Setting up Python environment...', vim.log.levels.INFO) + vim.cmd.redraw() local env = vim.fn.environ() env.VIRTUAL_ENV = '' @@ -264,10 +267,6 @@ function M.check_required_runtime() return false, timeout.reason end - if not M.setup_python_env() then - return false, 'no Python environment available (install uv or nix)' - end - return true end From 760e7d77313dedeb4a28c6764c325213a18d97ba Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Fri, 20 Feb 2026 17:47:36 -0500 Subject: [PATCH 22/86] fix(ci): format --- lua/cp/utils.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lua/cp/utils.lua b/lua/cp/utils.lua index b120a6f..b602940 100644 --- a/lua/cp/utils.lua +++ b/lua/cp/utils.lua @@ -189,7 +189,10 @@ function M.setup_python_env() .system({ 'uv', 'sync' }, { cwd = plugin_path, text = true, env = env }) :wait() if result.code ~= 0 then - logger.log('Failed to setup Python environment: ' .. result.stderr, vim.log.levels.ERROR) + logger.log( + 'Failed to setup Python environment: ' .. (result.stderr or ''), + vim.log.levels.ERROR + ) return false end if result.stderr and result.stderr ~= '' then From ff5ba39a592d079780819bb8226dcb35741349a4 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sat, 21 Feb 2026 23:58:09 -0500 Subject: [PATCH 23/86] docs: fix dependencies section in readme Problem: time and timeout were listed as optional dependencies despite being required for plugin initialization. nix was not mentioned as an alternative to uv for the Python scraping environment. Solution: rename section to "Dependencies", list time/timeout first, and add nix as an alternative to uv for scraping. --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cf82417..427496d 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,12 @@ Install using your package manager of choice or via luarocks install cp.nvim ``` -## Optional Dependencies +## Dependencies -- [uv](https://docs.astral.sh/uv/) for problem scraping - GNU [time](https://www.gnu.org/software/time/) and [timeout](https://www.gnu.org/software/coreutils/manual/html_node/timeout-invocation.html) +- [uv](https://docs.astral.sh/uv/) or [nix](https://nixos.org/) for problem + scraping ## Quick Start From 484a4a56d003cd3d7452904b799f8344fe392745 Mon Sep 17 00:00:00 2001 From: Harivansh Rathi Date: Sun, 22 Feb 2026 11:55:13 -0500 Subject: [PATCH 24/86] fix(scraper): pass uv.spawn env as KEY=VALUE list Neovim/libuv spawn expects env as a list of KEY=VALUE strings. Passing the map from vim.fn.environ() can fail process startup with ENOENT, which breaks NDJSON test scraping and surfaces as 'Failed to start scraper process'.\n\nConvert env map to a deterministic list before uv.spawn in the NDJSON scraper path. Co-authored-by: Codex --- lua/cp/scraper.lua | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lua/cp/scraper.lua b/lua/cp/scraper.lua index 21cd697..659983f 100644 --- a/lua/cp/scraper.lua +++ b/lua/cp/scraper.lua @@ -20,6 +20,15 @@ local function syshandle(result) return { success = true, data = data } end +local function spawn_env_list(env_map) + local out = {} + for key, value in pairs(env_map) do + out[#out + 1] = tostring(key) .. '=' .. tostring(value) + end + table.sort(out) + return out +end + ---@param platform string ---@param subcommand string ---@param args string[] @@ -56,7 +65,7 @@ local function run_scraper(platform, subcommand, args, opts) handle = uv.spawn(cmd[1], { args = vim.list_slice(cmd, 2), stdio = { nil, stdout, stderr }, - env = env, + env = spawn_env_list(env), cwd = plugin_path, }, function(code, signal) if buf ~= '' and opts.on_event then From 9fc34cb6fd7893bd7678777bf2b65ac25ab0e010 Mon Sep 17 00:00:00 2001 From: Harivansh Rathi Date: Sun, 22 Feb 2026 12:01:37 -0500 Subject: [PATCH 25/86] chore(scraper): add LuaCATS types for env helper Add LuaCATS annotations to the env conversion helper and drop the table.sort call since ordering is not required by uv.spawn. Co-authored-by: Codex --- lua/cp/scraper.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/cp/scraper.lua b/lua/cp/scraper.lua index 659983f..73b6177 100644 --- a/lua/cp/scraper.lua +++ b/lua/cp/scraper.lua @@ -20,12 +20,13 @@ local function syshandle(result) return { success = true, data = data } end +---@param env_map table +---@return string[] local function spawn_env_list(env_map) local out = {} for key, value in pairs(env_map) do out[#out + 1] = tostring(key) .. '=' .. tostring(value) end - table.sort(out) return out end From db5bd791f964a919e672d0b54bf06c6286648e89 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sun, 22 Feb 2026 15:48:13 -0500 Subject: [PATCH 26/86] fix(cache): invalidate stale cache on version mismatch Problem: after an install or update, the on-disk cache may contain data written by an older version of the plugin whose format no longer matches what the current code expects. Solution: embed a CACHE_VERSION in every saved cache file. On load, if the stored version is missing or differs from the current one, wipe the cache and rewrite it. Corrupt (non-decodable) cache files are handled the same way instead of only logging an error. --- lua/cp/cache.lua | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lua/cp/cache.lua b/lua/cp/cache.lua index 544aaf4..f3d0a94 100644 --- a/lua/cp/cache.lua +++ b/lua/cp/cache.lua @@ -38,6 +38,8 @@ local M = {} +local CACHE_VERSION = 1 + local logger = require('cp.log') local cache_file = vim.fn.stdpath('data') .. '/cp-nvim.json' local cache_data = {} @@ -65,9 +67,15 @@ function M.load() local ok, decoded = pcall(vim.json.decode, table.concat(content, '\n')) if ok then - cache_data = decoded + if decoded._version ~= CACHE_VERSION then + cache_data = {} + M.save() + else + cache_data = decoded + end else - logger.log('Could not decode json in cache file', vim.log.levels.ERROR) + cache_data = {} + M.save() end loaded = true end @@ -78,6 +86,7 @@ function M.save() vim.schedule(function() vim.fn.mkdir(vim.fn.fnamemodify(cache_file, ':h'), 'p') + cache_data._version = CACHE_VERSION local encoded = vim.json.encode(cache_data) local lines = vim.split(encoded, '\n') vim.fn.writefile(lines, cache_file) From d3ac300ea05f6e0c8ae9f9a40db5b79b90b27ea1 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sun, 22 Feb 2026 15:54:04 -0500 Subject: [PATCH 27/86] fix(cache): remove unused logger import Problem: the logger import became unused after replacing the error log with a silent cache wipe. Solution: drop the require. --- lua/cp/cache.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/lua/cp/cache.lua b/lua/cp/cache.lua index f3d0a94..efdcad7 100644 --- a/lua/cp/cache.lua +++ b/lua/cp/cache.lua @@ -40,7 +40,6 @@ local M = {} local CACHE_VERSION = 1 -local logger = require('cp.log') local cache_file = vim.fn.stdpath('data') .. '/cp-nvim.json' local cache_data = {} local loaded = false From 1c31abe3d6ad53e9dfeb4ae64090429547e515cb Mon Sep 17 00:00:00 2001 From: Harivansh Rathi Date: Sun, 22 Feb 2026 20:44:40 -0500 Subject: [PATCH 28/86] fix(open_url): open problem URL when switching problems Also fix contest-change detection so URL open logic triggers when either platform or contest changes. This makes :CP next/:CP prev and problem jumps open the correct page when open_url is enabled. Co-authored-by: Codex --- doc/cp.nvim.txt | 2 +- lua/cp/setup.lua | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/doc/cp.nvim.txt b/doc/cp.nvim.txt index e85ea45..49cdaf7 100644 --- a/doc/cp.nvim.txt +++ b/doc/cp.nvim.txt @@ -143,7 +143,7 @@ run CSES problems with Rust using the single schema: (default: concatenates contest_id and problem_id, lowercased) {ui} (|CpUI|) UI settings: panel, diff backend, picker. {open_url} (boolean) Open the contest & problem url in the browser - when the contest is first opened. + when a new contest is opened or the active problem changes. *CpPlatform* Fields: ~ diff --git a/lua/cp/setup.lua b/lua/cp/setup.lua index e3bb38d..deeadd9 100644 --- a/lua/cp/setup.lua +++ b/lua/cp/setup.lua @@ -121,6 +121,7 @@ end ---@param language? string function M.setup_contest(platform, contest_id, problem_id, language) local old_platform, old_contest_id = state.get_platform(), state.get_contest_id() + local old_problem_id = state.get_problem_id() state.set_platform(platform) state.set_contest_id(contest_id) @@ -133,7 +134,7 @@ function M.setup_contest(platform, contest_id, problem_id, language) end end - local is_new_contest = old_platform ~= platform and old_contest_id ~= contest_id + local is_new_contest = old_platform ~= platform or old_contest_id ~= contest_id cache.load() @@ -143,7 +144,9 @@ function M.setup_contest(platform, contest_id, problem_id, language) M.setup_problem(pid, language) start_tests(platform, contest_id, problems) - if config_module.get_config().open_url and is_new_contest and contest_data.url then + local is_new_problem = old_problem_id ~= pid + local should_open_url = config_module.get_config().open_url and (is_new_contest or is_new_problem) + if should_open_url and contest_data.url then vim.ui.open(contest_data.url:format(pid)) end end From e989897c7767b8d9c9a1be4ca5c0618b13a18ea4 Mon Sep 17 00:00:00 2001 From: Harivansh Rathi Date: Sun, 22 Feb 2026 22:16:48 -0500 Subject: [PATCH 29/86] style(lua): format URL-open condition in setup Co-authored-by: Codex --- lua/cp/setup.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/cp/setup.lua b/lua/cp/setup.lua index deeadd9..5130822 100644 --- a/lua/cp/setup.lua +++ b/lua/cp/setup.lua @@ -145,7 +145,8 @@ function M.setup_contest(platform, contest_id, problem_id, language) start_tests(platform, contest_id, problems) local is_new_problem = old_problem_id ~= pid - local should_open_url = config_module.get_config().open_url and (is_new_contest or is_new_problem) + local should_open_url = config_module.get_config().open_url + and (is_new_contest or is_new_problem) if should_open_url and contest_data.url then vim.ui.open(contest_data.url:format(pid)) end From 591f70a2370d08c2fff756eaad030831d6778b59 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 23 Feb 2026 17:37:47 -0500 Subject: [PATCH 30/86] build(flake): add lua-language-server to devShell Problem: lua-language-server is not available in the dev shell, making it impossible to run local type-checking diagnostics. Solution: add lua-language-server to the devShell packages. --- flake.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/flake.nix b/flake.nix index 705747f..7e1551b 100644 --- a/flake.nix +++ b/flake.nix @@ -65,6 +65,7 @@ packages = with (pkgsFor system); [ uv python312 + lua-language-server ]; }; }); From 4d58db8520ad8810426950fcf381c2fb8fc57bb7 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 23 Feb 2026 18:04:17 -0500 Subject: [PATCH 31/86] ci: nix config migration --- .github/workflows/quality.yaml | 29 +++++++---------------------- .luarc.json | 2 +- selene.toml | 3 +++ vim.toml | 30 ------------------------------ vim.yaml | 24 ++++++++++++++++++++++++ 5 files changed, 35 insertions(+), 53 deletions(-) delete mode 100644 vim.toml create mode 100644 vim.yaml diff --git a/.github/workflows/quality.yaml b/.github/workflows/quality.yaml index 731e74b..664b33e 100644 --- a/.github/workflows/quality.yaml +++ b/.github/workflows/quality.yaml @@ -28,6 +28,7 @@ jobs: - '*.lua' - '.luarc.json' - '*.toml' + - 'vim.yaml' python: - 'scripts/**/.py' - 'scrapers/**/*.py' @@ -45,11 +46,8 @@ jobs: if: ${{ needs.changes.outputs.lua == 'true' }} steps: - uses: actions/checkout@v4 - - uses: JohnnyMorganz/stylua-action@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - version: 2.1.0 - args: --check . + - uses: cachix/install-nix-action@v31 + - run: nix develop --command stylua --check . lua-lint: name: Lua Lint Check @@ -58,11 +56,8 @@ jobs: if: ${{ needs.changes.outputs.lua == 'true' }} steps: - uses: actions/checkout@v4 - - name: Lint with Selene - uses: NTBBloodbath/selene-action@v1.0.0 - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --display-style quiet . + - uses: cachix/install-nix-action@v31 + - run: nix develop --command selene --display-style quiet . lua-typecheck: name: Lua Type Check @@ -127,15 +122,5 @@ jobs: if: ${{ needs.changes.outputs.markdown == 'true' }} steps: - uses: actions/checkout@v4 - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 8 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - name: Install prettier - run: pnpm add -g prettier@3.1.0 - - name: Check markdown formatting with prettier - run: prettier --check . + - uses: cachix/install-nix-action@v31 + - run: nix develop --command prettier --check . diff --git a/.luarc.json b/.luarc.json index 3ccfeda..19558f6 100644 --- a/.luarc.json +++ b/.luarc.json @@ -1,5 +1,5 @@ { - "runtime.version": "Lua 5.1", + "runtime.version": "LuaJIT", "runtime.path": ["lua/?.lua", "lua/?/init.lua"], "diagnostics.globals": ["vim"], "workspace.library": ["$VIMRUNTIME/lua", "${3rd}/luv/library"], diff --git a/selene.toml b/selene.toml index 96cf5ab..f2ada4b 100644 --- a/selene.toml +++ b/selene.toml @@ -1 +1,4 @@ std = 'vim' + +[lints] +bad_string_escape = 'allow' diff --git a/vim.toml b/vim.toml deleted file mode 100644 index 8bf26ea..0000000 --- a/vim.toml +++ /dev/null @@ -1,30 +0,0 @@ -[selene] -base = "lua51" -name = "vim" - -[vim] -any = true - -[jit] -any = true - -[assert] -any = true - -[describe] -any = true - -[it] -any = true - -[before_each] -any = true - -[after_each] -any = true - -[spy] -any = true - -[stub] -any = true diff --git a/vim.yaml b/vim.yaml new file mode 100644 index 0000000..401fce6 --- /dev/null +++ b/vim.yaml @@ -0,0 +1,24 @@ +--- +base: lua51 +name: vim +lua_versions: + - luajit +globals: + vim: + any: true + jit: + any: true + assert: + any: true + describe: + any: true + it: + any: true + before_each: + any: true + after_each: + any: true + spy: + any: true + stub: + any: true From 48c08825b24e1da650d8ad321a0e3318c16175b7 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 23 Feb 2026 18:16:22 -0500 Subject: [PATCH 32/86] ci: add missing packages --- flake.nix | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flake.nix b/flake.nix index 7e1551b..202b13b 100644 --- a/flake.nix +++ b/flake.nix @@ -65,6 +65,9 @@ packages = with (pkgsFor system); [ uv python312 + prettier + stylua + selene lua-language-server ]; }; From 4ccab9ee1fe375d8c9282d797809edfb7c27a9b9 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 26 Feb 2026 19:09:16 -0500 Subject: [PATCH 33/86] fix(config): add bit to ignored filetypes --- vim.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vim.yaml b/vim.yaml index 401fce6..3821d25 100644 --- a/vim.yaml +++ b/vim.yaml @@ -22,3 +22,5 @@ globals: any: true stub: any: true + bit: + any: true From 3cb872a65f85145e3b688c70a6f6475fbd2a9e3a Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 26 Feb 2026 22:44:04 -0500 Subject: [PATCH 34/86] fix: replace deprecated vim.loop with vim.uv Problem: vim.loop is deprecated since Neovim 0.10 in favour of vim.uv. Five call sites across scraper.lua, setup.lua, utils.lua, and health.lua still referenced the old alias. Solution: replace every vim.loop reference with vim.uv directly. --- lua/cp/health.lua | 2 +- lua/cp/scraper.lua | 2 +- lua/cp/setup.lua | 2 +- lua/cp/utils.lua | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lua/cp/health.lua b/lua/cp/health.lua index d3a3fea..97595d6 100644 --- a/lua/cp/health.lua +++ b/lua/cp/health.lua @@ -13,7 +13,7 @@ local function check() vim.health.error('cp.nvim requires Neovim 0.10.0+') end - local uname = vim.loop.os_uname() + local uname = vim.uv.os_uname() if uname.sysname == 'Windows_NT' then vim.health.error('Windows is not supported') end diff --git a/lua/cp/scraper.lua b/lua/cp/scraper.lua index 73b6177..29cc63e 100644 --- a/lua/cp/scraper.lua +++ b/lua/cp/scraper.lua @@ -57,7 +57,7 @@ local function run_scraper(platform, subcommand, args, opts) env.CONDA_PREFIX = '' if opts and opts.ndjson then - local uv = vim.loop + local uv = vim.uv local stdout = uv.new_pipe(false) local stderr = uv.new_pipe(false) local buf = '' diff --git a/lua/cp/setup.lua b/lua/cp/setup.lua index 5130822..0572d55 100644 --- a/lua/cp/setup.lua +++ b/lua/cp/setup.lua @@ -179,7 +179,7 @@ function M.setup_contest(platform, contest_id, problem_id, language) contest_id = contest_id, language = lang, requested_problem_id = problem_id, - token = vim.loop.hrtime(), + token = vim.uv.hrtime(), }) logger.log('Fetching contests problems...', vim.log.levels.INFO, true) diff --git a/lua/cp/utils.lua b/lua/cp/utils.lua index b602940..285ebf8 100644 --- a/lua/cp/utils.lua +++ b/lua/cp/utils.lua @@ -5,7 +5,7 @@ local logger = require('cp.log') local _nix_python = nil local _nix_discovered = false -local uname = vim.loop.os_uname() +local uname = vim.uv.os_uname() local _time_cached = false local _time_path = nil @@ -336,7 +336,7 @@ function M.timeout_capability() end function M.cwd_executables() - local uv = vim.uv or vim.loop + local uv = vim.uv local req = uv.fs_scandir('.') if not req then return {} From d274e0c1170f6bdbe94a34f3084febc201ec6977 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 26 Feb 2026 22:44:32 -0500 Subject: [PATCH 35/86] fix(cache): replace stale M._cache field with get_raw_cache accessor Problem: M._cache = cache_data captured the initial empty table reference at module load time. After M.load() reassigns cache_data to the decoded JSON, M._cache is permanently stale and returns the wrong table. Solution: remove the field assignment and expose get_raw_cache() which closes over cache_data and always returns the current table. --- lua/cp/cache.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/cp/cache.lua b/lua/cp/cache.lua index efdcad7..33342c2 100644 --- a/lua/cp/cache.lua +++ b/lua/cp/cache.lua @@ -346,6 +346,8 @@ function M.get_data_pretty() return vim.inspect(cache_data) end -M._cache = cache_data +function M.get_raw_cache() + return cache_data +end return M From 81f52738406f6c7b29bc911ec165871caaa69490 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 26 Feb 2026 22:44:34 -0500 Subject: [PATCH 36/86] chore: convert .luarc.json to nested format and add busted library Problem: .luarc.json used the flat dotted-key format which is not the canonical LuaLS schema. The busted library was also missing, so LuaLS could not resolve types in test files. Solution: rewrite .luarc.json using nested objects and add ${3rd}/busted/library to workspace.library. --- .luarc.json | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/.luarc.json b/.luarc.json index 19558f6..e0f7a7c 100644 --- a/.luarc.json +++ b/.luarc.json @@ -1,8 +1,16 @@ { - "runtime.version": "LuaJIT", - "runtime.path": ["lua/?.lua", "lua/?/init.lua"], - "diagnostics.globals": ["vim"], - "workspace.library": ["$VIMRUNTIME/lua", "${3rd}/luv/library"], - "workspace.checkThirdParty": false, - "completion.callSnippet": "Replace" + "runtime": { + "version": "LuaJIT", + "path": ["lua/?.lua", "lua/?/init.lua"] + }, + "diagnostics": { + "globals": ["vim"] + }, + "workspace": { + "library": ["$VIMRUNTIME/lua", "${3rd}/luv/library", "${3rd}/busted/library"], + "checkThirdParty": false + }, + "completion": { + "callSnippet": "Replace" + } } From 585cf2a077a813cd071179b365108a248a594a2a Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 26 Feb 2026 22:54:06 -0500 Subject: [PATCH 37/86] feat(runner): run test cases in parallel Problem: test cases were executed sequentially, each waiting for the previous process to finish before starting the next. On problems with many test cases this meant wall-clock run time scaled linearly. Solution: fan out all test case processes simultaneously. A remaining counter fires on_done once all callbacks have returned. on_each is called per completion as before; callers that pass on_each ignore its arguments so the index semantics change is non-breaking. --- lua/cp/runner/run.lua | 45 ++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/lua/cp/runner/run.lua b/lua/cp/runner/run.lua index 36a560c..4e4a8f6 100644 --- a/lua/cp/runner/run.lua +++ b/lua/cp/runner/run.lua @@ -276,26 +276,35 @@ function M.run_all_test_cases(indices, debug, on_each, on_done) end end - local function run_next(pos) - if pos > #to_run then - logger.log( - ('Finished %s %d test cases.'):format(debug and 'debugging' or 'running', #to_run), - vim.log.levels.INFO, - true - ) - on_done(panel_state.test_cases) - return - end - - M.run_test_case(to_run[pos], debug, function() - if on_each then - on_each(pos, #to_run) - end - run_next(pos + 1) - end) + if #to_run == 0 then + logger.log( + ('Finished %s %d test cases.'):format(debug and 'debugging' or 'running', 0), + vim.log.levels.INFO, + true + ) + on_done(panel_state.test_cases) + return end - run_next(1) + local total = #to_run + local remaining = total + + for _, idx in ipairs(to_run) do + M.run_test_case(idx, debug, function() + if on_each then + on_each(idx, total) + end + remaining = remaining - 1 + if remaining == 0 then + logger.log( + ('Finished %s %d test cases.'):format(debug and 'debugging' or 'running', total), + vim.log.levels.INFO, + true + ) + on_done(panel_state.test_cases) + end + end) + end end ---@return PanelState From ce5648f9cfc1ef467e9126e4ab5d673b539cbdaf Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 26 Feb 2026 22:54:27 -0500 Subject: [PATCH 38/86] docs: add statusline integration recipes Problem: cp.nvim exposed no documentation showing how to integrate its runtime state into a statusline. Users had to discover the state module API by reading source. Solution: add a STATUSLINE INTEGRATION section to the vimdoc with a state API reference and recipes for vanilla statusline, lualine, and heirline. Also anchors the *cp.State* help tag referenced in prose elsewhere in the doc. --- doc/cp.nvim.txt | 110 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/doc/cp.nvim.txt b/doc/cp.nvim.txt index 49cdaf7..27563a1 100644 --- a/doc/cp.nvim.txt +++ b/doc/cp.nvim.txt @@ -888,6 +888,116 @@ Functions ~ Parameters: ~ {bufnr} (integer) Buffer handle +============================================================================== +STATUSLINE INTEGRATION *cp-statusline* + +cp.nvim exposes its runtime state through a public module that can be queried +from any statusline plugin. Import it with: >lua + local state = require('cp.state') +< +All getters return nil when no problem is active, so guard every value before +use. Calling any getter outside a CP context is safe and has no side effects. + +State API ~ + *cp.State* +The following getters are available for statusline use: + + get_platform() (string?) Platform id. e.g. "codeforces", "atcoder" + get_contest_id() (string?) Contest id. e.g. "1933", "abc324" + get_problem_id() (string?) Problem id. e.g. "A", "B" + get_language() (string?) Language id. e.g. "cpp", "python" + get_base_name() (string?) Derived filename stem. e.g. "1933a" + get_source_file() (string?) Full source filename. e.g. "1933a.cc" + get_active_panel() (string?) Non-nil when the test panel is open. + +Recipe: vanilla statusline ~ + +Set vim.o.statusline from an autocommand so it is recalculated on every +BufEnter: >lua + local function cp_component() + local state = require('cp.state') + local platform = state.get_platform() + if not platform then + return '' + end + local parts = { + platform, + state.get_contest_id(), + state.get_problem_id(), + state.get_language(), + } + local filtered = {} + for _, v in ipairs(parts) do + if v then filtered[#filtered + 1] = v end + end + return '[' .. table.concat(filtered, ' · ') .. ']' + end + + vim.api.nvim_create_autocmd({ 'BufEnter', 'User' }, { + callback = function() + vim.o.statusline = cp_component() .. ' %f %=%l:%c' + end + }) +< + +Recipe: lualine ~ + +Add a custom component to any lualine section. The cond field hides the +component entirely when no problem is active: >lua + local function cp_lualine() + local state = require('cp.state') + local parts = { + state.get_platform(), + state.get_contest_id(), + state.get_problem_id(), + state.get_language(), + } + local filtered = {} + for _, v in ipairs(parts) do + if v then filtered[#filtered + 1] = v end + end + return table.concat(filtered, ' · ') + end + + require('lualine').setup({ + sections = { + lualine_c = { + { + cp_lualine, + cond = function() + return require('cp.state').get_platform() ~= nil + end, + }, + }, + }, + }) +< + +Recipe: heirline ~ + +Build a heirline component using a provider and condition: >lua + local CpComponent = { + condition = function() + return require('cp.state').get_platform() ~= nil + end, + provider = function() + local state = require('cp.state') + local parts = { + state.get_platform(), + state.get_contest_id(), + state.get_problem_id(), + state.get_language(), + } + local filtered = {} + for _, v in ipairs(parts) do + if v then filtered[#filtered + 1] = v end + end + return '[' .. table.concat(filtered, ' · ') .. ']' + end, + } +< +Include CpComponent in your heirline StatusLine spec wherever desired. + ============================================================================== PANEL KEYMAPS *cp-panel-keys* From 2c25ec616afd3c3017e0bb6e46a667ceb8c4778e Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 26 Feb 2026 22:54:32 -0500 Subject: [PATCH 39/86] feat: add per-language template file support Problem: new solution files were always created empty, requiring users to manually paste boilerplate or rely on editor snippets that fire outside cp.nvim's control. Solution: add an optional template field to the language config. When set to a file path, its contents are written into every newly created solution buffer before the setup_code hook runs. Existing files are never overwritten. --- lua/cp/config.lua | 5 +++++ lua/cp/setup.lua | 53 ++++++++++++++++++++++++++++++++++++----------- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/lua/cp/config.lua b/lua/cp/config.lua index 4ed762b..6cf43d9 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -7,6 +7,7 @@ ---@class CpLanguage ---@field extension string ---@field commands CpLangCommands +---@field template? string ---@class CpPlatformOverrides ---@field extension? string @@ -215,6 +216,10 @@ local function validate_language(id, lang) commands = { lang.commands, { 'table' } }, }) + if lang.template ~= nil then + vim.validate({ template = { lang.template, 'string' } }) + end + if not lang.commands.run then error(('[cp.nvim] languages.%s.commands.run is required'):format(id)) end diff --git a/lua/cp/setup.lua b/lua/cp/setup.lua index 0572d55..86fdee1 100644 --- a/lua/cp/setup.lua +++ b/lua/cp/setup.lua @@ -8,6 +8,25 @@ local logger = require('cp.log') local scraper = require('cp.scraper') local state = require('cp.state') +local function apply_template(bufnr, lang_id, platform) + local config = config_module.get_config() + local eff = config.runtime.effective[platform] + and config.runtime.effective[platform][lang_id] + if not eff or not eff.template then + return + end + local path = vim.fn.expand(eff.template) + if vim.fn.filereadable(path) ~= 1 then + logger.log( + ('[cp.nvim] template not readable: %s'):format(path), + vim.log.levels.WARN + ) + return + end + local lines = vim.fn.readfile(path) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) +end + ---Get the language of the current file from cache ---@return string? local function get_current_file_language() @@ -270,14 +289,17 @@ function M.setup_problem(problem_id, language) mods = { silent = true, noautocmd = true, keepalt = true }, }) state.set_solution_win(vim.api.nvim_get_current_win()) - if config.hooks and config.hooks.setup_code and not vim.b[prov.bufnr].cp_setup_done then - local ok = pcall(config.hooks.setup_code, state) - if ok then + if not vim.b[prov.bufnr].cp_setup_done then + apply_template(prov.bufnr, lang, platform) + if config.hooks and config.hooks.setup_code then + local ok = pcall(config.hooks.setup_code, state) + if ok then + vim.b[prov.bufnr].cp_setup_done = true + end + else + helpers.clearcol(prov.bufnr) vim.b[prov.bufnr].cp_setup_done = true end - elseif not vim.b[prov.bufnr].cp_setup_done then - helpers.clearcol(prov.bufnr) - vim.b[prov.bufnr].cp_setup_done = true end cache.set_file_state( vim.fn.fnamemodify(source_file, ':p'), @@ -300,14 +322,21 @@ function M.setup_problem(problem_id, language) local bufnr = vim.api.nvim_get_current_buf() state.set_solution_win(vim.api.nvim_get_current_win()) require('cp.ui.views').ensure_io_view() - if config.hooks and config.hooks.setup_code and not vim.b[bufnr].cp_setup_done then - local ok = pcall(config.hooks.setup_code, state) - if ok then + if not vim.b[bufnr].cp_setup_done then + local is_new = vim.api.nvim_buf_line_count(bufnr) == 1 + and vim.api.nvim_buf_get_lines(bufnr, 0, 1, false)[1] == '' + if is_new then + apply_template(bufnr, lang, platform) + end + if config.hooks and config.hooks.setup_code then + local ok = pcall(config.hooks.setup_code, state) + if ok then + vim.b[bufnr].cp_setup_done = true + end + else + helpers.clearcol(bufnr) vim.b[bufnr].cp_setup_done = true end - elseif not vim.b[bufnr].cp_setup_done then - helpers.clearcol(bufnr) - vim.b[bufnr].cp_setup_done = true end cache.set_file_state( vim.fn.expand('%:p'), From 84d12758c2151ad57e4b7162a260cfcb18296f1f Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 26 Feb 2026 22:57:18 -0500 Subject: [PATCH 40/86] style(setup): apply stylua formatting --- lua/cp/setup.lua | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lua/cp/setup.lua b/lua/cp/setup.lua index 86fdee1..44c993d 100644 --- a/lua/cp/setup.lua +++ b/lua/cp/setup.lua @@ -10,17 +10,13 @@ local state = require('cp.state') local function apply_template(bufnr, lang_id, platform) local config = config_module.get_config() - local eff = config.runtime.effective[platform] - and config.runtime.effective[platform][lang_id] + local eff = config.runtime.effective[platform] and config.runtime.effective[platform][lang_id] if not eff or not eff.template then return end local path = vim.fn.expand(eff.template) if vim.fn.filereadable(path) ~= 1 then - logger.log( - ('[cp.nvim] template not readable: %s'):format(path), - vim.log.levels.WARN - ) + logger.log(('[cp.nvim] template not readable: %s'):format(path), vim.log.levels.WARN) return end local lines = vim.fn.readfile(path) From e685a8089f5db2d7d6ad60f85a46d8b59129b02f Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 26 Feb 2026 22:55:05 -0500 Subject: [PATCH 41/86] feat: add epsilon tolerance for floating-point output comparison Problem: output comparison used exact string equality after whitespace normalisation, causing correct solutions to fail on problems where floating-point answers are accepted within a tolerance (e.g. 1e-6). Solution: add an optional ui.panel.epsilon config value. When set, actual and expected output are compared token-by-token: numeric tokens are compared with math.abs(a - b) <= epsilon, non-numeric tokens fall back to exact string equality. Per-problem epsilon can also be stored in the cache and takes precedence over the global default. --- lua/cp/cache.lua | 29 ++++++++++++++++++++++++ lua/cp/config.lua | 10 ++++++++- lua/cp/runner/run.lua | 51 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/lua/cp/cache.lua b/lua/cp/cache.lua index 33342c2..da0f667 100644 --- a/lua/cp/cache.lua +++ b/lua/cp/cache.lua @@ -27,6 +27,7 @@ ---@field multi_test? boolean ---@field memory_mb? number ---@field timeout_ms? number +---@field epsilon? number ---@field combined_test? CombinedTest ---@field test_cases TestCase[] @@ -273,6 +274,34 @@ function M.get_constraints(platform, contest_id, problem_id) return problem_data.timeout_ms, problem_data.memory_mb end +---@param platform string +---@param contest_id string +---@param problem_id? string +---@return number? +function M.get_epsilon(platform, contest_id, problem_id) + vim.validate({ + platform = { platform, 'string' }, + contest_id = { contest_id, 'string' }, + problem_id = { problem_id, { 'string', 'nil' }, true }, + }) + + if + not cache_data[platform] + or not cache_data[platform][contest_id] + or not cache_data[platform][contest_id].index_map + then + return nil + end + + local index = cache_data[platform][contest_id].index_map[problem_id] + if not index then + return nil + end + + local problem_data = cache_data[platform][contest_id].problems[index] + return problem_data and problem_data.epsilon or nil +end + ---@param file_path string ---@return FileState|nil function M.get_file_state(file_path) diff --git a/lua/cp/config.lua b/lua/cp/config.lua index 6cf43d9..4304a31 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -21,6 +21,7 @@ ---@class PanelConfig ---@field diff_modes string[] ---@field max_output_lines integer +---@field epsilon number? ---@class DiffGitConfig ---@field args string[] @@ -174,7 +175,7 @@ M.defaults = { add_test_key = 'ga', save_and_exit_key = 'q', }, - panel = { diff_modes = { 'side-by-side', 'git', 'vim' }, max_output_lines = 50 }, + panel = { diff_modes = { 'side-by-side', 'git', 'vim' }, max_output_lines = 50, epsilon = nil }, diff = { git = { args = { 'diff', '--no-index', '--word-diff=plain', '--word-diff-regex=.', '--no-prefix' }, @@ -368,6 +369,13 @@ function M.setup(user_config) end, 'positive integer', }, + epsilon = { + cfg.ui.panel.epsilon, + function(v) + return v == nil or (type(v) == 'number' and v >= 0) + end, + 'nil or non-negative number', + }, git = { cfg.ui.diff.git, { 'table' } }, git_args = { cfg.ui.diff.git.args, is_string_list, 'string[]' }, width = { diff --git a/lua/cp/runner/run.lua b/lua/cp/runner/run.lua index 4e4a8f6..97ae7ad 100644 --- a/lua/cp/runner/run.lua +++ b/lua/cp/runner/run.lua @@ -19,6 +19,7 @@ ---@class ProblemConstraints ---@field timeout_ms number ---@field memory_mb number +---@field epsilon number? ---@class PanelState ---@field test_cases RanTestCase[] @@ -56,7 +57,8 @@ local function load_constraints_from_cache(platform, contest_id, problem_id) cache.load() local timeout_ms, memory_mb = cache.get_constraints(platform, contest_id, problem_id) if timeout_ms and memory_mb then - return { timeout_ms = timeout_ms, memory_mb = memory_mb } + local epsilon = cache.get_epsilon(platform, contest_id, problem_id) + return { timeout_ms = timeout_ms, memory_mb = memory_mb, epsilon = epsilon } end return nil end @@ -99,6 +101,49 @@ local function build_command(cmd, substitutions) return execute.build_command(cmd, substitutions) end +local function compare_outputs(actual, expected, epsilon) + local norm_actual = normalize_lines(actual) + local norm_expected = normalize_lines(expected) + + if epsilon == nil or epsilon == 0 then + return norm_actual == norm_expected + end + + local actual_lines = vim.split(norm_actual, '\n', { plain = true }) + local expected_lines = vim.split(norm_expected, '\n', { plain = true }) + + if #actual_lines ~= #expected_lines then + return false + end + + for i = 1, #actual_lines do + local a_tokens = vim.split(actual_lines[i], '%s+', { plain = false, trimempty = true }) + local e_tokens = vim.split(expected_lines[i], '%s+', { plain = false, trimempty = true }) + + if #a_tokens ~= #e_tokens then + return false + end + + for j = 1, #a_tokens do + local a_tok, e_tok = a_tokens[j], e_tokens[j] + local a_num = tonumber(a_tok) + local e_num = tonumber(e_tok) + + if a_num ~= nil and e_num ~= nil then + if math.abs(a_num - e_num) > epsilon then + return false + end + else + if a_tok ~= e_tok then + return false + end + end + end + end + + return true +end + ---@param test_case RanTestCase ---@param debug boolean? ---@param on_complete fun(result: { status: "pass"|"fail"|"tle"|"mle", actual: string, actual_highlights: Highlight[], error: string, stderr: string, time_ms: number, code: integer, ok: boolean, signal: string?, tled: boolean, mled: boolean, rss_mb: number }) @@ -143,7 +188,9 @@ local function run_single_test_case(test_case, debug, on_complete) end local expected = test_case.expected or '' - local ok = normalize_lines(out) == normalize_lines(expected) + local epsilon = (panel_state.constraints and panel_state.constraints.epsilon) + or config.ui.panel.epsilon + local ok = compare_outputs(out, expected, epsilon) local signal = r.signal if not signal and r.code and r.code >= 128 then From 24b088e8e9e71a484c0a9f36226e76471966d1e9 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 26 Feb 2026 23:00:38 -0500 Subject: [PATCH 42/86] style(runner): add luacats to compare_outputs --- lua/cp/runner/run.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lua/cp/runner/run.lua b/lua/cp/runner/run.lua index 97ae7ad..042baf2 100644 --- a/lua/cp/runner/run.lua +++ b/lua/cp/runner/run.lua @@ -101,6 +101,10 @@ local function build_command(cmd, substitutions) return execute.build_command(cmd, substitutions) end +---@param actual string +---@param expected string +---@param epsilon number? +---@return boolean local function compare_outputs(actual, expected, epsilon) local norm_actual = normalize_lines(actual) local norm_expected = normalize_lines(expected) From d3324aafa359621945b584d38312ebd80e8761e7 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 2 Mar 2026 23:44:36 -0500 Subject: [PATCH 43/86] fix(config): propagate template through platform overrides Problem: CpPlatformOverrides lacked a template field and merge_lang() never copied ov.template into the effective language config, so per-platform template overrides were silently dropped. Solution: add template? to CpPlatformOverrides and forward it in merge_lang(), matching how extension is handled. --- lua/cp/config.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lua/cp/config.lua b/lua/cp/config.lua index 4304a31..fcd1e96 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -12,6 +12,7 @@ ---@class CpPlatformOverrides ---@field extension? string ---@field commands? CpLangCommands +---@field template? string ---@class CpPlatform ---@field enabled_languages string[] @@ -259,6 +260,9 @@ local function merge_lang(base, ov) if ov.commands then out.commands = vim.tbl_deep_extend('force', out.commands or {}, ov.commands or {}) end + if ov.template then + out.template = ov.template + end return out end From 6a395af98fab9ae11fe788d8ceaf24c3aac50b87 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 00:09:57 -0500 Subject: [PATCH 44/86] feat(config): add templates.cursor_marker for post-template cursor placement Problem: after apply_template writes a file's content to the buffer, cursor positioning was left entirely to the user's setup_code hook, forcing everyone to reimplement the same placeholder-stripping logic. Solution: add an optional templates.cursor_marker config key. When set, apply_template scans the written lines for the marker, strips it, and positions the cursor there via bufwinid so it works in both the provisional and existing-file paths. --- lua/cp/config.lua | 11 +++++++++++ lua/cp/setup.lua | 15 +++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/lua/cp/config.lua b/lua/cp/config.lua index fcd1e96..280ecd5 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -9,6 +9,9 @@ ---@field commands CpLangCommands ---@field template? string +---@class CpTemplatesConfig +---@field cursor_marker? string + ---@class CpPlatformOverrides ---@field extension? string ---@field commands? CpLangCommands @@ -86,6 +89,7 @@ ---@class cp.Config ---@field languages table ---@field platforms table +---@field templates? CpTemplatesConfig ---@field hooks Hooks ---@field debug boolean ---@field open_url boolean @@ -320,6 +324,13 @@ function M.setup(user_config) error('[cp.nvim] At least one platform must be configured') end + if cfg.templates ~= nil then + vim.validate({ templates = { cfg.templates, 'table' } }) + if cfg.templates.cursor_marker ~= nil then + vim.validate({ cursor_marker = { cfg.templates.cursor_marker, 'string' } }) + end + end + vim.validate({ hooks = { cfg.hooks, { 'table' } }, ui = { cfg.ui, { 'table' } }, diff --git a/lua/cp/setup.lua b/lua/cp/setup.lua index 44c993d..43a50c0 100644 --- a/lua/cp/setup.lua +++ b/lua/cp/setup.lua @@ -21,6 +21,21 @@ local function apply_template(bufnr, lang_id, platform) end local lines = vim.fn.readfile(path) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + local marker = config.templates and config.templates.cursor_marker + if marker then + for lnum, line in ipairs(lines) do + local col = line:find(marker, 1, true) + if col then + local new_line = line:sub(1, col - 1) .. line:sub(col + #marker) + vim.api.nvim_buf_set_lines(bufnr, lnum - 1, lnum, false, { new_line }) + local winid = vim.fn.bufwinid(bufnr) + if winid ~= -1 then + vim.api.nvim_win_set_cursor(winid, { lnum, col - 1 }) + end + break + end + end + end end ---Get the language of the current file from cache From add022af8cda4292e3cee308aad48c6c964b4bb1 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 00:31:42 -0500 Subject: [PATCH 45/86] refactor(hooks): replace flat hooks API with setup/on namespaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: the hooks API conflated distinct lifecycle scopes under a flat table with inconsistent naming (setup_code, before_run, setup_io_input), making it hard to reason about when each hook fires. Solution: introduce two namespaces — hooks.setup.{contest,code,io} for one-time initialization and hooks.on.{enter,run,debug} for recurring events. hooks.setup.contest fires once when a contest dir is newly created; hooks.on.enter is registered as a buffer-scoped BufEnter autocmd and fires immediately after setup.code. The provisional buffer setup_code callsite is removed as it ran on an unresolved temp buffer. --- lua/cp/config.lua | 66 ++++++++++++++++++++++++++++++++++----------- lua/cp/setup.lua | 52 ++++++++++++++++++++++------------- lua/cp/ui/views.lua | 23 +++++++--------- 3 files changed, 95 insertions(+), 46 deletions(-) diff --git a/lua/cp/config.lua b/lua/cp/config.lua index 280ecd5..7ca81eb 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -33,12 +33,23 @@ ---@class DiffConfig ---@field git DiffGitConfig +---@class CpSetupIOHooks +---@field input? fun(bufnr: integer, state: cp.State) +---@field output? fun(bufnr: integer, state: cp.State) + +---@class CpSetupHooks +---@field contest? fun(state: cp.State) +---@field code? fun(state: cp.State) +---@field io? CpSetupIOHooks + +---@class CpOnHooks +---@field enter? fun(state: cp.State) +---@field run? fun(state: cp.State) +---@field debug? fun(state: cp.State) + ---@class Hooks ----@field before_run? fun(state: cp.State) ----@field before_debug? fun(state: cp.State) ----@field setup_code? fun(state: cp.State) ----@field setup_io_input? fun(bufnr: integer, state: cp.State) ----@field setup_io_output? fun(bufnr: integer, state: cp.State) +---@field setup? CpSetupHooks +---@field on? CpOnHooks ---@class VerdictFormatData ---@field index integer @@ -156,11 +167,19 @@ M.defaults = { }, }, hooks = { - before_run = nil, - before_debug = nil, - setup_code = nil, - setup_io_input = helpers.clearcol, - setup_io_output = helpers.clearcol, + setup = { + contest = nil, + code = nil, + io = { + input = helpers.clearcol, + output = helpers.clearcol, + }, + }, + on = { + enter = nil, + run = nil, + debug = nil, + }, }, debug = false, scrapers = constants.PLATFORMS, @@ -352,12 +371,29 @@ function M.setup(user_config) end, ('one of {%s}'):format(table.concat(constants.PLATFORMS, ',')), }, - before_run = { cfg.hooks.before_run, { 'function', 'nil' }, true }, - before_debug = { cfg.hooks.before_debug, { 'function', 'nil' }, true }, - setup_code = { cfg.hooks.setup_code, { 'function', 'nil' }, true }, - setup_io_input = { cfg.hooks.setup_io_input, { 'function', 'nil' }, true }, - setup_io_output = { cfg.hooks.setup_io_output, { 'function', 'nil' }, true }, }) + if cfg.hooks.setup ~= nil then + vim.validate({ setup = { cfg.hooks.setup, 'table' } }) + vim.validate({ + contest = { cfg.hooks.setup.contest, { 'function', 'nil' }, true }, + code = { cfg.hooks.setup.code, { 'function', 'nil' }, true }, + }) + if cfg.hooks.setup.io ~= nil then + vim.validate({ io = { cfg.hooks.setup.io, 'table' } }) + vim.validate({ + input = { cfg.hooks.setup.io.input, { 'function', 'nil' }, true }, + output = { cfg.hooks.setup.io.output, { 'function', 'nil' }, true }, + }) + end + end + if cfg.hooks.on ~= nil then + vim.validate({ on = { cfg.hooks.on, 'table' } }) + vim.validate({ + enter = { cfg.hooks.on.enter, { 'function', 'nil' }, true }, + run = { cfg.hooks.on.run, { 'function', 'nil' }, true }, + debug = { cfg.hooks.on.debug, { 'function', 'nil' }, true }, + }) + end local layouts = require('cp.ui.layouts') vim.validate({ diff --git a/lua/cp/setup.lua b/lua/cp/setup.lua index 43a50c0..46c3dc0 100644 --- a/lua/cp/setup.lua +++ b/lua/cp/setup.lua @@ -196,13 +196,6 @@ function M.setup_contest(platform, contest_id, problem_id, language) state.set_language(lang) - if cfg.hooks and cfg.hooks.setup_code and not vim.b[bufnr].cp_setup_done then - local ok = pcall(cfg.hooks.setup_code, state) - if ok then - vim.b[bufnr].cp_setup_done = true - end - end - state.set_provisional({ bufnr = bufnr, platform = platform, @@ -281,7 +274,15 @@ function M.setup_problem(problem_id, language) return end - vim.fn.mkdir(vim.fn.fnamemodify(source_file, ':h'), 'p') + local contest_dir = vim.fn.fnamemodify(source_file, ':h') + local is_new_dir = vim.fn.isdirectory(contest_dir) == 0 + vim.fn.mkdir(contest_dir, 'p') + if is_new_dir then + local s = config.hooks and config.hooks.setup + if s and s.contest then + pcall(s.contest, state) + end + end local prov = state.get_provisional() if prov and prov.platform == platform and prov.contest_id == (state.get_contest_id() or '') then @@ -302,15 +303,23 @@ function M.setup_problem(problem_id, language) state.set_solution_win(vim.api.nvim_get_current_win()) if not vim.b[prov.bufnr].cp_setup_done then apply_template(prov.bufnr, lang, platform) - if config.hooks and config.hooks.setup_code then - local ok = pcall(config.hooks.setup_code, state) - if ok then - vim.b[prov.bufnr].cp_setup_done = true - end + local s = config.hooks and config.hooks.setup + if s and s.code then + local ok = pcall(s.code, state) + if ok then vim.b[prov.bufnr].cp_setup_done = true end else helpers.clearcol(prov.bufnr) vim.b[prov.bufnr].cp_setup_done = true end + local o = config.hooks and config.hooks.on + if o and o.enter then + local bufnr = prov.bufnr + vim.api.nvim_create_autocmd('BufEnter', { + buffer = bufnr, + callback = function() pcall(o.enter, state) end, + }) + pcall(o.enter, state) + end end cache.set_file_state( vim.fn.fnamemodify(source_file, ':p'), @@ -339,15 +348,22 @@ function M.setup_problem(problem_id, language) if is_new then apply_template(bufnr, lang, platform) end - if config.hooks and config.hooks.setup_code then - local ok = pcall(config.hooks.setup_code, state) - if ok then - vim.b[bufnr].cp_setup_done = true - end + local s = config.hooks and config.hooks.setup + if s and s.code then + local ok = pcall(s.code, state) + if ok then vim.b[bufnr].cp_setup_done = true end else helpers.clearcol(bufnr) vim.b[bufnr].cp_setup_done = true end + local o = config.hooks and config.hooks.on + if o and o.enter then + vim.api.nvim_create_autocmd('BufEnter', { + buffer = bufnr, + callback = function() pcall(o.enter, state) end, + }) + pcall(o.enter, state) + end end cache.set_file_state( vim.fn.expand('%:p'), diff --git a/lua/cp/ui/views.lua b/lua/cp/ui/views.lua index 2d67569..c6d1281 100644 --- a/lua/cp/ui/views.lua +++ b/lua/cp/ui/views.lua @@ -444,12 +444,12 @@ function M.ensure_io_view() local cfg = config_module.get_config() - if cfg.hooks and cfg.hooks.setup_io_output then - pcall(cfg.hooks.setup_io_output, output_buf, state) + local io = cfg.hooks and cfg.hooks.setup and cfg.hooks.setup.io + if io and io.output then + pcall(io.output, output_buf, state) end - - if cfg.hooks and cfg.hooks.setup_io_input then - pcall(cfg.hooks.setup_io_input, input_buf, state) + if io and io.input then + pcall(io.input, input_buf, state) end local test_cases = cache.get_test_cases(platform, contest_id, problem_id) @@ -958,15 +958,12 @@ function M.toggle_panel(panel_opts) setup_keybindings_for_buffer(test_buffers.tab_buf) - if config.hooks and config.hooks.before_run then - vim.schedule_wrap(function() - config.hooks.before_run(state) - end) + local o = config.hooks and config.hooks.on + if o and o.run then + vim.schedule(function() o.run(state) end) end - if panel_opts and panel_opts.debug and config.hooks and config.hooks.before_debug then - vim.schedule_wrap(function() - config.hooks.before_debug(state) - end) + if panel_opts and panel_opts.debug and o and o.debug then + vim.schedule(function() o.debug(state) end) end vim.api.nvim_set_current_win(test_windows.tab_win) From 0e88e0f1827ef508bb8599aa1d3a9943f669e437 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 00:43:43 -0500 Subject: [PATCH 46/86] fix(utils): skip uv python setup on NixOS Problem: uv downloads glibc-linked Python binaries that NixOS cannot run, causing setup_python_env to fail with exit status 127. Solution: detect NixOS via /etc/NIXOS and bypass the uv sync path, falling through directly to nix-based Python discovery. --- lua/cp/utils.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/cp/utils.lua b/lua/cp/utils.lua index 285ebf8..d300ee7 100644 --- a/lua/cp/utils.lua +++ b/lua/cp/utils.lua @@ -175,7 +175,9 @@ function M.setup_python_env() return true end - if vim.fn.executable('uv') == 1 then + local on_nixos = vim.fn.filereadable('/etc/NIXOS') == 1 + + if not on_nixos and vim.fn.executable('uv') == 1 then local plugin_path = M.get_plugin_path() logger.log('Python env: uv sync (dir=' .. plugin_path .. ')') vim.notify('[cp.nvim] Setting up Python environment...', vim.log.levels.INFO) From 72ea6249f4d2c9e259c318ce2c98857ddf8776cc Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 00:45:35 -0500 Subject: [PATCH 47/86] ci: format --- lua/cp/config.lua | 12 ++++++------ lua/cp/setup.lua | 16 ++++++++++++---- lua/cp/ui/views.lua | 8 ++++++-- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/lua/cp/config.lua b/lua/cp/config.lua index 7ca81eb..2dd2b32 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -169,15 +169,15 @@ M.defaults = { hooks = { setup = { contest = nil, - code = nil, + code = nil, io = { - input = helpers.clearcol, + input = helpers.clearcol, output = helpers.clearcol, }, }, on = { enter = nil, - run = nil, + run = nil, debug = nil, }, }, @@ -376,12 +376,12 @@ function M.setup(user_config) vim.validate({ setup = { cfg.hooks.setup, 'table' } }) vim.validate({ contest = { cfg.hooks.setup.contest, { 'function', 'nil' }, true }, - code = { cfg.hooks.setup.code, { 'function', 'nil' }, true }, + code = { cfg.hooks.setup.code, { 'function', 'nil' }, true }, }) if cfg.hooks.setup.io ~= nil then vim.validate({ io = { cfg.hooks.setup.io, 'table' } }) vim.validate({ - input = { cfg.hooks.setup.io.input, { 'function', 'nil' }, true }, + input = { cfg.hooks.setup.io.input, { 'function', 'nil' }, true }, output = { cfg.hooks.setup.io.output, { 'function', 'nil' }, true }, }) end @@ -390,7 +390,7 @@ function M.setup(user_config) vim.validate({ on = { cfg.hooks.on, 'table' } }) vim.validate({ enter = { cfg.hooks.on.enter, { 'function', 'nil' }, true }, - run = { cfg.hooks.on.run, { 'function', 'nil' }, true }, + run = { cfg.hooks.on.run, { 'function', 'nil' }, true }, debug = { cfg.hooks.on.debug, { 'function', 'nil' }, true }, }) end diff --git a/lua/cp/setup.lua b/lua/cp/setup.lua index 46c3dc0..1bef5f5 100644 --- a/lua/cp/setup.lua +++ b/lua/cp/setup.lua @@ -306,7 +306,9 @@ function M.setup_problem(problem_id, language) local s = config.hooks and config.hooks.setup if s and s.code then local ok = pcall(s.code, state) - if ok then vim.b[prov.bufnr].cp_setup_done = true end + if ok then + vim.b[prov.bufnr].cp_setup_done = true + end else helpers.clearcol(prov.bufnr) vim.b[prov.bufnr].cp_setup_done = true @@ -316,7 +318,9 @@ function M.setup_problem(problem_id, language) local bufnr = prov.bufnr vim.api.nvim_create_autocmd('BufEnter', { buffer = bufnr, - callback = function() pcall(o.enter, state) end, + callback = function() + pcall(o.enter, state) + end, }) pcall(o.enter, state) end @@ -351,7 +355,9 @@ function M.setup_problem(problem_id, language) local s = config.hooks and config.hooks.setup if s and s.code then local ok = pcall(s.code, state) - if ok then vim.b[bufnr].cp_setup_done = true end + if ok then + vim.b[bufnr].cp_setup_done = true + end else helpers.clearcol(bufnr) vim.b[bufnr].cp_setup_done = true @@ -360,7 +366,9 @@ function M.setup_problem(problem_id, language) if o and o.enter then vim.api.nvim_create_autocmd('BufEnter', { buffer = bufnr, - callback = function() pcall(o.enter, state) end, + callback = function() + pcall(o.enter, state) + end, }) pcall(o.enter, state) end diff --git a/lua/cp/ui/views.lua b/lua/cp/ui/views.lua index c6d1281..c2c1c31 100644 --- a/lua/cp/ui/views.lua +++ b/lua/cp/ui/views.lua @@ -960,10 +960,14 @@ function M.toggle_panel(panel_opts) local o = config.hooks and config.hooks.on if o and o.run then - vim.schedule(function() o.run(state) end) + vim.schedule(function() + o.run(state) + end) end if panel_opts and panel_opts.debug and o and o.debug then - vim.schedule(function() o.debug(state) end) + vim.schedule(function() + o.debug(state) + end) end vim.api.nvim_set_current_win(test_windows.tab_win) From dc9cb10f3ace4d0ef64dca47d4709c0928d6ec3d Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 00:50:02 -0500 Subject: [PATCH 48/86] doc: update --- doc/cp.nvim.txt | 80 +++++++++++++++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 29 deletions(-) diff --git a/doc/cp.nvim.txt b/doc/cp.nvim.txt index 27563a1..b56db09 100644 --- a/doc/cp.nvim.txt +++ b/doc/cp.nvim.txt @@ -220,38 +220,60 @@ run CSES problems with Rust using the single schema: *cp.Hooks* Fields: ~ - {before_run} (function, optional) Called before test panel opens. - function(state: cp.State) - {before_debug} (function, optional) Called before debug build/run. - function(state: cp.State) - {setup_code} (function, optional) Called after source file is opened. - function(state: cp.State) - {setup_io_input} (function, optional) Called when I/O input buffer created. - function(bufnr: integer, state: cp.State) - Default: helpers.clearcol (removes line numbers/columns) - {setup_io_output} (function, optional) Called when I/O output buffer created. - function(bufnr: integer, state: cp.State) - Default: helpers.clearcol (removes line numbers/columns) + {setup} (|cp.CpSetupHooks|, optional) One-time initialization hooks. + {on} (|cp.CpOnHooks|, optional) Recurring event hooks. - Hook functions receive the cp.nvim state object (|cp.State|). See + *cp.CpSetupHooks* + Fields: ~ + {contest} (function, optional) Called once when a contest directory + is first created (not on subsequent visits). + function(state: cp.State) + {code} (function, optional) Called after the source buffer is + opened for the first time (guarded by cp_setup_done). + function(state: cp.State) + {io} (|cp.CpSetupIOHooks|, optional) I/O buffer hooks. + + *cp.CpSetupIOHooks* + Fields: ~ + {input} (function, optional) Called when the I/O input buffer is + created. function(bufnr: integer, state: cp.State) + Default: helpers.clearcol + {output} (function, optional) Called when the I/O output buffer is + created. function(bufnr: integer, state: cp.State) + Default: helpers.clearcol + + *cp.CpOnHooks* + Fields: ~ + {enter} (function, optional) Called on every BufEnter on the + solution buffer. Registered as a buffer-scoped autocmd and + fired immediately after setup.code. + function(state: cp.State) + {run} (function, optional) Called before the test panel opens. + function(state: cp.State) + {debug} (function, optional) Called before a debug run. + function(state: cp.State) + + All hook functions receive the cp.nvim state object (|cp.State|). See |lua/cp/state.lua| for available methods and fields. - The I/O buffer hooks are called once when the buffers are first created - during problem setup. Use these to customize buffer appearance (e.g., - remove line numbers, set custom options). Access helpers via: ->lua - local helpers = require('cp').helpers -< Example usage: >lua hooks = { - setup_code = function(state) - print("Setting up " .. state.get_base_name()) - print("Source file: " .. state.get_source_file()) - end, - setup_io_input = function(bufnr, state) - vim.api.nvim_set_option_value('number', false, { buf = bufnr }) - end + setup = { + contest = function(state) + local dir = vim.fn.fnamemodify( + state.get_source_file(state.get_language()), ':h') + vim.fn.system({ 'cp', '~/.clang-format', dir .. '/.clang-format' }) + end, + code = function(state) + vim.opt_local.foldmethod = 'marker' + vim.diagnostic.enable(false) + end, + }, + on = { + enter = function(state) vim.opt_local.winbar = '' end, + run = function(state) require('config.lsp').format() end, + }, } < @@ -657,9 +679,9 @@ While in the I/O view buffers, use the configured keymaps to cycle through tests Buffer Customization ~ -Use the setup_io_input and setup_io_output hooks (see |cp.Hooks|) to customize -buffer appearance. By default, line numbers and columns are removed via -helpers.clearcol (see |cp-helpers|). +Use the hooks.setup.io.input and hooks.setup.io.output hooks (see |cp.Hooks|) +to customize buffer appearance. By default, line numbers and columns are +removed via helpers.clearcol (see |cp-helpers|). ============================================================================== VERDICT FORMATTING *cp-verdict-format* From 865e3b59281b43e3b8ee97c4cfd5c6d87f0fd452 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 14:51:37 -0500 Subject: [PATCH 49/86] refactor: rename epsilon to precision in runner Problem: the tolerance field for floating-point comparison was named `epsilon`, which is an implementation detail, not the user-visible concept. Solution: rename to `precision` in run.lua type annotations, internal variables, and comparison logic. --- lua/cp/runner/run.lua | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lua/cp/runner/run.lua b/lua/cp/runner/run.lua index 042baf2..306a1f8 100644 --- a/lua/cp/runner/run.lua +++ b/lua/cp/runner/run.lua @@ -19,7 +19,7 @@ ---@class ProblemConstraints ---@field timeout_ms number ---@field memory_mb number ----@field epsilon number? +---@field precision number? ---@class PanelState ---@field test_cases RanTestCase[] @@ -57,8 +57,8 @@ local function load_constraints_from_cache(platform, contest_id, problem_id) cache.load() local timeout_ms, memory_mb = cache.get_constraints(platform, contest_id, problem_id) if timeout_ms and memory_mb then - local epsilon = cache.get_epsilon(platform, contest_id, problem_id) - return { timeout_ms = timeout_ms, memory_mb = memory_mb, epsilon = epsilon } + local precision = cache.get_precision(platform, contest_id, problem_id) + return { timeout_ms = timeout_ms, memory_mb = memory_mb, precision = precision } end return nil end @@ -103,13 +103,13 @@ end ---@param actual string ---@param expected string ----@param epsilon number? +---@param precision number? ---@return boolean -local function compare_outputs(actual, expected, epsilon) +local function compare_outputs(actual, expected, precision) local norm_actual = normalize_lines(actual) local norm_expected = normalize_lines(expected) - if epsilon == nil or epsilon == 0 then + if precision == nil or precision == 0 then return norm_actual == norm_expected end @@ -134,7 +134,7 @@ local function compare_outputs(actual, expected, epsilon) local e_num = tonumber(e_tok) if a_num ~= nil and e_num ~= nil then - if math.abs(a_num - e_num) > epsilon then + if math.abs(a_num - e_num) > precision then return false end else @@ -192,9 +192,9 @@ local function run_single_test_case(test_case, debug, on_complete) end local expected = test_case.expected or '' - local epsilon = (panel_state.constraints and panel_state.constraints.epsilon) - or config.ui.panel.epsilon - local ok = compare_outputs(out, expected, epsilon) + local precision = (panel_state.constraints and panel_state.constraints.precision) + or config.ui.panel.precision + local ok = compare_outputs(out, expected, precision) local signal = r.signal if not signal and r.code and r.code >= 128 then From 90bd13580bbe75a5c15df78f2527f78cdcf7b921 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 14:51:42 -0500 Subject: [PATCH 50/86] feat(scraper): add precision extraction, start_time, and submit support Problem: problem pages contain floating-point precision requirements and contest start timestamps that were not being extracted or stored. The submit workflow also needed a foundation in the scraper layer. Solution: add extract_precision() to base.py and propagate through all scrapers into cache. Add start_time to ContestSummary and extract it from AtCoder and Codeforces. Add SubmitResult model, abstract submit() method, submit CLI case with get_language_id() resolution, stdin/env_extra support in run_scraper, and a full AtCoder submit implementation; stub the remaining platforms. --- lua/cp/cache.lua | 24 +++++++-- lua/cp/scraper.lua | 27 ++++++++++ lua/cp/setup.lua | 3 +- scrapers/atcoder.py | 112 ++++++++++++++++++++++++++++++++++++++--- scrapers/base.py | 46 ++++++++++++++++- scrapers/codechef.py | 9 +++- scrapers/codeforces.py | 22 ++++++-- scrapers/cses.py | 14 ++++-- scrapers/models.py | 8 +++ 9 files changed, 245 insertions(+), 20 deletions(-) diff --git a/lua/cp/cache.lua b/lua/cp/cache.lua index da0f667..209c319 100644 --- a/lua/cp/cache.lua +++ b/lua/cp/cache.lua @@ -27,7 +27,7 @@ ---@field multi_test? boolean ---@field memory_mb? number ---@field timeout_ms? number ----@field epsilon? number +---@field precision? number ---@field combined_test? CombinedTest ---@field test_cases TestCase[] @@ -231,7 +231,8 @@ function M.set_test_cases( timeout_ms, memory_mb, interactive, - multi_test + multi_test, + precision ) vim.validate({ platform = { platform, 'string' }, @@ -243,6 +244,7 @@ function M.set_test_cases( memory_mb = { memory_mb, { 'number', 'nil' }, true }, interactive = { interactive, { 'boolean', 'nil' }, true }, multi_test = { multi_test, { 'boolean', 'nil' }, true }, + precision = { precision, { 'number', 'nil' }, true }, }) local index = cache_data[platform][contest_id].index_map[problem_id] @@ -253,6 +255,7 @@ function M.set_test_cases( cache_data[platform][contest_id].problems[index].memory_mb = memory_mb cache_data[platform][contest_id].problems[index].interactive = interactive cache_data[platform][contest_id].problems[index].multi_test = multi_test + cache_data[platform][contest_id].problems[index].precision = precision M.save() end @@ -278,7 +281,7 @@ end ---@param contest_id string ---@param problem_id? string ---@return number? -function M.get_epsilon(platform, contest_id, problem_id) +function M.get_precision(platform, contest_id, problem_id) vim.validate({ platform = { platform, 'string' }, contest_id = { contest_id, 'string' }, @@ -299,7 +302,7 @@ function M.get_epsilon(platform, contest_id, problem_id) end local problem_data = cache_data[platform][contest_id].problems[index] - return problem_data and problem_data.epsilon or nil + return problem_data and problem_data.precision or nil end ---@param file_path string @@ -349,11 +352,24 @@ function M.set_contest_summaries(platform, contests) cache_data[platform][contest.id] = cache_data[platform][contest.id] or {} cache_data[platform][contest.id].display_name = contest.display_name cache_data[platform][contest.id].name = contest.name + if contest.start_time then + cache_data[platform][contest.id].start_time = contest.start_time + end end M.save() end +---@param platform string +---@param contest_id string +---@return integer? +function M.get_contest_start_time(platform, contest_id) + if not cache_data[platform] or not cache_data[platform][contest_id] then + return nil + end + return cache_data[platform][contest_id].start_time +end + function M.clear_all() cache_data = {} M.save() diff --git a/lua/cp/scraper.lua b/lua/cp/scraper.lua index 29cc63e..216e27e 100644 --- a/lua/cp/scraper.lua +++ b/lua/cp/scraper.lua @@ -56,6 +56,12 @@ local function run_scraper(platform, subcommand, args, opts) env.PYTHONPATH = '' env.CONDA_PREFIX = '' + if opts and opts.env_extra then + for k, v in pairs(opts.env_extra) do + env[k] = v + end + end + if opts and opts.ndjson then local uv = vim.uv local stdout = uv.new_pipe(false) @@ -126,6 +132,9 @@ local function run_scraper(platform, subcommand, args, opts) end local sysopts = { text = true, timeout = 30000, env = env, cwd = plugin_path } + if opts and opts.stdin then + sysopts.stdin = opts.stdin + end if opts and opts.sync then local result = vim.system(cmd, sysopts):wait() return syshandle(result) @@ -228,6 +237,7 @@ function M.scrape_all_tests(platform, contest_id, callback) memory_mb = ev.memory_mb or 0, interactive = ev.interactive or false, multi_test = ev.multi_test or false, + precision = ev.precision, problem_id = ev.problem_id, }) end @@ -236,4 +246,21 @@ function M.scrape_all_tests(platform, contest_id, callback) }) end +function M.submit(platform, contest_id, problem_id, language, source_code, credentials, callback) + local creds_json = vim.json.encode(credentials) + run_scraper(platform, 'submit', { contest_id, problem_id, language }, { + stdin = source_code, + env_extra = { CP_CREDENTIALS = creds_json }, + on_exit = function(result) + if type(callback) == 'function' then + if result and result.success then + callback(result.data or { success = true }) + else + callback({ success = false, error = result and result.error or 'unknown' }) + end + end + end, + }) +end + return M diff --git a/lua/cp/setup.lua b/lua/cp/setup.lua index 1bef5f5..e96c37f 100644 --- a/lua/cp/setup.lua +++ b/lua/cp/setup.lua @@ -130,7 +130,8 @@ local function start_tests(platform, contest_id, problems) ev.timeout_ms or 0, ev.memory_mb or 0, ev.interactive, - ev.multi_test + ev.multi_test, + ev.precision ) local io_state = state.get_io_view_state() diff --git a/scrapers/atcoder.py b/scrapers/atcoder.py index 1b946dd..9b7fad6 100644 --- a/scrapers/atcoder.py +++ b/scrapers/atcoder.py @@ -14,13 +14,14 @@ from bs4 import BeautifulSoup, Tag from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry -from .base import BaseScraper +from .base import BaseScraper, extract_precision from .models import ( CombinedTest, ContestListResult, ContestSummary, MetadataResult, ProblemSummary, + SubmitResult, TestCase, TestsResult, ) @@ -121,6 +122,23 @@ def _parse_last_page(html: str) -> int: return max(nums) if nums else 1 +def _parse_start_time(tr: Tag) -> int | None: + tds = tr.select("td") + if not tds: + return None + time_el = tds[0].select_one("time.fixtime-full") + if not time_el: + return None + text = time_el.get_text(strip=True) + try: + from datetime import datetime + + dt = datetime.strptime(text, "%Y-%m-%d %H:%M:%S%z") + return int(dt.timestamp()) + except (ValueError, TypeError): + return None + + def _parse_archive_contests(html: str) -> list[ContestSummary]: soup = BeautifulSoup(html, "html.parser") tbody = soup.select_one("table.table-default tbody") or soup.select_one("tbody") @@ -139,7 +157,10 @@ def _parse_archive_contests(html: str) -> list[ContestSummary]: continue cid = m.group(1) name = a.get_text(strip=True) - out.append(ContestSummary(id=cid, name=name, display_name=name)) + start_time = _parse_start_time(tr) + out.append( + ContestSummary(id=cid, name=name, display_name=name, start_time=start_time) + ) return out @@ -169,7 +190,7 @@ def _parse_tasks_list(html: str) -> list[dict[str, str]]: return rows -def _extract_problem_info(html: str) -> tuple[int, float, bool]: +def _extract_problem_info(html: str) -> tuple[int, float, bool, float | None]: soup = BeautifulSoup(html, "html.parser") txt = soup.get_text(" ", strip=True) timeout_ms = 0 @@ -181,9 +202,10 @@ def _extract_problem_info(html: str) -> tuple[int, float, bool]: if ms: memory_mb = float(ms.group(1)) * MIB_TO_MB div = soup.select_one("#problem-statement") - txt = div.get_text(" ", strip=True) if div else soup.get_text(" ", strip=True) - interactive = "This is an interactive" in txt - return timeout_ms, memory_mb, interactive + body = div.get_text(" ", strip=True) if div else soup.get_text(" ", strip=True) + interactive = "This is an interactive" in body + precision = extract_precision(body) + return timeout_ms, memory_mb, interactive, precision def _extract_samples(html: str) -> list[TestCase]: @@ -220,12 +242,13 @@ def _scrape_problem_page_sync(contest_id: str, slug: str) -> dict[str, Any]: tests = _extract_samples(html) except Exception: tests = [] - timeout_ms, memory_mb, interactive = _extract_problem_info(html) + timeout_ms, memory_mb, interactive, precision = _extract_problem_info(html) return { "tests": tests, "timeout_ms": timeout_ms, "memory_mb": memory_mb, "interactive": interactive, + "precision": precision, } @@ -241,14 +264,29 @@ def _to_problem_summaries(rows: list[dict[str, str]]) -> list[ProblemSummary]: return out +async def _fetch_upcoming_contests_async( + client: httpx.AsyncClient, +) -> list[ContestSummary]: + try: + html = await _get_async(client, f"{BASE_URL}/contests/") + return _parse_archive_contests(html) + except Exception: + return [] + + async def _fetch_all_contests_async() -> list[ContestSummary]: async with httpx.AsyncClient( limits=httpx.Limits(max_connections=100, max_keepalive_connections=100), ) as client: + upcoming = await _fetch_upcoming_contests_async(client) first_html = await _get_async(client, ARCHIVE_URL) last = _parse_last_page(first_html) out = _parse_archive_contests(first_html) if last <= 1: + seen = {c.id for c in out} + for c in upcoming: + if c.id not in seen: + out.append(c) return out tasks = [ asyncio.create_task(_get_async(client, f"{ARCHIVE_URL}?page={p}")) @@ -257,6 +295,10 @@ async def _fetch_all_contests_async() -> list[ContestSummary]: for coro in asyncio.as_completed(tasks): html = await coro out.extend(_parse_archive_contests(html)) + seen = {c.id for c in out} + for c in upcoming: + if c.id not in seen: + out.append(c) return out @@ -319,6 +361,7 @@ class AtcoderScraper(BaseScraper): "memory_mb": data.get("memory_mb", 0), "interactive": bool(data.get("interactive")), "multi_test": False, + "precision": data.get("precision"), } ), flush=True, @@ -326,6 +369,61 @@ class AtcoderScraper(BaseScraper): await asyncio.gather(*(emit(r) for r in rows)) + async def submit(self, contest_id: str, problem_id: str, source_code: str, language_id: str, credentials: dict[str, str]) -> SubmitResult: + def _submit_sync() -> SubmitResult: + try: + login_page = _session.get(f"{BASE_URL}/login", headers=HEADERS, timeout=TIMEOUT_SECONDS) + login_page.raise_for_status() + soup = BeautifulSoup(login_page.text, "html.parser") + csrf_input = soup.find("input", {"name": "csrf_token"}) + if not csrf_input: + return SubmitResult(success=False, error="Could not find CSRF token on login page") + csrf_token = csrf_input.get("value", "") + + login_resp = _session.post( + f"{BASE_URL}/login", + data={ + "username": credentials.get("username", ""), + "password": credentials.get("password", ""), + "csrf_token": csrf_token, + }, + headers=HEADERS, + timeout=TIMEOUT_SECONDS, + ) + login_resp.raise_for_status() + + submit_page = _session.get( + f"{BASE_URL}/contests/{contest_id}/submit", + headers=HEADERS, + timeout=TIMEOUT_SECONDS, + ) + submit_page.raise_for_status() + soup = BeautifulSoup(submit_page.text, "html.parser") + csrf_input = soup.find("input", {"name": "csrf_token"}) + if not csrf_input: + return SubmitResult(success=False, error="Could not find CSRF token on submit page") + csrf_token = csrf_input.get("value", "") + + task_screen_name = f"{contest_id}_{problem_id}" + submit_resp = _session.post( + f"{BASE_URL}/contests/{contest_id}/submit", + data={ + "data.TaskScreenName": task_screen_name, + "data.LanguageId": language_id, + "sourceCode": source_code, + "csrf_token": csrf_token, + }, + headers=HEADERS, + timeout=TIMEOUT_SECONDS, + ) + submit_resp.raise_for_status() + + return SubmitResult(success=True, error="", submission_id="", verdict="submitted") + except Exception as e: + return SubmitResult(success=False, error=str(e)) + + return await asyncio.to_thread(_submit_sync) + async def main_async() -> int: if len(sys.argv) < 2: diff --git a/scrapers/base.py b/scrapers/base.py index 4b685d0..6cd1c5a 100644 --- a/scrapers/base.py +++ b/scrapers/base.py @@ -1,8 +1,31 @@ import asyncio +import json +import os +import re import sys from abc import ABC, abstractmethod -from .models import CombinedTest, ContestListResult, MetadataResult, TestsResult +from .language_ids import get_language_id +from .models import CombinedTest, ContestListResult, MetadataResult, SubmitResult, TestsResult + +_PRECISION_ABS_REL_RE = re.compile( + r"(?:absolute|relative)\s+error[^.]*?10\s*[\^{]\s*\{?\s*[-\u2212]\s*(\d+)\s*\}?", + re.IGNORECASE, +) +_PRECISION_DECIMAL_RE = re.compile( + r"round(?:ed)?\s+to\s+(\d+)\s+decimal\s+place", + re.IGNORECASE, +) + + +def extract_precision(text: str) -> float | None: + m = _PRECISION_ABS_REL_RE.search(text) + if m: + return 10 ** -int(m.group(1)) + m = _PRECISION_DECIMAL_RE.search(text) + if m: + return 10 ** -int(m.group(1)) + return None class BaseScraper(ABC): @@ -19,6 +42,9 @@ class BaseScraper(ABC): @abstractmethod async def stream_tests_for_category_async(self, category_id: str) -> None: ... + @abstractmethod + async def submit(self, contest_id: str, problem_id: str, source_code: str, language_id: str, credentials: dict[str, str]) -> SubmitResult: ... + def _usage(self) -> str: name = self.platform_name return f"Usage: {name}.py metadata | tests | contests" @@ -40,6 +66,9 @@ class BaseScraper(ABC): def _contests_error(self, msg: str) -> ContestListResult: return ContestListResult(success=False, error=msg) + def _submit_error(self, msg: str) -> SubmitResult: + return SubmitResult(success=False, error=msg) + async def _run_cli_async(self, args: list[str]) -> int: if len(args) < 2: print(self._metadata_error(self._usage()).model_dump_json()) @@ -71,6 +100,21 @@ class BaseScraper(ABC): print(result.model_dump_json()) return 0 if result.success else 1 + case "submit": + if len(args) != 5: + print(self._submit_error("Usage: submit ").model_dump_json()) + return 1 + source_code = sys.stdin.read() + creds_raw = os.environ.get("CP_CREDENTIALS", "{}") + try: + credentials = json.loads(creds_raw) + except json.JSONDecodeError: + credentials = {} + language_id = get_language_id(self.platform_name, args[4]) or args[4] + result = await self.submit(args[2], args[3], source_code, language_id, credentials) + print(result.model_dump_json()) + return 0 if result.success else 1 + case _: print( self._metadata_error( diff --git a/scrapers/codechef.py b/scrapers/codechef.py index 46c2a1c..0e53f26 100644 --- a/scrapers/codechef.py +++ b/scrapers/codechef.py @@ -8,12 +8,13 @@ from typing import Any import httpx from curl_cffi import requests as curl_requests -from .base import BaseScraper +from .base import BaseScraper, extract_precision from .models import ( ContestListResult, ContestSummary, MetadataResult, ProblemSummary, + SubmitResult, TestCase, ) @@ -219,11 +220,13 @@ class CodeChefScraper(BaseScraper): ) memory_mb = _extract_memory_limit(html) interactive = False + precision = extract_precision(html) except Exception: tests = [] timeout_ms = 1000 memory_mb = 256.0 interactive = False + precision = None combined_input = "\n".join(t.input for t in tests) if tests else "" combined_expected = ( "\n".join(t.expected for t in tests) if tests else "" @@ -241,6 +244,7 @@ class CodeChefScraper(BaseScraper): "memory_mb": memory_mb, "interactive": interactive, "multi_test": False, + "precision": precision, } tasks = [run_one(problem_code) for problem_code in problems.keys()] @@ -248,6 +252,9 @@ class CodeChefScraper(BaseScraper): payload = await coro print(json.dumps(payload), flush=True) + async def submit(self, contest_id: str, problem_id: str, source_code: str, language_id: str, credentials: dict[str, str]) -> SubmitResult: + return SubmitResult(success=False, error="CodeChef submit not yet implemented", submission_id="", verdict="") + if __name__ == "__main__": CodeChefScraper().run_cli() diff --git a/scrapers/codeforces.py b/scrapers/codeforces.py index a67489f..7863c27 100644 --- a/scrapers/codeforces.py +++ b/scrapers/codeforces.py @@ -9,12 +9,13 @@ import requests from bs4 import BeautifulSoup, Tag from curl_cffi import requests as curl_requests -from .base import BaseScraper +from .base import BaseScraper, extract_precision from .models import ( ContestListResult, ContestSummary, MetadataResult, ProblemSummary, + SubmitResult, TestCase, ) @@ -153,6 +154,7 @@ def _parse_all_blocks(html: str) -> list[dict[str, Any]]: raw_samples, is_grouped = _extract_samples(b) timeout_ms, memory_mb = _extract_limits(b) interactive = _is_interactive(b) + precision = extract_precision(b.get_text(" ", strip=True)) if is_grouped and raw_samples: combined_input = f"{len(raw_samples)}\n" + "\n".join( @@ -179,6 +181,7 @@ def _parse_all_blocks(html: str) -> list[dict[str, Any]]: "memory_mb": memory_mb, "interactive": interactive, "multi_test": is_grouped, + "precision": precision, } ) return out @@ -228,11 +231,20 @@ class CodeforcesScraper(BaseScraper): contests: list[ContestSummary] = [] for c in data["result"]: - if c.get("phase") != "FINISHED": + phase = c.get("phase") + if phase not in ("FINISHED", "BEFORE", "CODING"): continue cid = str(c["id"]) name = c["name"] - contests.append(ContestSummary(id=cid, name=name, display_name=name)) + start_time = c.get("startTimeSeconds") if phase != "FINISHED" else None + contests.append( + ContestSummary( + id=cid, + name=name, + display_name=name, + start_time=start_time, + ) + ) if not contests: return self._contests_error("No contests found") @@ -263,11 +275,15 @@ class CodeforcesScraper(BaseScraper): "memory_mb": b.get("memory_mb", 0), "interactive": bool(b.get("interactive")), "multi_test": bool(b.get("multi_test", False)), + "precision": b.get("precision"), } ), flush=True, ) + async def submit(self, contest_id: str, problem_id: str, source_code: str, language_id: str, credentials: dict[str, str]) -> SubmitResult: + return SubmitResult(success=False, error="Codeforces submit not yet implemented", submission_id="", verdict="") + if __name__ == "__main__": CodeforcesScraper().run_cli() diff --git a/scrapers/cses.py b/scrapers/cses.py index 5440b34..f840238 100644 --- a/scrapers/cses.py +++ b/scrapers/cses.py @@ -7,12 +7,13 @@ from typing import Any import httpx -from .base import BaseScraper +from .base import BaseScraper, extract_precision from .models import ( ContestListResult, ContestSummary, MetadataResult, ProblemSummary, + SubmitResult, TestCase, ) @@ -129,17 +130,21 @@ def parse_category_problems(category_id: str, html: str) -> list[ProblemSummary] return [] -def _extract_problem_info(html: str) -> tuple[int, int, bool]: +def _extract_problem_info(html: str) -> tuple[int, int, bool, float | None]: tm = TIME_RE.search(html) mm = MEM_RE.search(html) t = int(round(float(tm.group(1)) * 1000)) if tm else 0 m = int(mm.group(1)) if mm else 0 md = MD_BLOCK_RE.search(html) interactive = False + precision = None if md: body = md.group(1) interactive = "This is an interactive problem." in body - return t, m, interactive + from bs4 import BeautifulSoup + + precision = extract_precision(BeautifulSoup(body, "html.parser").get_text(" ")) + return t, m, interactive, precision def parse_title(html: str) -> str: @@ -257,6 +262,9 @@ class CSESScraper(BaseScraper): payload = await coro print(json.dumps(payload), flush=True) + async def submit(self, contest_id: str, problem_id: str, source_code: str, language_id: str, credentials: dict[str, str]) -> SubmitResult: + return SubmitResult(success=False, error="CSES submit not yet implemented", submission_id="", verdict="") + if __name__ == "__main__": CSESScraper().run_cli() diff --git a/scrapers/models.py b/scrapers/models.py index be0944d..68de9a9 100644 --- a/scrapers/models.py +++ b/scrapers/models.py @@ -26,6 +26,7 @@ class ContestSummary(BaseModel): id: str name: str display_name: str | None = None + start_time: int | None = None model_config = ConfigDict(extra="forbid") @@ -63,6 +64,13 @@ class TestsResult(ScrapingResult): model_config = ConfigDict(extra="forbid") +class SubmitResult(ScrapingResult): + submission_id: str = "" + verdict: str = "" + + model_config = ConfigDict(extra="forbid") + + class ScraperConfig(BaseModel): timeout_seconds: int = 30 max_retries: int = 3 From 4e8da8488246c667309fe2a41547d3637bc5f9b4 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 14:51:47 -0500 Subject: [PATCH 51/86] feat(platforms): add kattis and usaco scrapers Add KattisScraper and USACOScraper with contest list, metadata, and test case fetching. Register kattis and usaco in PLATFORMS, PLATFORM_DISPLAY_NAMES, and default platform configs. --- lua/cp/config.lua | 20 ++- lua/cp/constants.lua | 6 +- scrapers/kattis.py | 231 ++++++++++++++++++++++++++++++++++ scrapers/usaco.py | 288 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 539 insertions(+), 6 deletions(-) create mode 100644 scrapers/kattis.py create mode 100644 scrapers/usaco.py diff --git a/lua/cp/config.lua b/lua/cp/config.lua index 2dd2b32..fcebc0e 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -25,7 +25,7 @@ ---@class PanelConfig ---@field diff_modes string[] ---@field max_output_lines integer ----@field epsilon number? +---@field precision number? ---@class DiffGitConfig ---@field args string[] @@ -165,6 +165,14 @@ M.defaults = { enabled_languages = { 'cpp', 'python' }, default_language = 'cpp', }, + kattis = { + enabled_languages = { 'cpp', 'python' }, + default_language = 'cpp', + }, + usaco = { + enabled_languages = { 'cpp', 'python' }, + default_language = 'cpp', + }, }, hooks = { setup = { @@ -199,7 +207,11 @@ M.defaults = { add_test_key = 'ga', save_and_exit_key = 'q', }, - panel = { diff_modes = { 'side-by-side', 'git', 'vim' }, max_output_lines = 50, epsilon = nil }, + panel = { + diff_modes = { 'side-by-side', 'git', 'vim' }, + max_output_lines = 50, + precision = nil, + }, diff = { git = { args = { 'diff', '--no-index', '--word-diff=plain', '--word-diff-regex=.', '--no-prefix' }, @@ -420,8 +432,8 @@ function M.setup(user_config) end, 'positive integer', }, - epsilon = { - cfg.ui.panel.epsilon, + precision = { + cfg.ui.panel.precision, function(v) return v == nil or (type(v) == 'number' and v >= 0) end, diff --git a/lua/cp/constants.lua b/lua/cp/constants.lua index 7bdaa16..c4bac3e 100644 --- a/lua/cp/constants.lua +++ b/lua/cp/constants.lua @@ -1,13 +1,15 @@ local M = {} -M.PLATFORMS = { 'atcoder', 'codechef', 'codeforces', 'cses' } -M.ACTIONS = { 'run', 'panel', 'next', 'prev', 'pick', 'cache', 'interact', 'edit' } +M.PLATFORMS = { 'atcoder', 'codechef', 'codeforces', 'cses', 'kattis', 'usaco' } +M.ACTIONS = { 'run', 'panel', 'next', 'prev', 'pick', 'cache', 'interact', 'edit', 'race', 'stress', 'submit' } M.PLATFORM_DISPLAY_NAMES = { atcoder = 'AtCoder', codechef = 'CodeChef', codeforces = 'CodeForces', cses = 'CSES', + kattis = 'Kattis', + usaco = 'USACO', } M.CPP = 'cpp' diff --git a/scrapers/kattis.py b/scrapers/kattis.py new file mode 100644 index 0000000..c98dc35 --- /dev/null +++ b/scrapers/kattis.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 + +import asyncio +import io +import json +import re +import zipfile + +import httpx + +from .base import BaseScraper +from .models import ( + ContestListResult, + ContestSummary, + MetadataResult, + ProblemSummary, + SubmitResult, + TestCase, +) + +BASE_URL = "https://open.kattis.com" +HEADERS = { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" +} +TIMEOUT_S = 15.0 +CONNECTIONS = 8 + +TIME_RE = re.compile( + r"CPU Time limit\s*]*>\s*(\d+)\s*seconds?\s*", + re.DOTALL, +) +MEM_RE = re.compile( + r"Memory limit\s*]*>\s*(\d+)\s*MB\s*", + re.DOTALL, +) +LAST_PAGE_RE = re.compile(r"\bpage=(\d+)") + + +async def _fetch_text(client: httpx.AsyncClient, url: str) -> str: + r = await client.get(url, headers=HEADERS, timeout=TIMEOUT_S) + r.raise_for_status() + return r.text + + +async def _fetch_bytes(client: httpx.AsyncClient, url: str) -> bytes: + r = await client.get(url, headers=HEADERS, timeout=TIMEOUT_S) + r.raise_for_status() + return r.content + + +def _parse_limits(html: str) -> tuple[int, int]: + tm = TIME_RE.search(html) + mm = MEM_RE.search(html) + timeout_ms = int(tm.group(1)) * 1000 if tm else 1000 + memory_mb = int(mm.group(1)) if mm else 1024 + return timeout_ms, memory_mb + + +def _parse_samples_html(html: str) -> list[TestCase]: + tests: list[TestCase] = [] + tables = re.finditer(r']*>.*?', html, re.DOTALL) + for table_match in tables: + table_html = table_match.group(0) + pres = re.findall(r"
(.*?)
", table_html, re.DOTALL) + if len(pres) >= 2: + inp = pres[0].strip() + out = pres[1].strip() + tests.append(TestCase(input=inp, expected=out)) + return tests + + +def _parse_samples_zip(data: bytes) -> list[TestCase]: + try: + zf = zipfile.ZipFile(io.BytesIO(data)) + except zipfile.BadZipFile: + return [] + inputs: dict[str, str] = {} + outputs: dict[str, str] = {} + for name in zf.namelist(): + content = zf.read(name).decode("utf-8").strip() + if name.endswith(".in"): + key = name[: -len(".in")] + inputs[key] = content + elif name.endswith(".ans"): + key = name[: -len(".ans")] + outputs[key] = content + tests: list[TestCase] = [] + for key in sorted(set(inputs) & set(outputs)): + tests.append(TestCase(input=inputs[key], expected=outputs[key])) + return tests + + +def _is_interactive(html: str) -> bool: + return "This is an interactive problem" in html + + +def _parse_problem_rows(html: str) -> list[tuple[str, str]]: + seen: set[str] = set() + out: list[tuple[str, str]] = [] + for m in re.finditer( + r'\s*([^<]+)', + html, + ): + pid = m.group(1) + name = m.group(2).strip() + if pid not in seen: + seen.add(pid) + out.append((pid, name)) + return out + + +def _parse_last_page(html: str) -> int: + nums = [int(m.group(1)) for m in LAST_PAGE_RE.finditer(html)] + return max(nums) if nums else 0 + + +class KattisScraper(BaseScraper): + @property + def platform_name(self) -> str: + return "kattis" + + async def scrape_contest_metadata(self, contest_id: str) -> MetadataResult: + try: + async with httpx.AsyncClient() as client: + html = await _fetch_text(client, f"{BASE_URL}/problems/{contest_id}") + timeout_ms, memory_mb = _parse_limits(html) + title_m = re.search(r"([^<]+)", html) + name = ( + title_m.group(1).split("\u2013")[0].strip() if title_m else contest_id + ) + return MetadataResult( + success=True, + error="", + contest_id=contest_id, + problems=[ProblemSummary(id=contest_id, name=name)], + url=f"{BASE_URL}/problems/%s", + ) + except Exception as e: + return self._metadata_error(str(e)) + + async def scrape_contest_list(self) -> ContestListResult: + try: + async with httpx.AsyncClient( + limits=httpx.Limits(max_connections=CONNECTIONS) + ) as client: + first_html = await _fetch_text( + client, f"{BASE_URL}/problems?page=0&order=problem_difficulty" + ) + last = _parse_last_page(first_html) + rows = _parse_problem_rows(first_html) + + sem = asyncio.Semaphore(CONNECTIONS) + + async def fetch_page(page: int) -> list[tuple[str, str]]: + async with sem: + html = await _fetch_text( + client, + f"{BASE_URL}/problems?page={page}&order=problem_difficulty", + ) + return _parse_problem_rows(html) + + tasks = [fetch_page(p) for p in range(1, last + 1)] + for coro in asyncio.as_completed(tasks): + rows.extend(await coro) + + seen: set[str] = set() + contests: list[ContestSummary] = [] + for pid, name in rows: + if pid not in seen: + seen.add(pid) + contests.append( + ContestSummary(id=pid, name=name, display_name=name) + ) + if not contests: + return self._contests_error("No problems found") + return ContestListResult(success=True, error="", contests=contests) + except Exception as e: + return self._contests_error(str(e)) + + async def stream_tests_for_category_async(self, category_id: str) -> None: + async with httpx.AsyncClient( + limits=httpx.Limits(max_connections=CONNECTIONS) + ) as client: + try: + html = await _fetch_text(client, f"{BASE_URL}/problems/{category_id}") + except Exception: + return + + timeout_ms, memory_mb = _parse_limits(html) + interactive = _is_interactive(html) + + tests: list[TestCase] = [] + try: + zip_data = await _fetch_bytes( + client, + f"{BASE_URL}/problems/{category_id}/file/statement/samples.zip", + ) + tests = _parse_samples_zip(zip_data) + except Exception: + tests = _parse_samples_html(html) + + combined_input = "\n".join(t.input for t in tests) if tests else "" + combined_expected = "\n".join(t.expected for t in tests) if tests else "" + + print( + json.dumps( + { + "problem_id": category_id, + "combined": { + "input": combined_input, + "expected": combined_expected, + }, + "tests": [ + {"input": t.input, "expected": t.expected} for t in tests + ], + "timeout_ms": timeout_ms, + "memory_mb": memory_mb, + "interactive": interactive, + "multi_test": False, + } + ), + flush=True, + ) + + + async def submit(self, contest_id: str, problem_id: str, source_code: str, language_id: str, credentials: dict[str, str]) -> SubmitResult: + return SubmitResult(success=False, error="Kattis submit not yet implemented", submission_id="", verdict="") + + +if __name__ == "__main__": + KattisScraper().run_cli() diff --git a/scrapers/usaco.py b/scrapers/usaco.py new file mode 100644 index 0000000..b8c6c9f --- /dev/null +++ b/scrapers/usaco.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python3 + +import asyncio +import json +import re +from typing import Any + +import httpx + +from .base import BaseScraper +from .models import ( + ContestListResult, + ContestSummary, + MetadataResult, + ProblemSummary, + SubmitResult, + TestCase, +) + +BASE_URL = "http://www.usaco.org" +HEADERS = { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" +} +TIMEOUT_S = 15.0 +CONNECTIONS = 4 + +MONTHS = [ + "dec", + "jan", + "feb", + "mar", + "open", +] + +DIVISION_HEADING_RE = re.compile( + r"

.*?USACO\s+(\d{4})\s+(\w+)\s+Contest,\s+(\w+)\s*

", + re.IGNORECASE, +) +PROBLEM_BLOCK_RE = re.compile( + r"([^<]+)\s*.*?" + r"viewproblem2&cpid=(\d+)", + re.DOTALL, +) +SAMPLE_IN_RE = re.compile(r"(.*?)", re.DOTALL) +SAMPLE_OUT_RE = re.compile(r"(.*?)", re.DOTALL) +TIME_NOTE_RE = re.compile( + r"time\s+limit\s+(?:for\s+this\s+problem\s+is\s+)?(\d+)s", + re.IGNORECASE, +) +MEMORY_NOTE_RE = re.compile( + r"memory\s+limit\s+(?:for\s+this\s+problem\s+is\s+)?(\d+)\s*MB", + re.IGNORECASE, +) +RESULTS_PAGE_RE = re.compile( + r'href="index\.php\?page=([a-z]+\d{2,4}results)"', + re.IGNORECASE, +) + + +async def _fetch_text(client: httpx.AsyncClient, url: str) -> str: + r = await client.get(url, headers=HEADERS, timeout=TIMEOUT_S, follow_redirects=True) + r.raise_for_status() + return r.text + + +def _parse_results_page(html: str) -> dict[str, list[tuple[str, str]]]: + sections: dict[str, list[tuple[str, str]]] = {} + current_div: str | None = None + + parts = re.split(r"(

.*?

)", html, flags=re.DOTALL) + for part in parts: + heading_m = DIVISION_HEADING_RE.search(part) + if heading_m: + current_div = heading_m.group(3).lower() + sections.setdefault(current_div, []) + continue + if current_div is not None: + for m in PROBLEM_BLOCK_RE.finditer(part): + name = m.group(1).strip() + cpid = m.group(2) + sections[current_div].append((cpid, name)) + + return sections + + +def _parse_contest_id(contest_id: str) -> tuple[str, str]: + parts = contest_id.rsplit("_", 1) + if len(parts) != 2: + return contest_id, "" + return parts[0], parts[1].lower() + + +def _results_page_slug(month_year: str) -> str: + return f"{month_year}results" + + +def _parse_problem_page(html: str) -> dict[str, Any]: + inputs = SAMPLE_IN_RE.findall(html) + outputs = SAMPLE_OUT_RE.findall(html) + tests: list[TestCase] = [] + for inp, out in zip(inputs, outputs): + tests.append( + TestCase( + input=inp.strip().replace("\r", ""), + expected=out.strip().replace("\r", ""), + ) + ) + + tm = TIME_NOTE_RE.search(html) + mm = MEMORY_NOTE_RE.search(html) + timeout_ms = int(tm.group(1)) * 1000 if tm else 4000 + memory_mb = int(mm.group(1)) if mm else 256 + + interactive = "interactive problem" in html.lower() + + return { + "tests": tests, + "timeout_ms": timeout_ms, + "memory_mb": memory_mb, + "interactive": interactive, + } + + +class USACOScraper(BaseScraper): + @property + def platform_name(self) -> str: + return "usaco" + + async def scrape_contest_metadata(self, contest_id: str) -> MetadataResult: + try: + month_year, division = _parse_contest_id(contest_id) + if not division: + return self._metadata_error( + f"Invalid contest ID '{contest_id}'. " + "Expected format: _ (e.g. dec24_gold)" + ) + + slug = _results_page_slug(month_year) + async with httpx.AsyncClient() as client: + html = await _fetch_text(client, f"{BASE_URL}/index.php?page={slug}") + sections = _parse_results_page(html) + problems_raw = sections.get(division, []) + if not problems_raw: + return self._metadata_error( + f"No problems found for {contest_id} (division: {division})" + ) + problems = [ + ProblemSummary(id=cpid, name=name) for cpid, name in problems_raw + ] + return MetadataResult( + success=True, + error="", + contest_id=contest_id, + problems=problems, + url=f"{BASE_URL}/index.php?page=viewproblem2&cpid=%s", + ) + except Exception as e: + return self._metadata_error(str(e)) + + async def scrape_contest_list(self) -> ContestListResult: + try: + async with httpx.AsyncClient( + limits=httpx.Limits(max_connections=CONNECTIONS) + ) as client: + html = await _fetch_text(client, f"{BASE_URL}/index.php?page=contests") + + page_slugs: set[str] = set() + for m in RESULTS_PAGE_RE.finditer(html): + page_slugs.add(m.group(1)) + + recent_patterns = [] + for year in range(15, 27): + for month in MONTHS: + recent_patterns.append(f"{month}{year:02d}results") + page_slugs.update(recent_patterns) + + contests: list[ContestSummary] = [] + sem = asyncio.Semaphore(CONNECTIONS) + + async def check_page(slug: str) -> list[ContestSummary]: + async with sem: + try: + page_html = await _fetch_text( + client, f"{BASE_URL}/index.php?page={slug}" + ) + except Exception: + return [] + sections = _parse_results_page(page_html) + if not sections: + return [] + month_year = slug.replace("results", "") + out: list[ContestSummary] = [] + for div in sections: + cid = f"{month_year}_{div}" + year_m = re.search(r"\d{2,4}", month_year) + month_m = re.search(r"[a-z]+", month_year) + year_str = year_m.group() if year_m else "" + month_str = month_m.group().capitalize() if month_m else "" + if len(year_str) == 2: + year_str = f"20{year_str}" + display = ( + f"USACO {year_str} {month_str} - {div.capitalize()}" + ) + out.append( + ContestSummary(id=cid, name=cid, display_name=display) + ) + return out + + tasks = [check_page(slug) for slug in sorted(page_slugs)] + for coro in asyncio.as_completed(tasks): + contests.extend(await coro) + + if not contests: + return self._contests_error("No contests found") + return ContestListResult(success=True, error="", contests=contests) + except Exception as e: + return self._contests_error(str(e)) + + async def stream_tests_for_category_async(self, category_id: str) -> None: + month_year, division = _parse_contest_id(category_id) + if not division: + return + + slug = _results_page_slug(month_year) + async with httpx.AsyncClient( + limits=httpx.Limits(max_connections=CONNECTIONS) + ) as client: + try: + html = await _fetch_text(client, f"{BASE_URL}/index.php?page={slug}") + except Exception: + return + + sections = _parse_results_page(html) + problems_raw = sections.get(division, []) + if not problems_raw: + return + + sem = asyncio.Semaphore(CONNECTIONS) + + async def run_one(cpid: str) -> dict[str, Any]: + async with sem: + try: + problem_html = await _fetch_text( + client, + f"{BASE_URL}/index.php?page=viewproblem2&cpid={cpid}", + ) + info = _parse_problem_page(problem_html) + except Exception: + info = { + "tests": [], + "timeout_ms": 4000, + "memory_mb": 256, + "interactive": False, + } + + tests: list[TestCase] = info["tests"] + combined_input = "\n".join(t.input for t in tests) if tests else "" + combined_expected = ( + "\n".join(t.expected for t in tests) if tests else "" + ) + + return { + "problem_id": cpid, + "combined": { + "input": combined_input, + "expected": combined_expected, + }, + "tests": [ + {"input": t.input, "expected": t.expected} for t in tests + ], + "timeout_ms": info["timeout_ms"], + "memory_mb": info["memory_mb"], + "interactive": info["interactive"], + "multi_test": False, + } + + tasks = [run_one(cpid) for cpid, _ in problems_raw] + for coro in asyncio.as_completed(tasks): + payload = await coro + print(json.dumps(payload), flush=True) + + + async def submit(self, contest_id: str, problem_id: str, source_code: str, language_id: str, credentials: dict[str, str]) -> SubmitResult: + return SubmitResult(success=False, error="USACO submit not yet implemented", submission_id="", verdict="") + + +if __name__ == "__main__": + USACOScraper().run_cli() From f5c1b978a2ff1ce672bb9c6548815a17e96fe3cd Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 14:51:51 -0500 Subject: [PATCH 52/86] feat(race): add contest countdown timer Add race.lua with a 1-second vim.uv timer that counts down to a contest start time and auto-calls setup.setup_contest() at T=0. Exposes M.start(), M.stop(), and M.status() for command dispatch and statusline integration. --- lua/cp/race.lua | 143 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 lua/cp/race.lua diff --git a/lua/cp/race.lua b/lua/cp/race.lua new file mode 100644 index 0000000..689ed41 --- /dev/null +++ b/lua/cp/race.lua @@ -0,0 +1,143 @@ +local M = {} + +local cache = require('cp.cache') +local constants = require('cp.constants') +local logger = require('cp.log') +local scraper = require('cp.scraper') + +local race_state = { + timer = nil, + platform = nil, + contest_id = nil, + language = nil, + start_time = nil, +} + +local function format_countdown(seconds) + local h = math.floor(seconds / 3600) + local m = math.floor((seconds % 3600) / 60) + local s = seconds % 60 + return string.format('%02d:%02d:%02d', h, m, s) +end + +function M.start(platform, contest_id, language) + if not platform or not vim.tbl_contains(constants.PLATFORMS, platform) then + logger.log('Invalid platform', vim.log.levels.ERROR) + return + end + if not contest_id or contest_id == '' then + logger.log('Contest ID required', vim.log.levels.ERROR) + return + end + if race_state.timer then + logger.log('Race already active. Use :CP race stop first.', vim.log.levels.WARN) + return + end + + cache.load() + local start_time = cache.get_contest_start_time(platform, contest_id) + + if not start_time then + logger.log('Fetching contest list...', vim.log.levels.INFO, true) + local contests = scraper.scrape_contest_list(platform) + if contests and #contests > 0 then + cache.set_contest_summaries(platform, contests) + start_time = cache.get_contest_start_time(platform, contest_id) + end + end + + if not start_time then + logger.log( + ('No start time found for %s contest %s'):format( + constants.PLATFORM_DISPLAY_NAMES[platform] or platform, + contest_id + ), + vim.log.levels.ERROR + ) + return + end + + local remaining = start_time - os.time() + if remaining <= 0 then + logger.log('Contest has already started, setting up...', vim.log.levels.INFO, true) + require('cp.setup').setup_contest(platform, contest_id, nil, language) + return + end + + race_state.platform = platform + race_state.contest_id = contest_id + race_state.language = language + race_state.start_time = start_time + + logger.log( + ('Race started for %s %s — %s remaining'):format( + constants.PLATFORM_DISPLAY_NAMES[platform] or platform, + contest_id, + format_countdown(remaining) + ), + vim.log.levels.INFO, + true + ) + + local timer = vim.uv.new_timer() + race_state.timer = timer + timer:start( + 1000, + 1000, + vim.schedule_wrap(function() + local r = race_state.start_time - os.time() + if r <= 0 then + timer:stop() + timer:close() + race_state.timer = nil + local p = race_state.platform + local c = race_state.contest_id + local l = race_state.language + race_state.platform = nil + race_state.contest_id = nil + race_state.language = nil + race_state.start_time = nil + logger.log('Contest started!', vim.log.levels.INFO, true) + require('cp.setup').setup_contest(p, c, nil, l) + else + vim.notify( + ('[cp.nvim] %s %s — %s'):format( + constants.PLATFORM_DISPLAY_NAMES[race_state.platform] or race_state.platform, + race_state.contest_id, + format_countdown(r) + ), + vim.log.levels.INFO + ) + end + end) + ) +end + +function M.stop() + if not race_state.timer then + logger.log('No active race', vim.log.levels.WARN) + return + end + race_state.timer:stop() + race_state.timer:close() + race_state.timer = nil + race_state.platform = nil + race_state.contest_id = nil + race_state.language = nil + race_state.start_time = nil + logger.log('Race cancelled', vim.log.levels.INFO, true) +end + +function M.status() + if not race_state.timer or not race_state.start_time then + return { active = false } + end + return { + active = true, + platform = race_state.platform, + contest_id = race_state.contest_id, + remaining_seconds = math.max(0, race_state.start_time - os.time()), + } +end + +return M From 39b7b3d83f8269b1e83b00507e3091054ea8badb Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 14:51:56 -0500 Subject: [PATCH 53/86] feat(stress): add stress test loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add stress.lua that auto-detects or accepts generator and brute solution files, compiles C++ if needed, and launches scripts/stress.py in a terminal buffer with session save/restore and cleanup autocmds. Add scripts/stress.py as a standalone loop that runs generator → brute → candidate, comparing outputs and exiting on the first mismatch. --- lua/cp/stress.lua | 235 ++++++++++++++++++++++++++++++++++++++++++++++ scripts/stress.py | 107 +++++++++++++++++++++ 2 files changed, 342 insertions(+) create mode 100644 lua/cp/stress.lua create mode 100755 scripts/stress.py diff --git a/lua/cp/stress.lua b/lua/cp/stress.lua new file mode 100644 index 0000000..3e51881 --- /dev/null +++ b/lua/cp/stress.lua @@ -0,0 +1,235 @@ +local M = {} + +local logger = require('cp.log') +local state = require('cp.state') +local utils = require('cp.utils') + +local GENERATOR_PATTERNS = { + 'gen.py', + 'gen.cc', + 'gen.cpp', + 'generator.py', + 'generator.cc', + 'generator.cpp', +} + +local BRUTE_PATTERNS = { + 'brute.py', + 'brute.cc', + 'brute.cpp', + 'slow.py', + 'slow.cc', + 'slow.cpp', +} + +local function find_file(patterns) + for _, pattern in ipairs(patterns) do + if vim.fn.filereadable(pattern) == 1 then + return pattern + end + end + return nil +end + +local function compile_cpp(source, output) + local result = vim.system({ 'sh', '-c', 'g++ -O2 -o ' .. output .. ' ' .. source }):wait() + if result.code ~= 0 then + logger.log( + ('Failed to compile %s: %s'):format(source, result.stderr or ''), + vim.log.levels.ERROR + ) + return false + end + return true +end + +local function build_run_cmd(file) + local ext = file:match('%.([^%.]+)$') + if ext == 'cc' or ext == 'cpp' then + local base = file:gsub('%.[^%.]+$', '') + local bin = base .. '_bin' + if not compile_cpp(file, bin) then + return nil + end + return './' .. bin + elseif ext == 'py' then + return 'python3 ' .. file + end + return './' .. file +end + +function M.toggle(generator_cmd, brute_cmd) + if state.get_active_panel() == 'stress' then + if state.stress_buf and vim.api.nvim_buf_is_valid(state.stress_buf) then + local job = vim.b[state.stress_buf].terminal_job_id + if job then + vim.fn.jobstop(job) + end + end + if state.saved_stress_session then + vim.cmd.source(state.saved_stress_session) + vim.fn.delete(state.saved_stress_session) + state.saved_stress_session = nil + end + state.set_active_panel(nil) + return + end + + if state.get_active_panel() then + logger.log('Another panel is already active.', vim.log.levels.WARN) + return + end + + local gen_file = generator_cmd + local brute_file = brute_cmd + + if not gen_file then + gen_file = find_file(GENERATOR_PATTERNS) + end + if not brute_file then + brute_file = find_file(BRUTE_PATTERNS) + end + + if not gen_file then + logger.log( + 'No generator found. Pass generator as first arg or add gen.{py,cc,cpp}.', + vim.log.levels.ERROR + ) + return + end + if not brute_file then + logger.log( + 'No brute solution found. Pass brute as second arg or add brute.{py,cc,cpp}.', + vim.log.levels.ERROR + ) + return + end + + local gen_cmd = build_run_cmd(gen_file) + if not gen_cmd then + return + end + + local brute_run_cmd = build_run_cmd(brute_file) + if not brute_run_cmd then + return + end + + state.saved_stress_session = vim.fn.tempname() + -- selene: allow(mixed_table) + vim.cmd.mksession({ state.saved_stress_session, bang = true }) + vim.cmd.only({ mods = { silent = true } }) + + local execute = require('cp.runner.execute') + + local function restore_session() + if state.saved_stress_session then + vim.cmd.source(state.saved_stress_session) + vim.fn.delete(state.saved_stress_session) + state.saved_stress_session = nil + end + end + + execute.compile_problem(false, function(compile_result) + if not compile_result.success then + local run = require('cp.runner.run') + run.handle_compilation_failure(compile_result.output) + restore_session() + return + end + + local binary = state.get_binary_file() + if not binary or binary == '' then + logger.log('No binary produced.', vim.log.levels.ERROR) + restore_session() + return + end + + local script = vim.fn.fnamemodify(utils.get_plugin_path() .. '/scripts/stress.py', ':p') + + local cmdline + if utils.is_nix_build() then + cmdline = table.concat({ + vim.fn.shellescape(utils.get_nix_python()), + vim.fn.shellescape(script), + vim.fn.shellescape(gen_cmd), + vim.fn.shellescape(brute_run_cmd), + vim.fn.shellescape(binary), + }, ' ') + else + cmdline = table.concat({ + 'uv', + 'run', + vim.fn.shellescape(script), + vim.fn.shellescape(gen_cmd), + vim.fn.shellescape(brute_run_cmd), + vim.fn.shellescape(binary), + }, ' ') + end + + vim.cmd.terminal(cmdline) + local term_buf = vim.api.nvim_get_current_buf() + local term_win = vim.api.nvim_get_current_win() + + local cleaned = false + local function cleanup() + if cleaned then + return + end + cleaned = true + if term_buf and vim.api.nvim_buf_is_valid(term_buf) then + local job = vim.b[term_buf] and vim.b[term_buf].terminal_job_id or nil + if job then + pcall(vim.fn.jobstop, job) + end + end + restore_session() + state.stress_buf = nil + state.stress_win = nil + state.set_active_panel(nil) + end + + vim.api.nvim_create_autocmd({ 'BufWipeout', 'BufUnload' }, { + buffer = term_buf, + callback = cleanup, + }) + + vim.api.nvim_create_autocmd('WinClosed', { + callback = function() + if cleaned then + return + end + local any = false + for _, win in ipairs(vim.api.nvim_list_wins()) do + if vim.api.nvim_win_is_valid(win) and vim.api.nvim_win_get_buf(win) == term_buf then + any = true + break + end + end + if not any then + cleanup() + end + end, + }) + + vim.api.nvim_create_autocmd('TermClose', { + buffer = term_buf, + callback = function() + vim.b[term_buf].cp_stress_exited = true + end, + }) + + vim.keymap.set('t', '', function() + cleanup() + end, { buffer = term_buf, silent = true }) + vim.keymap.set('n', '', function() + cleanup() + end, { buffer = term_buf, silent = true }) + + state.stress_buf = term_buf + state.stress_win = term_win + state.set_active_panel('stress') + end) +end + +return M diff --git a/scripts/stress.py b/scripts/stress.py new file mode 100755 index 0000000..429ca26 --- /dev/null +++ b/scripts/stress.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +import subprocess +import sys + + +def main() -> None: + argv = sys.argv[1:] + max_iterations = 1000 + timeout = 10 + + positional: list[str] = [] + i = 0 + while i < len(argv): + if argv[i] == "--max-iterations" and i + 1 < len(argv): + max_iterations = int(argv[i + 1]) + i += 2 + elif argv[i] == "--timeout" and i + 1 < len(argv): + timeout = int(argv[i + 1]) + i += 2 + else: + positional.append(argv[i]) + i += 1 + + if len(positional) != 3: + print( + "Usage: stress.py " + "[--max-iterations N] [--timeout S]", + file=sys.stderr, + ) + sys.exit(1) + + generator, brute, candidate = positional + + for iteration in range(1, max_iterations + 1): + try: + gen_result = subprocess.run( + generator, + capture_output=True, + text=True, + shell=True, + timeout=timeout, + ) + except subprocess.TimeoutExpired: + print(f"[stress] generator timed out on iteration {iteration}", file=sys.stderr) + sys.exit(1) + + if gen_result.returncode != 0: + print( + f"[stress] generator failed on iteration {iteration} " + f"(exit code {gen_result.returncode})", + file=sys.stderr, + ) + if gen_result.stderr: + print(gen_result.stderr, file=sys.stderr, end="") + sys.exit(1) + + test_input = gen_result.stdout + + try: + brute_result = subprocess.run( + brute, + input=test_input, + capture_output=True, + text=True, + shell=True, + timeout=timeout, + ) + except subprocess.TimeoutExpired: + print(f"[stress] brute timed out on iteration {iteration}", file=sys.stderr) + print(f"\n--- input ---\n{test_input}", end="") + sys.exit(1) + + try: + cand_result = subprocess.run( + candidate, + input=test_input, + capture_output=True, + text=True, + shell=True, + timeout=timeout, + ) + except subprocess.TimeoutExpired: + print(f"[stress] candidate timed out on iteration {iteration}", file=sys.stderr) + print(f"\n--- input ---\n{test_input}", end="") + sys.exit(1) + + brute_out = brute_result.stdout.strip() + cand_out = cand_result.stdout.strip() + + if brute_out != cand_out: + print(f"[stress] mismatch on iteration {iteration}", file=sys.stderr) + print(f"\n--- input ---\n{test_input}", end="") + print(f"\n--- expected (brute) ---\n{brute_out}") + print(f"\n--- actual (candidate) ---\n{cand_out}") + sys.exit(1) + + print(f"[stress] iteration {iteration} OK", file=sys.stderr) + + print( + f"[stress] all {max_iterations} iterations passed", + file=sys.stderr, + ) + sys.exit(0) + + +if __name__ == "__main__": + main() From a75694e9e02912d6bca7b030bfae56bd2e54fa8d Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 14:52:00 -0500 Subject: [PATCH 54/86] feat(submit): add solution submission UI Add submit.lua that reads credentials from a local JSON store (prompting via vim.ui.input/inputsecret on first use), reads the source file, and delegates to scraper.submit(). Add language_ids.py with platform-to- language-ID mappings for atcoder, codeforces, and cses. --- lua/cp/submit.lua | 106 +++++++++++++++++++++++++++++++++++++++ scrapers/language_ids.py | 18 +++++++ 2 files changed, 124 insertions(+) create mode 100644 lua/cp/submit.lua create mode 100644 scrapers/language_ids.py diff --git a/lua/cp/submit.lua b/lua/cp/submit.lua new file mode 100644 index 0000000..616dbbb --- /dev/null +++ b/lua/cp/submit.lua @@ -0,0 +1,106 @@ +local M = {} + +local logger = require('cp.log') +local state = require('cp.state') + +local credentials_file = vim.fn.stdpath('data') .. '/cp-nvim-credentials.json' + +local function load_credentials(platform) + if vim.fn.filereadable(credentials_file) ~= 1 then + return nil + end + local content = vim.fn.readfile(credentials_file) + if #content == 0 then + return nil + end + local ok, data = pcall(vim.json.decode, table.concat(content, '\n')) + if not ok then + return nil + end + return data[platform] +end + +local function save_credentials(platform, creds) + local data = {} + if vim.fn.filereadable(credentials_file) == 1 then + local content = vim.fn.readfile(credentials_file) + if #content > 0 then + local ok, decoded = pcall(vim.json.decode, table.concat(content, '\n')) + if ok then + data = decoded + end + end + end + data[platform] = creds + vim.fn.mkdir(vim.fn.fnamemodify(credentials_file, ':h'), 'p') + vim.fn.writefile({ vim.json.encode(data) }, credentials_file) +end + +local function prompt_credentials(platform, callback) + local saved = load_credentials(platform) + if saved and saved.username and saved.password then + callback(saved) + return + end + vim.ui.input({ prompt = platform .. ' username: ' }, function(username) + if not username or username == '' then + logger.log('Submit cancelled', vim.log.levels.WARN) + return + end + vim.fn.inputsave() + local password = vim.fn.inputsecret(platform .. ' password: ') + vim.fn.inputrestore() + if not password or password == '' then + logger.log('Submit cancelled', vim.log.levels.WARN) + return + end + local creds = { username = username, password = password } + save_credentials(platform, creds) + callback(creds) + end) +end + +function M.submit(opts) + local platform = state.get_platform() + local contest_id = state.get_contest_id() + local problem_id = state.get_problem_id() + local language = (opts and opts.language) or state.get_language() + if not platform or not contest_id or not problem_id or not language then + logger.log('No active problem. Use :CP first.', vim.log.levels.ERROR) + return + end + + local source_file = state.get_source_file() + if not source_file or vim.fn.filereadable(source_file) ~= 1 then + logger.log('Source file not found', vim.log.levels.ERROR) + return + end + + prompt_credentials(platform, function(creds) + local source_lines = vim.fn.readfile(source_file) + local source_code = table.concat(source_lines, '\n') + + require('cp.scraper').submit( + platform, + contest_id, + problem_id, + language, + source_code, + creds, + function(result) + vim.schedule(function() + if result and result.success then + logger.log('Submitted successfully', vim.log.levels.INFO, true) + else + logger.log( + 'Submit failed: ' .. (result and result.error or 'unknown error'), + vim.log.levels.ERROR + ) + end + end) + end + ) + end) +end + +return M diff --git a/scrapers/language_ids.py b/scrapers/language_ids.py new file mode 100644 index 0000000..d0a47e6 --- /dev/null +++ b/scrapers/language_ids.py @@ -0,0 +1,18 @@ +LANGUAGE_IDS = { + "atcoder": { + "cpp": "5028", + "python": "5078", + }, + "codeforces": { + "cpp": "89", + "python": "70", + }, + "cses": { + "cpp": "C++17", + "python": "Python3", + }, +} + + +def get_language_id(platform: str, language: str) -> str | None: + return LANGUAGE_IDS.get(platform, {}).get(language) From bfa2cf893c09e46d510c59c4acbea96b7bae9193 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 14:52:10 -0500 Subject: [PATCH 55/86] feat: wire race, stress, and submit commands and keymaps Add command parsing and dispatch for :CP race, :CP race stop, :CP stress, and :CP submit. Add tab-completion for race (platform/contest/--lang), stress (cwd executables at arg 2 and 3), and race stop. Add (cp-stress), (cp-submit), and (cp-race-stop) keymaps. --- lua/cp/commands/init.lua | 38 ++++++++++++++++++++++++++++++++++++++ plugin/cp.lua | 29 +++++++++++++++++++++++++---- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/lua/cp/commands/init.lua b/lua/cp/commands/init.lua index 316bb5c..1e158c7 100644 --- a/lua/cp/commands/init.lua +++ b/lua/cp/commands/init.lua @@ -16,6 +16,8 @@ local actions = constants.ACTIONS ---@field platform? string ---@field problem_id? string ---@field interactor_cmd? string +---@field generator_cmd? string +---@field brute_cmd? string ---@field test_index? integer ---@field test_indices? integer[] ---@field mode? string @@ -53,6 +55,27 @@ local function parse_command(args) else return { type = 'error', message = 'unknown cache subcommand: ' .. subcommand } end + elseif first == 'race' then + if args[2] == 'stop' then + return { type = 'action', action = 'race_stop' } + end + if not args[2] or not args[3] then + return { + type = 'error', + message = 'Usage: :CP race [--lang ]', + } + end + local language = nil + if args[4] == '--lang' and args[5] then + language = args[5] + end + return { + type = 'action', + action = 'race', + platform = args[2], + contest = args[3], + language = language, + } elseif first == 'interact' then local inter = args[2] if inter and inter ~= '' then @@ -60,6 +83,13 @@ local function parse_command(args) else return { type = 'action', action = 'interact' } end + elseif first == 'stress' then + return { + type = 'action', + action = 'stress', + generator_cmd = args[2], + brute_cmd = args[3], + } elseif first == 'edit' then local test_index = nil if #args >= 2 then @@ -285,6 +315,14 @@ function M.handle_command(opts) elseif cmd.action == 'edit' then local edit = require('cp.ui.edit') edit.toggle_edit(cmd.test_index) + elseif cmd.action == 'stress' then + require('cp.stress').toggle(cmd.generator_cmd, cmd.brute_cmd) + elseif cmd.action == 'submit' then + require('cp.submit').submit({ language = cmd.language }) + elseif cmd.action == 'race' then + require('cp.race').start(cmd.platform, cmd.contest, cmd.language) + elseif cmd.action == 'race_stop' then + require('cp.race').stop() end elseif cmd.type == 'problem_jump' then local platform = state.get_platform() diff --git a/plugin/cp.lua b/plugin/cp.lua index 2689e71..e9b159b 100644 --- a/plugin/cp.lua +++ b/plugin/cp.lua @@ -66,7 +66,7 @@ end, { return filter_candidates(contests) elseif args[2] == 'cache' then return filter_candidates({ 'clear', 'read' }) - elseif args[2] == 'interact' then + elseif args[2] == 'stress' or args[2] == 'interact' then local utils = require('cp.utils') return filter_candidates(utils.cwd_executables()) elseif args[2] == 'edit' then @@ -103,6 +103,10 @@ end, { end end return filter_candidates(candidates) + elseif args[2] == 'race' then + local candidates = { 'stop' } + vim.list_extend(candidates, platforms) + return filter_candidates(candidates) elseif args[2] == 'next' or args[2] == 'prev' or args[2] == 'pick' then return filter_candidates({ '--lang' }) else @@ -112,7 +116,15 @@ end, { end end elseif num_args == 4 then - if args[2] == 'cache' and args[3] == 'clear' then + if args[2] == 'stress' then + local utils = require('cp.utils') + return filter_candidates(utils.cwd_executables()) + elseif args[2] == 'race' and vim.tbl_contains(platforms, args[3]) then + local cache = require('cp.cache') + cache.load() + local contests = cache.get_cached_contest_ids(args[3]) + return filter_candidates(contests) + elseif args[2] == 'cache' and args[3] == 'clear' then local candidates = vim.list_extend({}, platforms) table.insert(candidates, '') return filter_candidates(candidates) @@ -134,7 +146,9 @@ end, { return filter_candidates(candidates) end elseif num_args == 5 then - if args[2] == 'cache' and args[3] == 'clear' and vim.tbl_contains(platforms, args[4]) then + if args[2] == 'race' and vim.tbl_contains(platforms, args[3]) then + return filter_candidates({ '--lang' }) + elseif args[2] == 'cache' and args[3] == 'clear' and vim.tbl_contains(platforms, args[4]) then local cache = require('cp.cache') cache.load() local contests = cache.get_cached_contest_ids(args[4]) @@ -147,7 +161,9 @@ end, { end end elseif num_args == 6 then - if vim.tbl_contains(platforms, args[2]) and args[5] == '--lang' then + if args[2] == 'race' and vim.tbl_contains(platforms, args[3]) and args[5] == '--lang' then + return filter_candidates(get_enabled_languages(args[3])) + elseif vim.tbl_contains(platforms, args[2]) and args[5] == '--lang' then return filter_candidates(get_enabled_languages(args[2])) end end @@ -168,3 +184,8 @@ vim.keymap.set('n', '(cp-next)', cp_action('next'), { desc = 'CP next prob vim.keymap.set('n', '(cp-prev)', cp_action('prev'), { desc = 'CP previous problem' }) vim.keymap.set('n', '(cp-pick)', cp_action('pick'), { desc = 'CP pick contest' }) vim.keymap.set('n', '(cp-interact)', cp_action('interact'), { desc = 'CP interactive mode' }) +vim.keymap.set('n', '(cp-stress)', cp_action('stress'), { desc = 'CP stress test' }) +vim.keymap.set('n', '(cp-submit)', cp_action('submit'), { desc = 'CP submit solution' }) +vim.keymap.set('n', '(cp-race-stop)', function() + require('cp.race').stop() +end, { desc = 'CP stop race countdown' }) From ad90d564ca8fe735e69a578234c805a947d711d3 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 14:52:17 -0500 Subject: [PATCH 56/86] fix(views): fix interactive guard logic and add stress panel support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: toggle_interactive() had its condition inverted — it blocked :CP interact on non-interactive problems while showing the message "This problem is interactive", and passed through on interactive ones. The panel guard in toggle_panel() was also missing a nil-check on contest_data.index_map, which could crash if the index map was absent. Solution: invert the toggle_interactive() guard to match the symmetrical pattern in toggle_view(), fix the error message to say "not interactive", and add the missing index_map guard. Also handle the stress panel type in M.disable() so :CP stress can be toggled off. --- lua/cp/ui/views.lua | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lua/cp/ui/views.lua b/lua/cp/ui/views.lua index c2c1c31..041200e 100644 --- a/lua/cp/ui/views.lua +++ b/lua/cp/ui/views.lua @@ -26,6 +26,8 @@ function M.disable() M.toggle_panel() elseif active_panel == 'interactive' then M.toggle_interactive() + elseif active_panel == 'stress' then + require('cp.stress').toggle() else logger.log(('Unknown panel type: %s'):format(tostring(active_panel))) end @@ -67,11 +69,11 @@ function M.toggle_interactive(interactor_cmd) cache.load() local contest_data = cache.get_contest_data(platform, contest_id) if - not contest_data - or not contest_data.index_map - or not contest_data.problems[contest_data.index_map[problem_id]].interactive + contest_data + and contest_data.index_map + and not contest_data.problems[contest_data.index_map[problem_id]].interactive then - logger.log('This problem is interactive. Use :CP interact.', vim.log.levels.ERROR) + logger.log('This problem is not interactive. Use :CP {run,panel}.', vim.log.levels.ERROR) return end @@ -830,6 +832,7 @@ function M.toggle_panel(panel_opts) local contest_data = cache.get_contest_data(platform, contest_id) if contest_data + and contest_data.index_map and contest_data.problems[contest_data.index_map[state.get_problem_id()]].interactive then logger.log('This is an interactive problem. Use :CP interact instead.', vim.log.levels.WARN) From bad219e57864520a417216288ca09e99b3bee725 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 14:58:57 -0500 Subject: [PATCH 57/86] ci: format --- lua/cp/constants.lua | 14 +++++++++++++- scrapers/atcoder.py | 25 ++++++++++++++++++++----- scrapers/base.py | 27 +++++++++++++++++++++++---- scrapers/codechef.py | 16 ++++++++++++++-- scrapers/codeforces.py | 16 ++++++++++++++-- scrapers/cses.py | 16 ++++++++++++++-- scrapers/kattis.py | 17 ++++++++++++++--- scrapers/usaco.py | 17 ++++++++++++++--- scripts/stress.py | 10 ++++++++-- 9 files changed, 134 insertions(+), 24 deletions(-) diff --git a/lua/cp/constants.lua b/lua/cp/constants.lua index c4bac3e..21e8f62 100644 --- a/lua/cp/constants.lua +++ b/lua/cp/constants.lua @@ -1,7 +1,19 @@ local M = {} M.PLATFORMS = { 'atcoder', 'codechef', 'codeforces', 'cses', 'kattis', 'usaco' } -M.ACTIONS = { 'run', 'panel', 'next', 'prev', 'pick', 'cache', 'interact', 'edit', 'race', 'stress', 'submit' } +M.ACTIONS = { + 'run', + 'panel', + 'next', + 'prev', + 'pick', + 'cache', + 'interact', + 'edit', + 'race', + 'stress', + 'submit', +} M.PLATFORM_DISPLAY_NAMES = { atcoder = 'AtCoder', diff --git a/scrapers/atcoder.py b/scrapers/atcoder.py index 9b7fad6..c7549b8 100644 --- a/scrapers/atcoder.py +++ b/scrapers/atcoder.py @@ -369,15 +369,26 @@ class AtcoderScraper(BaseScraper): await asyncio.gather(*(emit(r) for r in rows)) - async def submit(self, contest_id: str, problem_id: str, source_code: str, language_id: str, credentials: dict[str, str]) -> SubmitResult: + async def submit( + self, + contest_id: str, + problem_id: str, + source_code: str, + language_id: str, + credentials: dict[str, str], + ) -> SubmitResult: def _submit_sync() -> SubmitResult: try: - login_page = _session.get(f"{BASE_URL}/login", headers=HEADERS, timeout=TIMEOUT_SECONDS) + login_page = _session.get( + f"{BASE_URL}/login", headers=HEADERS, timeout=TIMEOUT_SECONDS + ) login_page.raise_for_status() soup = BeautifulSoup(login_page.text, "html.parser") csrf_input = soup.find("input", {"name": "csrf_token"}) if not csrf_input: - return SubmitResult(success=False, error="Could not find CSRF token on login page") + return SubmitResult( + success=False, error="Could not find CSRF token on login page" + ) csrf_token = csrf_input.get("value", "") login_resp = _session.post( @@ -401,7 +412,9 @@ class AtcoderScraper(BaseScraper): soup = BeautifulSoup(submit_page.text, "html.parser") csrf_input = soup.find("input", {"name": "csrf_token"}) if not csrf_input: - return SubmitResult(success=False, error="Could not find CSRF token on submit page") + return SubmitResult( + success=False, error="Could not find CSRF token on submit page" + ) csrf_token = csrf_input.get("value", "") task_screen_name = f"{contest_id}_{problem_id}" @@ -418,7 +431,9 @@ class AtcoderScraper(BaseScraper): ) submit_resp.raise_for_status() - return SubmitResult(success=True, error="", submission_id="", verdict="submitted") + return SubmitResult( + success=True, error="", submission_id="", verdict="submitted" + ) except Exception as e: return SubmitResult(success=False, error=str(e)) diff --git a/scrapers/base.py b/scrapers/base.py index 6cd1c5a..ed0636b 100644 --- a/scrapers/base.py +++ b/scrapers/base.py @@ -6,7 +6,13 @@ import sys from abc import ABC, abstractmethod from .language_ids import get_language_id -from .models import CombinedTest, ContestListResult, MetadataResult, SubmitResult, TestsResult +from .models import ( + CombinedTest, + ContestListResult, + MetadataResult, + SubmitResult, + TestsResult, +) _PRECISION_ABS_REL_RE = re.compile( r"(?:absolute|relative)\s+error[^.]*?10\s*[\^{]\s*\{?\s*[-\u2212]\s*(\d+)\s*\}?", @@ -43,7 +49,14 @@ class BaseScraper(ABC): async def stream_tests_for_category_async(self, category_id: str) -> None: ... @abstractmethod - async def submit(self, contest_id: str, problem_id: str, source_code: str, language_id: str, credentials: dict[str, str]) -> SubmitResult: ... + async def submit( + self, + contest_id: str, + problem_id: str, + source_code: str, + language_id: str, + credentials: dict[str, str], + ) -> SubmitResult: ... def _usage(self) -> str: name = self.platform_name @@ -102,7 +115,11 @@ class BaseScraper(ABC): case "submit": if len(args) != 5: - print(self._submit_error("Usage: submit ").model_dump_json()) + print( + self._submit_error( + "Usage: submit " + ).model_dump_json() + ) return 1 source_code = sys.stdin.read() creds_raw = os.environ.get("CP_CREDENTIALS", "{}") @@ -111,7 +128,9 @@ class BaseScraper(ABC): except json.JSONDecodeError: credentials = {} language_id = get_language_id(self.platform_name, args[4]) or args[4] - result = await self.submit(args[2], args[3], source_code, language_id, credentials) + result = await self.submit( + args[2], args[3], source_code, language_id, credentials + ) print(result.model_dump_json()) return 0 if result.success else 1 diff --git a/scrapers/codechef.py b/scrapers/codechef.py index 0e53f26..57ce33e 100644 --- a/scrapers/codechef.py +++ b/scrapers/codechef.py @@ -252,8 +252,20 @@ class CodeChefScraper(BaseScraper): payload = await coro print(json.dumps(payload), flush=True) - async def submit(self, contest_id: str, problem_id: str, source_code: str, language_id: str, credentials: dict[str, str]) -> SubmitResult: - return SubmitResult(success=False, error="CodeChef submit not yet implemented", submission_id="", verdict="") + async def submit( + self, + contest_id: str, + problem_id: str, + source_code: str, + language_id: str, + credentials: dict[str, str], + ) -> SubmitResult: + return SubmitResult( + success=False, + error="CodeChef submit not yet implemented", + submission_id="", + verdict="", + ) if __name__ == "__main__": diff --git a/scrapers/codeforces.py b/scrapers/codeforces.py index 7863c27..c0495d8 100644 --- a/scrapers/codeforces.py +++ b/scrapers/codeforces.py @@ -281,8 +281,20 @@ class CodeforcesScraper(BaseScraper): flush=True, ) - async def submit(self, contest_id: str, problem_id: str, source_code: str, language_id: str, credentials: dict[str, str]) -> SubmitResult: - return SubmitResult(success=False, error="Codeforces submit not yet implemented", submission_id="", verdict="") + async def submit( + self, + contest_id: str, + problem_id: str, + source_code: str, + language_id: str, + credentials: dict[str, str], + ) -> SubmitResult: + return SubmitResult( + success=False, + error="Codeforces submit not yet implemented", + submission_id="", + verdict="", + ) if __name__ == "__main__": diff --git a/scrapers/cses.py b/scrapers/cses.py index f840238..554af4d 100644 --- a/scrapers/cses.py +++ b/scrapers/cses.py @@ -262,8 +262,20 @@ class CSESScraper(BaseScraper): payload = await coro print(json.dumps(payload), flush=True) - async def submit(self, contest_id: str, problem_id: str, source_code: str, language_id: str, credentials: dict[str, str]) -> SubmitResult: - return SubmitResult(success=False, error="CSES submit not yet implemented", submission_id="", verdict="") + async def submit( + self, + contest_id: str, + problem_id: str, + source_code: str, + language_id: str, + credentials: dict[str, str], + ) -> SubmitResult: + return SubmitResult( + success=False, + error="CSES submit not yet implemented", + submission_id="", + verdict="", + ) if __name__ == "__main__": diff --git a/scrapers/kattis.py b/scrapers/kattis.py index c98dc35..24bfb45 100644 --- a/scrapers/kattis.py +++ b/scrapers/kattis.py @@ -222,9 +222,20 @@ class KattisScraper(BaseScraper): flush=True, ) - - async def submit(self, contest_id: str, problem_id: str, source_code: str, language_id: str, credentials: dict[str, str]) -> SubmitResult: - return SubmitResult(success=False, error="Kattis submit not yet implemented", submission_id="", verdict="") + async def submit( + self, + contest_id: str, + problem_id: str, + source_code: str, + language_id: str, + credentials: dict[str, str], + ) -> SubmitResult: + return SubmitResult( + success=False, + error="Kattis submit not yet implemented", + submission_id="", + verdict="", + ) if __name__ == "__main__": diff --git a/scrapers/usaco.py b/scrapers/usaco.py index b8c6c9f..32372f4 100644 --- a/scrapers/usaco.py +++ b/scrapers/usaco.py @@ -279,9 +279,20 @@ class USACOScraper(BaseScraper): payload = await coro print(json.dumps(payload), flush=True) - - async def submit(self, contest_id: str, problem_id: str, source_code: str, language_id: str, credentials: dict[str, str]) -> SubmitResult: - return SubmitResult(success=False, error="USACO submit not yet implemented", submission_id="", verdict="") + async def submit( + self, + contest_id: str, + problem_id: str, + source_code: str, + language_id: str, + credentials: dict[str, str], + ) -> SubmitResult: + return SubmitResult( + success=False, + error="USACO submit not yet implemented", + submission_id="", + verdict="", + ) if __name__ == "__main__": diff --git a/scripts/stress.py b/scripts/stress.py index 429ca26..af40a42 100755 --- a/scripts/stress.py +++ b/scripts/stress.py @@ -41,7 +41,10 @@ def main() -> None: timeout=timeout, ) except subprocess.TimeoutExpired: - print(f"[stress] generator timed out on iteration {iteration}", file=sys.stderr) + print( + f"[stress] generator timed out on iteration {iteration}", + file=sys.stderr, + ) sys.exit(1) if gen_result.returncode != 0: @@ -80,7 +83,10 @@ def main() -> None: timeout=timeout, ) except subprocess.TimeoutExpired: - print(f"[stress] candidate timed out on iteration {iteration}", file=sys.stderr) + print( + f"[stress] candidate timed out on iteration {iteration}", + file=sys.stderr, + ) print(f"\n--- input ---\n{test_input}", end="") sys.exit(1) From de5a20c56707ed1a02bb9369cccc37a9970af412 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 15:01:59 -0500 Subject: [PATCH 58/86] fix: resolve typecheck errors in cache, atcoder, cses, and usaco Problem: lua typecheck flagged missing start_time field on ContestSummary; ty flagged BeautifulSoup Tag/NavigableString union on csrf_input.get(), a 3-tuple unpack where _extract_problem_info now returns 4 values in cses.py, and an untyped list assignment in usaco.py. Solution: add start_time? to ContestSummary LuaDoc, guard csrf_input with hasattr check and type: ignore, unpack precision from _extract_problem_info in cses.py callers, and use cast() in usaco.py. --- lua/cp/cache.lua | 1 + scrapers/atcoder.py | 8 ++++---- scrapers/cses.py | 12 ++++++++++-- scrapers/usaco.py | 4 ++-- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/lua/cp/cache.lua b/lua/cp/cache.lua index 209c319..61c6a89 100644 --- a/lua/cp/cache.lua +++ b/lua/cp/cache.lua @@ -15,6 +15,7 @@ ---@field display_name string ---@field name string ---@field id string +---@field start_time? integer ---@class CombinedTest ---@field input string diff --git a/scrapers/atcoder.py b/scrapers/atcoder.py index c7549b8..14debdc 100644 --- a/scrapers/atcoder.py +++ b/scrapers/atcoder.py @@ -385,11 +385,11 @@ class AtcoderScraper(BaseScraper): login_page.raise_for_status() soup = BeautifulSoup(login_page.text, "html.parser") csrf_input = soup.find("input", {"name": "csrf_token"}) - if not csrf_input: + if not csrf_input or not hasattr(csrf_input, "get"): return SubmitResult( success=False, error="Could not find CSRF token on login page" ) - csrf_token = csrf_input.get("value", "") + csrf_token = csrf_input.get("value", "") or "" # type: ignore[union-attr] login_resp = _session.post( f"{BASE_URL}/login", @@ -411,11 +411,11 @@ class AtcoderScraper(BaseScraper): submit_page.raise_for_status() soup = BeautifulSoup(submit_page.text, "html.parser") csrf_input = soup.find("input", {"name": "csrf_token"}) - if not csrf_input: + if not csrf_input or not hasattr(csrf_input, "get"): return SubmitResult( success=False, error="Could not find CSRF token on submit page" ) - csrf_token = csrf_input.get("value", "") + csrf_token = csrf_input.get("value", "") or "" # type: ignore[union-attr] task_screen_name = f"{contest_id}_{problem_id}" submit_resp = _session.post( diff --git a/scrapers/cses.py b/scrapers/cses.py index 554af4d..473558f 100644 --- a/scrapers/cses.py +++ b/scrapers/cses.py @@ -232,10 +232,17 @@ class CSESScraper(BaseScraper): try: html = await fetch_text(client, task_path(pid)) tests = parse_tests(html) - timeout_ms, memory_mb, interactive = _extract_problem_info(html) + timeout_ms, memory_mb, interactive, precision = ( + _extract_problem_info(html) + ) except Exception: tests = [] - timeout_ms, memory_mb, interactive = 0, 0, False + timeout_ms, memory_mb, interactive, precision = ( + 0, + 0, + False, + None, + ) combined_input = "\n".join(t.input for t in tests) if tests else "" combined_expected = ( @@ -255,6 +262,7 @@ class CSESScraper(BaseScraper): "memory_mb": memory_mb, "interactive": interactive, "multi_test": False, + "precision": precision, } tasks = [run_one(p.id) for p in problems] diff --git a/scrapers/usaco.py b/scrapers/usaco.py index 32372f4..565f1b5 100644 --- a/scrapers/usaco.py +++ b/scrapers/usaco.py @@ -3,7 +3,7 @@ import asyncio import json import re -from typing import Any +from typing import Any, cast import httpx @@ -253,7 +253,7 @@ class USACOScraper(BaseScraper): "interactive": False, } - tests: list[TestCase] = info["tests"] + tests = cast(list[TestCase], info["tests"]) combined_input = "\n".join(t.input for t in tests) if tests else "" combined_expected = ( "\n".join(t.expected for t in tests) if tests else "" From e79f992e0b28cbaa4f6e81f7a8be8cf8df35a502 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 15:08:51 -0500 Subject: [PATCH 59/86] fix: resolve lua typecheck warnings in race and scraper Problem: luals flagged undefined-field on uv timer methods because race_state.timer was untyped, and undefined-field on env_extra/stdin because they were missing from the run_scraper opts annotation. Solution: hoist race_state.timer into a typed local before the nil check so luals can narrow through it; add env_extra and stdin to the opts inline type in run_scraper. --- lua/cp/race.lua | 7 ++++--- lua/cp/scraper.lua | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lua/cp/race.lua b/lua/cp/race.lua index 689ed41..0e303ee 100644 --- a/lua/cp/race.lua +++ b/lua/cp/race.lua @@ -114,12 +114,13 @@ function M.start(platform, contest_id, language) end function M.stop() - if not race_state.timer then + local timer = race_state.timer + if not timer then logger.log('No active race', vim.log.levels.WARN) return end - race_state.timer:stop() - race_state.timer:close() + timer:stop() + timer:close() race_state.timer = nil race_state.platform = nil race_state.contest_id = nil diff --git a/lua/cp/scraper.lua b/lua/cp/scraper.lua index 216e27e..6c1242d 100644 --- a/lua/cp/scraper.lua +++ b/lua/cp/scraper.lua @@ -33,7 +33,7 @@ end ---@param platform string ---@param subcommand string ---@param args string[] ----@param opts { sync?: boolean, ndjson?: boolean, on_event?: fun(ev: table), on_exit?: fun(result: table) } +---@param opts { sync?: boolean, ndjson?: boolean, on_event?: fun(ev: table), on_exit?: fun(result: table), env_extra?: table, stdin?: string } local function run_scraper(platform, subcommand, args, opts) if not utils.setup_python_env() then local msg = 'no Python environment available (install uv or nix)' From 7e48ba05cfdfa3b5d67a4a17ad9f36cbc0002b41 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 15:41:43 -0500 Subject: [PATCH 60/86] feat(kattis): rewrite scraper to support real contests Problem: scrape_contest_list paginated the entire Kattis problem database (3000+ problems) treating each as a "contest". scrape_contest_metadata only handled single-problem access. stream_tests_for_category_async could not fetch tests for multiple problems in a real contest. Solution: replace the paginated problem loop with a single GET to /contests that returns ~150 real timed contests. Add contest-aware path to scrape_contest_metadata that fetches /contests/{id}/problems and returns all problem slugs; fall back to single-problem path when the ID is not a contest. Add _stream_single_problem helper and update stream_tests_for_category_async to fan out concurrently over all contest problem slugs before falling back to the single-problem path. --- scrapers/kattis.py | 245 +++++++++++++++++++++++++++------------------ 1 file changed, 146 insertions(+), 99 deletions(-) diff --git a/scrapers/kattis.py b/scrapers/kattis.py index 24bfb45..d1675bf 100644 --- a/scrapers/kattis.py +++ b/scrapers/kattis.py @@ -5,6 +5,7 @@ import io import json import re import zipfile +from datetime import datetime import httpx @@ -33,7 +34,6 @@ MEM_RE = re.compile( r"Memory limit\s*]*>\s*(\d+)\s*MB\s*", re.DOTALL, ) -LAST_PAGE_RE = re.compile(r"\bpage=(\d+)") async def _fetch_text(client: httpx.AsyncClient, url: str) -> str: @@ -94,24 +94,110 @@ def _is_interactive(html: str) -> bool: return "This is an interactive problem" in html -def _parse_problem_rows(html: str) -> list[tuple[str, str]]: +def _parse_contests_page(html: str) -> list[ContestSummary]: + results: list[ContestSummary] = [] seen: set[str] = set() - out: list[tuple[str, str]] = [] - for m in re.finditer( - r'\s*([^<]+)', - html, - ): - pid = m.group(1) - name = m.group(2).strip() - if pid not in seen: - seen.add(pid) - out.append((pid, name)) - return out + for row_m in re.finditer(r"]*>(.*?)", html, re.DOTALL): + row = row_m.group(1) + link_m = re.search(r'href="/contests/([a-z0-9]+)"[^>]*>([^<]+)', row) + if not link_m: + continue + cid = link_m.group(1) + name = link_m.group(2).strip() + if cid in seen: + continue + seen.add(cid) + start_time: int | None = None + ts_m = re.search(r'data-timestamp="(\d+)"', row) + if ts_m: + start_time = int(ts_m.group(1)) + else: + time_m = re.search(r']+datetime="([^"]+)"', row) + if time_m: + try: + dt = datetime.fromisoformat(time_m.group(1).replace("Z", "+00:00")) + start_time = int(dt.timestamp()) + except Exception: + pass + results.append(ContestSummary(id=cid, name=name, start_time=start_time)) + return results -def _parse_last_page(html: str) -> int: - nums = [int(m.group(1)) for m in LAST_PAGE_RE.finditer(html)] - return max(nums) if nums else 0 +def _parse_contest_problem_list(html: str) -> list[tuple[str, str]]: + if "The problems will become available when the contest starts" in html: + return [] + results: list[tuple[str, str]] = [] + seen: set[str] = set() + for row_m in re.finditer(r"]*>(.*?)", html, re.DOTALL): + row = row_m.group(1) + link_m = re.search( + r'href="/contests/[^/]+/problems/([^"]+)"[^>]*>([^<]+)', row + ) + if not link_m: + continue + slug = link_m.group(1) + name = link_m.group(2).strip() + if slug in seen: + continue + seen.add(slug) + label_m = re.search(r"]*>\s*([A-Z])\s*", row) + label = label_m.group(1) if label_m else "" + display = f"{label} - {name}" if label else name + results.append((slug, display)) + return results + + +async def _fetch_contest_slugs( + client: httpx.AsyncClient, contest_id: str +) -> list[tuple[str, str]]: + try: + html = await _fetch_text(client, f"{BASE_URL}/contests/{contest_id}/problems") + return _parse_contest_problem_list(html) + except httpx.HTTPStatusError: + return [] + except Exception: + return [] + + +async def _stream_single_problem(client: httpx.AsyncClient, slug: str) -> None: + try: + html = await _fetch_text(client, f"{BASE_URL}/problems/{slug}") + except Exception: + return + + timeout_ms, memory_mb = _parse_limits(html) + interactive = _is_interactive(html) + + tests: list[TestCase] = [] + try: + zip_data = await _fetch_bytes( + client, + f"{BASE_URL}/problems/{slug}/file/statement/samples.zip", + ) + tests = _parse_samples_zip(zip_data) + except Exception: + tests = _parse_samples_html(html) + + combined_input = "\n".join(t.input for t in tests) if tests else "" + combined_expected = "\n".join(t.expected for t in tests) if tests else "" + + print( + json.dumps( + { + "problem_id": slug, + "combined": { + "input": combined_input, + "expected": combined_expected, + }, + "tests": [{"input": t.input, "expected": t.expected} for t in tests], + "timeout_ms": timeout_ms, + "memory_mb": memory_mb, + "interactive": interactive, + "multi_test": False, + } + ), + flush=True, + ) class KattisScraper(BaseScraper): @@ -122,57 +208,46 @@ class KattisScraper(BaseScraper): async def scrape_contest_metadata(self, contest_id: str) -> MetadataResult: try: async with httpx.AsyncClient() as client: - html = await _fetch_text(client, f"{BASE_URL}/problems/{contest_id}") - timeout_ms, memory_mb = _parse_limits(html) - title_m = re.search(r"([^<]+)", html) - name = ( - title_m.group(1).split("\u2013")[0].strip() if title_m else contest_id - ) - return MetadataResult( - success=True, - error="", - contest_id=contest_id, - problems=[ProblemSummary(id=contest_id, name=name)], - url=f"{BASE_URL}/problems/%s", - ) + slugs = await _fetch_contest_slugs(client, contest_id) + if slugs: + return MetadataResult( + success=True, + error="", + contest_id=contest_id, + problems=[ + ProblemSummary(id=slug, name=name) for slug, name in slugs + ], + url=f"{BASE_URL}/problems/%s", + ) + try: + html = await _fetch_text( + client, f"{BASE_URL}/problems/{contest_id}" + ) + except Exception as e: + return self._metadata_error(str(e)) + title_m = re.search(r"([^<]+)", html) + name = ( + title_m.group(1).split("\u2013")[0].strip() + if title_m + else contest_id + ) + return MetadataResult( + success=True, + error="", + contest_id=contest_id, + problems=[ProblemSummary(id=contest_id, name=name)], + url=f"{BASE_URL}/problems/%s", + ) except Exception as e: return self._metadata_error(str(e)) async def scrape_contest_list(self) -> ContestListResult: try: - async with httpx.AsyncClient( - limits=httpx.Limits(max_connections=CONNECTIONS) - ) as client: - first_html = await _fetch_text( - client, f"{BASE_URL}/problems?page=0&order=problem_difficulty" - ) - last = _parse_last_page(first_html) - rows = _parse_problem_rows(first_html) - - sem = asyncio.Semaphore(CONNECTIONS) - - async def fetch_page(page: int) -> list[tuple[str, str]]: - async with sem: - html = await _fetch_text( - client, - f"{BASE_URL}/problems?page={page}&order=problem_difficulty", - ) - return _parse_problem_rows(html) - - tasks = [fetch_page(p) for p in range(1, last + 1)] - for coro in asyncio.as_completed(tasks): - rows.extend(await coro) - - seen: set[str] = set() - contests: list[ContestSummary] = [] - for pid, name in rows: - if pid not in seen: - seen.add(pid) - contests.append( - ContestSummary(id=pid, name=name, display_name=name) - ) + async with httpx.AsyncClient() as client: + html = await _fetch_text(client, f"{BASE_URL}/contests") + contests = _parse_contests_page(html) if not contests: - return self._contests_error("No problems found") + return self._contests_error("No contests found") return ContestListResult(success=True, error="", contests=contests) except Exception as e: return self._contests_error(str(e)) @@ -181,46 +256,18 @@ class KattisScraper(BaseScraper): async with httpx.AsyncClient( limits=httpx.Limits(max_connections=CONNECTIONS) ) as client: - try: - html = await _fetch_text(client, f"{BASE_URL}/problems/{category_id}") - except Exception: + slugs = await _fetch_contest_slugs(client, category_id) + if slugs: + sem = asyncio.Semaphore(CONNECTIONS) + + async def emit_one(slug: str, _name: str) -> None: + async with sem: + await _stream_single_problem(client, slug) + + await asyncio.gather(*(emit_one(s, n) for s, n in slugs)) return - timeout_ms, memory_mb = _parse_limits(html) - interactive = _is_interactive(html) - - tests: list[TestCase] = [] - try: - zip_data = await _fetch_bytes( - client, - f"{BASE_URL}/problems/{category_id}/file/statement/samples.zip", - ) - tests = _parse_samples_zip(zip_data) - except Exception: - tests = _parse_samples_html(html) - - combined_input = "\n".join(t.input for t in tests) if tests else "" - combined_expected = "\n".join(t.expected for t in tests) if tests else "" - - print( - json.dumps( - { - "problem_id": category_id, - "combined": { - "input": combined_input, - "expected": combined_expected, - }, - "tests": [ - {"input": t.input, "expected": t.expected} for t in tests - ], - "timeout_ms": timeout_ms, - "memory_mb": memory_mb, - "interactive": interactive, - "multi_test": False, - } - ), - flush=True, - ) + await _stream_single_problem(client, category_id) async def submit( self, From 52cf54d05c950d117531aba28c4858f36ef82cf8 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 15:56:43 -0500 Subject: [PATCH 61/86] docs: document new platforms, commands, and features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: vimdoc only covered AtCoder, Codeforces, and CSES, and had no entries for race, stress, or submit — all of which shipped in this branch. The platform list was also stale and the workflow example pointed users to the AtCoder website to submit manually. Solution: add CodeChef, USACO, and Kattis to the supported platforms list and platform-specific usage section (including Kattis's dual single-problem/full-contest behavior). Document :CP stress, :CP race, and :CP submit in the commands section, add their mappings, and add dedicated STRESS TESTING, RACE, and SUBMIT sections. Update get_active_panel() to list its return values, add the cp.race.status() API under the statusline section, and update the workflow example step 8 to use :CP submit. --- doc/cp.nvim.txt | 150 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 147 insertions(+), 3 deletions(-) diff --git a/doc/cp.nvim.txt b/doc/cp.nvim.txt index b56db09..4a10227 100644 --- a/doc/cp.nvim.txt +++ b/doc/cp.nvim.txt @@ -9,7 +9,7 @@ INTRODUCTION *cp.nvim* cp.nvim is a competitive programming plugin that automates problem setup, compilation, and testing workflow for online judges. -Supported platforms (for now!): AtCoder, Codeforces, CSES +Supported platforms: AtCoder, CodeChef, Codeforces, CSES, Kattis, USACO ============================================================================== REQUIREMENTS *cp-requirements* @@ -68,6 +68,18 @@ Configuration is done via `vim.g.cp`. Set this before using the plugin: enabled_languages = { 'cpp', 'python' }, default_language = 'cpp', }, + codechef = { + enabled_languages = { 'cpp', 'python' }, + default_language = 'cpp', + }, + usaco = { + enabled_languages = { 'cpp', 'python' }, + default_language = 'cpp', + }, + kattis = { + enabled_languages = { 'cpp', 'python' }, + default_language = 'cpp', + }, }, open_url = true, debug = false, @@ -344,6 +356,13 @@ COMMANDS *cp-commands* *cp-interact*). Otherwise, runs the source file. Only valid for interactive problems. + :CP stress [generator] [brute] + Start an automated stress test loop against a + brute-force reference. Toggles off if already + running. Without arguments, auto-detects a + generator and brute script in the working + directory. See |cp-stress|. + Navigation Commands ~ :CP next [--lang {language}] Navigate to next problem in current contest. @@ -401,6 +420,25 @@ COMMANDS *cp-commands* :CP edit 3 " Edit all, start at test 3 < + Race Commands ~ + :CP race {platform} {contest_id} [--lang {language}] + Start a countdown to the contest's scheduled + start time. At T=0, automatically runs: + :CP {platform} {contest_id} [--lang ...] + Examples: > + :CP race atcoder abc400 + :CP race codeforces 2100 --lang python +< + :CP race stop + Cancel an active race countdown. + + Submit Commands ~ + :CP submit [--lang {language}] + Submit the current solution to the online + judge. Prompts for credentials on first use + and stores them for subsequent submissions. + --lang: Submit solution for a specific language. + State Restoration ~ :CP Restore state from current file. Automatically detects platform, contest, problem, @@ -488,6 +526,15 @@ through the same code path as |:CP|. *(cp-interact)* (cp-interact) Open interactive mode. Equivalent to :CP interact. + *(cp-stress)* +(cp-stress) Run stress test loop. Equivalent to :CP stress. + + *(cp-submit)* +(cp-submit) Submit current solution. Equivalent to :CP submit. + + *(cp-race-stop)* +(cp-race-stop) Cancel active race countdown. Equivalent to :CP race stop. + Example configuration: >lua vim.keymap.set('n', 'cr', '(cp-run)') vim.keymap.set('n', 'cp', '(cp-panel)') @@ -496,6 +543,9 @@ Example configuration: >lua vim.keymap.set('n', 'cN', '(cp-prev)') vim.keymap.set('n', 'cc', '(cp-pick)') vim.keymap.set('n', 'ci', '(cp-interact)') + vim.keymap.set('n', 'cs', '(cp-stress)') + vim.keymap.set('n', 'cu', '(cp-submit)') + vim.keymap.set('n', 'cR', '(cp-race-stop)') < ============================================================================== @@ -575,6 +625,41 @@ URL format: https://cses.fi/problemset/task/{problem_id} Usage examples: > :CP cses dynamic_programming " Set up all problems in dp category +CodeChef ~ + *cp-codechef* +URL format: https://www.codechef.com/{contest_id}/problems/{problem_id} + +The contest_id is the contest code from the URL (e.g. START209). + +Usage examples: > + :CP codechef START209 " Set up codechef.com/START209 + +USACO ~ + *cp-usaco* +URL format: https://usaco.org/index.php?page=viewproblem2&cpid={cpid} + +The contest_id combines the abbreviated month, two-digit year, and division +in lowercase, joined by underscores (e.g. dec24_gold, feb23_silver). + +Usage examples: > + :CP usaco dec24_gold " Set up December 2024 Gold division + :CP usaco feb23_silver " Set up February 2023 Silver division + +Kattis ~ + *cp-kattis* +Kattis supports single-problem and full-contest modes. + +Single problem — the contest_id is the problem slug from the URL: +URL format: https://open.kattis.com/problems/{slug} + +Full contest — the contest_id is the contest ID from the URL. All problems +are set up at once with :CP next/:CP prev navigation: +URL format: https://open.kattis.com/contests/{id} + +Usage examples: > + :CP kattis primesieve " Single problem + :CP kattis t8tnpe " Full contest (all problems, A–H navigation) + ============================================================================== COMPLETE WORKFLOW EXAMPLE *cp-example* @@ -609,7 +694,9 @@ Example: Setting up and solving AtCoder contest ABC324 :CP < Automatically restores abc323 contest context -8. Submit solutions on AtCoder website +8. Submit solution: > + :CP submit +< Prompts for credentials on first use and submits to AtCoder. ============================================================================== I/O VIEW *cp-io-view* @@ -820,6 +907,55 @@ When using :CP interact {interactor}, the interactor must be executable Keymaps ~ Close the terminal and restore the previous layout. +============================================================================== +STRESS TESTING *cp-stress* + +Start an automated stress test loop to find inputs where your solution +disagrees with a brute-force reference. + +:CP stress [generator] [brute] + Start the stress loop. Toggles off if the loop is already running. + {generator} Generator script path (default: auto-detected). + {brute} Brute-force solution path (default: auto-detected). + Auto-detection looks for files named gen.* and brute.* in the CWD. + + The stress panel opens and streams results for each iteration. + On a mismatch, the failing input is displayed in the panel. + +Keymaps ~ + Close the stress panel and restore the previous layout. + +============================================================================== +RACE *cp-race* + +Count down to a contest's start time and automatically run setup at T=0. + +:CP race {platform} {contest_id} [--lang {language}] + Start a countdown timer. At T=0, automatically runs: + :CP {platform} {contest_id} [--lang {language}] + Examples: > + :CP race atcoder abc400 + :CP race codeforces 2100 --lang python +< +:CP race stop + Cancel an active race countdown. + +Statusline integration: see |cp-race-status|. + +============================================================================== +SUBMIT *cp-submit* + +Submit the current solution to the online judge. + +:CP submit [--lang {language}] + Submit the current solution. Prompts for credentials on first use + and stores them for subsequent submissions. + --lang: Override the language to submit. + + Platform support: + AtCoder Fully implemented. + Others Not yet implemented. + ============================================================================== ANSI COLORS AND HIGHLIGHTING *cp-ansi* @@ -930,7 +1066,15 @@ The following getters are available for statusline use: get_language() (string?) Language id. e.g. "cpp", "python" get_base_name() (string?) Derived filename stem. e.g. "1933a" get_source_file() (string?) Full source filename. e.g. "1933a.cc" - get_active_panel() (string?) Non-nil when the test panel is open. + get_active_panel() (string?) One of 'run', 'interactive', 'stress', or + nil when no panel is open. + +Race API ~ + *cp-race-status* + require('cp.race').status() returns a table describing the race state: + { active = false } + { active = true, platform = string, contest_id = string, + remaining_seconds = number } Recipe: vanilla statusline ~ From a08d1f0c5ed00c5978488a88f52b70dd55f15c43 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 16:21:21 -0500 Subject: [PATCH 62/86] refactor(submit): consolidate credentials into main cache file Problem: credentials were stored in a separate file, cp-nvim-credentials.json, alongside the main cp-nvim.json cache. Two files for one plugin's persistent state was unnecessary. Solution: add get_credentials/set_credentials to cache.lua, storing credentials under _credentials[platform] in the shared cache. Update clear_all() to preserve _credentials across cache wipes. Remove the separate file, load_credentials, and save_credentials from submit.lua. --- lua/cp/cache.lua | 21 +++++++++++++++++++++ lua/cp/submit.lua | 38 +++----------------------------------- 2 files changed, 24 insertions(+), 35 deletions(-) diff --git a/lua/cp/cache.lua b/lua/cp/cache.lua index 61c6a89..60c91de 100644 --- a/lua/cp/cache.lua +++ b/lua/cp/cache.lua @@ -371,8 +371,29 @@ function M.get_contest_start_time(platform, contest_id) return cache_data[platform][contest_id].start_time end +---@param platform string +---@return table? +function M.get_credentials(platform) + if not cache_data._credentials then + return nil + end + return cache_data._credentials[platform] +end + +---@param platform string +---@param creds table +function M.set_credentials(platform, creds) + cache_data._credentials = cache_data._credentials or {} + cache_data._credentials[platform] = creds + M.save() +end + function M.clear_all() + local creds = cache_data._credentials cache_data = {} + if creds then + cache_data._credentials = creds + end M.save() end diff --git a/lua/cp/submit.lua b/lua/cp/submit.lua index 616dbbb..d7f91fa 100644 --- a/lua/cp/submit.lua +++ b/lua/cp/submit.lua @@ -1,43 +1,11 @@ local M = {} +local cache = require('cp.cache') local logger = require('cp.log') local state = require('cp.state') -local credentials_file = vim.fn.stdpath('data') .. '/cp-nvim-credentials.json' - -local function load_credentials(platform) - if vim.fn.filereadable(credentials_file) ~= 1 then - return nil - end - local content = vim.fn.readfile(credentials_file) - if #content == 0 then - return nil - end - local ok, data = pcall(vim.json.decode, table.concat(content, '\n')) - if not ok then - return nil - end - return data[platform] -end - -local function save_credentials(platform, creds) - local data = {} - if vim.fn.filereadable(credentials_file) == 1 then - local content = vim.fn.readfile(credentials_file) - if #content > 0 then - local ok, decoded = pcall(vim.json.decode, table.concat(content, '\n')) - if ok then - data = decoded - end - end - end - data[platform] = creds - vim.fn.mkdir(vim.fn.fnamemodify(credentials_file, ':h'), 'p') - vim.fn.writefile({ vim.json.encode(data) }, credentials_file) -end - local function prompt_credentials(platform, callback) - local saved = load_credentials(platform) + local saved = cache.get_credentials(platform) if saved and saved.username and saved.password then callback(saved) return @@ -55,7 +23,7 @@ local function prompt_credentials(platform, callback) return end local creds = { username = username, password = password } - save_credentials(platform, creds) + cache.set_credentials(platform, creds) callback(creds) end) end From 3e0b7beabf4c7f1a4db0fcce9112c4dd44e5ec0d Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 16:26:48 -0500 Subject: [PATCH 63/86] feat: add :CP login command for explicit credential management Problem: credentials were only set implicitly on first :CP submit. There was no way to update wrong credentials, log out, or set credentials ahead of time without editing the cache JSON manually. Solution: add :CP login [platform] which always prompts for username and password and overwrites any saved credentials for that platform. Omitting the platform falls back to the active platform. Wire the command through constants, parse_command, handle_command, and add tab-completion (suggests platform names). Document in vimdoc under the SUBMIT section and in the commands reference. --- doc/cp.nvim.txt | 27 +++++++++++++++++++++++---- lua/cp/commands/init.lua | 8 ++++++++ lua/cp/constants.lua | 1 + lua/cp/login.lua | 32 ++++++++++++++++++++++++++++++++ plugin/cp.lua | 2 ++ 5 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 lua/cp/login.lua diff --git a/doc/cp.nvim.txt b/doc/cp.nvim.txt index 4a10227..4a66764 100644 --- a/doc/cp.nvim.txt +++ b/doc/cp.nvim.txt @@ -433,10 +433,20 @@ COMMANDS *cp-commands* Cancel an active race countdown. Submit Commands ~ + :CP login [platform] + Set or update stored credentials for a platform. + Always prompts for username and password, + overwriting any previously saved credentials. + If [platform] is omitted, uses the active platform. + Examples: > + :CP login atcoder + :CP login codeforces +< :CP submit [--lang {language}] Submit the current solution to the online - judge. Prompts for credentials on first use - and stores them for subsequent submissions. + judge. Uses stored credentials (set via + :CP login). Prompts on first use if no + credentials are saved. --lang: Submit solution for a specific language. State Restoration ~ @@ -947,9 +957,18 @@ SUBMIT *cp-submit* Submit the current solution to the online judge. +:CP login [platform] *cp-login* + Set or update stored credentials for a platform. Always prompts for + username and password, overwriting any previously saved credentials. + Omit [platform] to use the currently active platform. + + Credentials are stored in the main cache file under _credentials. + Use this command before submitting for the first time, or any time + you need to update stored credentials. + :CP submit [--lang {language}] - Submit the current solution. Prompts for credentials on first use - and stores them for subsequent submissions. + Submit the current solution. Uses stored credentials (set via + :CP login). Prompts on first use if no credentials are saved. --lang: Override the language to submit. Platform support: diff --git a/lua/cp/commands/init.lua b/lua/cp/commands/init.lua index 1e158c7..3eca340 100644 --- a/lua/cp/commands/init.lua +++ b/lua/cp/commands/init.lua @@ -83,6 +83,12 @@ local function parse_command(args) else return { type = 'action', action = 'interact' } end + elseif first == 'login' then + return { + type = 'action', + action = 'login', + platform = args[2], + } elseif first == 'stress' then return { type = 'action', @@ -317,6 +323,8 @@ function M.handle_command(opts) edit.toggle_edit(cmd.test_index) elseif cmd.action == 'stress' then require('cp.stress').toggle(cmd.generator_cmd, cmd.brute_cmd) + elseif cmd.action == 'login' then + require('cp.login').login(cmd.platform) elseif cmd.action == 'submit' then require('cp.submit').submit({ language = cmd.language }) elseif cmd.action == 'race' then diff --git a/lua/cp/constants.lua b/lua/cp/constants.lua index 21e8f62..02b7089 100644 --- a/lua/cp/constants.lua +++ b/lua/cp/constants.lua @@ -13,6 +13,7 @@ M.ACTIONS = { 'race', 'stress', 'submit', + 'login', } M.PLATFORM_DISPLAY_NAMES = { diff --git a/lua/cp/login.lua b/lua/cp/login.lua new file mode 100644 index 0000000..e1e8f25 --- /dev/null +++ b/lua/cp/login.lua @@ -0,0 +1,32 @@ +local M = {} + +local cache = require('cp.cache') +local logger = require('cp.log') +local state = require('cp.state') + +function M.login(platform) + platform = platform or state.get_platform() + if not platform then + logger.log('No platform specified. Usage: :CP login ', vim.log.levels.ERROR) + return + end + + vim.ui.input({ prompt = platform .. ' username: ' }, function(username) + if not username or username == '' then + logger.log('Login cancelled', vim.log.levels.WARN) + return + end + vim.fn.inputsave() + local password = vim.fn.inputsecret(platform .. ' password: ') + vim.fn.inputrestore() + if not password or password == '' then + logger.log('Login cancelled', vim.log.levels.WARN) + return + end + cache.load() + cache.set_credentials(platform, { username = username, password = password }) + logger.log(platform .. ' credentials saved', vim.log.levels.INFO) + end) +end + +return M diff --git a/plugin/cp.lua b/plugin/cp.lua index e9b159b..51bb858 100644 --- a/plugin/cp.lua +++ b/plugin/cp.lua @@ -103,6 +103,8 @@ end, { end end return filter_candidates(candidates) + elseif args[2] == 'login' then + return filter_candidates(platforms) elseif args[2] == 'race' then local candidates = { 'stop' } vim.list_extend(candidates, platforms) From a04702d87cc93a6cb95f0f85cf431fd61539f34c Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 16:42:35 -0500 Subject: [PATCH 64/86] refactor: replace :CP login with :CP credentials subcommand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: :CP login was a poor API — no way to clear credentials without raw Lua, and the single command didn't scale to multiple operations. Solution: replace login with a :CP credentials subcommand following the same pattern as :CP cache. :CP credentials set [platform] prompts and saves; :CP credentials clear [platform] removes one or all platforms. Add cache.clear_credentials(), rename login.lua to credentials.lua, update parse/dispatch/tab-complete, and rewrite vimdoc accordingly. --- doc/cp.nvim.txt | 48 ++++++++++++++++++--------- lua/cp/cache.lua | 12 +++++++ lua/cp/commands/init.lua | 29 +++++++++++----- lua/cp/constants.lua | 2 +- lua/cp/{login.lua => credentials.lua} | 23 ++++++++++--- plugin/cp.lua | 6 ++-- 6 files changed, 88 insertions(+), 32 deletions(-) rename lua/cp/{login.lua => credentials.lua} (57%) diff --git a/doc/cp.nvim.txt b/doc/cp.nvim.txt index 4a66764..ef4135e 100644 --- a/doc/cp.nvim.txt +++ b/doc/cp.nvim.txt @@ -432,21 +432,29 @@ COMMANDS *cp-commands* :CP race stop Cancel an active race countdown. - Submit Commands ~ - :CP login [platform] + Credential Commands ~ + :CP credentials set [platform] Set or update stored credentials for a platform. Always prompts for username and password, overwriting any previously saved credentials. If [platform] is omitted, uses the active platform. Examples: > - :CP login atcoder - :CP login codeforces + :CP credentials set atcoder + :CP credentials set codeforces < + :CP credentials clear [platform] + Remove stored credentials. Without [platform], + clears credentials for all platforms. + Examples: > + :CP credentials clear atcoder + :CP credentials clear +< + Submit Commands ~ :CP submit [--lang {language}] Submit the current solution to the online judge. Uses stored credentials (set via - :CP login). Prompts on first use if no - credentials are saved. + :CP credentials set). Prompts on first use + if no credentials are saved. --lang: Submit solution for a specific language. State Restoration ~ @@ -952,23 +960,31 @@ Count down to a contest's start time and automatically run setup at T=0. Statusline integration: see |cp-race-status|. +============================================================================== +CREDENTIALS *cp-credentials* + +Manage stored login credentials for platform submission. + +Credentials are stored under _credentials in the main cache file +(stdpath('data')/cp-nvim.json). Use :CP cache read to inspect them. + +:CP credentials set [platform] + Set or update credentials for a platform. Always prompts for + username and password, overwriting any previously saved values. + Omit [platform] to use the currently active platform. + +:CP credentials clear [platform] + Remove stored credentials. Without [platform], clears all platforms. + ============================================================================== SUBMIT *cp-submit* Submit the current solution to the online judge. -:CP login [platform] *cp-login* - Set or update stored credentials for a platform. Always prompts for - username and password, overwriting any previously saved credentials. - Omit [platform] to use the currently active platform. - - Credentials are stored in the main cache file under _credentials. - Use this command before submitting for the first time, or any time - you need to update stored credentials. - :CP submit [--lang {language}] Submit the current solution. Uses stored credentials (set via - :CP login). Prompts on first use if no credentials are saved. + :CP credentials set). Prompts on first use if no credentials + are saved. --lang: Override the language to submit. Platform support: diff --git a/lua/cp/cache.lua b/lua/cp/cache.lua index 60c91de..baea9b9 100644 --- a/lua/cp/cache.lua +++ b/lua/cp/cache.lua @@ -388,6 +388,18 @@ function M.set_credentials(platform, creds) M.save() end +---@param platform string? +function M.clear_credentials(platform) + if platform then + if cache_data._credentials then + cache_data._credentials[platform] = nil + end + else + cache_data._credentials = nil + end + M.save() +end + function M.clear_all() local creds = cache_data._credentials cache_data = {} diff --git a/lua/cp/commands/init.lua b/lua/cp/commands/init.lua index 3eca340..2a6be0a 100644 --- a/lua/cp/commands/init.lua +++ b/lua/cp/commands/init.lua @@ -83,12 +83,20 @@ local function parse_command(args) else return { type = 'action', action = 'interact' } end - elseif first == 'login' then - return { - type = 'action', - action = 'login', - platform = args[2], - } + elseif first == 'credentials' then + local subcommand = args[2] + if not subcommand then + return { type = 'error', message = 'credentials command requires subcommand (set, clear)' } + end + if vim.tbl_contains({ 'set', 'clear' }, subcommand) then + return { + type = 'credentials', + subcommand = subcommand, + platform = args[3], + } + else + return { type = 'error', message = 'unknown credentials subcommand: ' .. subcommand } + end elseif first == 'stress' then return { type = 'action', @@ -323,8 +331,6 @@ function M.handle_command(opts) edit.toggle_edit(cmd.test_index) elseif cmd.action == 'stress' then require('cp.stress').toggle(cmd.generator_cmd, cmd.brute_cmd) - elseif cmd.action == 'login' then - require('cp.login').login(cmd.platform) elseif cmd.action == 'submit' then require('cp.submit').submit({ language = cmd.language }) elseif cmd.action == 'race' then @@ -360,6 +366,13 @@ function M.handle_command(opts) local setup = require('cp.setup') setup.setup_contest(platform, contest_id, problem_id, cmd.language) + elseif cmd.type == 'credentials' then + local creds = require('cp.credentials') + if cmd.subcommand == 'set' then + creds.set(cmd.platform) + elseif cmd.subcommand == 'clear' then + creds.clear(cmd.platform) + end elseif cmd.type == 'cache' then local cache_commands = require('cp.commands.cache') cache_commands.handle_cache_command(cmd) diff --git a/lua/cp/constants.lua b/lua/cp/constants.lua index 02b7089..c1add6a 100644 --- a/lua/cp/constants.lua +++ b/lua/cp/constants.lua @@ -13,7 +13,7 @@ M.ACTIONS = { 'race', 'stress', 'submit', - 'login', + 'credentials', } M.PLATFORM_DISPLAY_NAMES = { diff --git a/lua/cp/login.lua b/lua/cp/credentials.lua similarity index 57% rename from lua/cp/login.lua rename to lua/cp/credentials.lua index e1e8f25..f9b18b9 100644 --- a/lua/cp/login.lua +++ b/lua/cp/credentials.lua @@ -4,29 +4,42 @@ local cache = require('cp.cache') local logger = require('cp.log') local state = require('cp.state') -function M.login(platform) +function M.set(platform) platform = platform or state.get_platform() if not platform then - logger.log('No platform specified. Usage: :CP login ', vim.log.levels.ERROR) + logger.log( + 'No platform specified. Usage: :CP credentials set ', + vim.log.levels.ERROR + ) return end vim.ui.input({ prompt = platform .. ' username: ' }, function(username) if not username or username == '' then - logger.log('Login cancelled', vim.log.levels.WARN) + logger.log('Cancelled', vim.log.levels.WARN) return end vim.fn.inputsave() local password = vim.fn.inputsecret(platform .. ' password: ') vim.fn.inputrestore() if not password or password == '' then - logger.log('Login cancelled', vim.log.levels.WARN) + logger.log('Cancelled', vim.log.levels.WARN) return end cache.load() cache.set_credentials(platform, { username = username, password = password }) - logger.log(platform .. ' credentials saved', vim.log.levels.INFO) + logger.log(platform .. ' credentials saved', vim.log.levels.INFO, true) end) end +function M.clear(platform) + cache.load() + cache.clear_credentials(platform) + if platform then + logger.log(platform .. ' credentials cleared', vim.log.levels.INFO, true) + else + logger.log('all credentials cleared', vim.log.levels.INFO, true) + end +end + return M diff --git a/plugin/cp.lua b/plugin/cp.lua index 51bb858..41e474f 100644 --- a/plugin/cp.lua +++ b/plugin/cp.lua @@ -103,8 +103,8 @@ end, { end end return filter_candidates(candidates) - elseif args[2] == 'login' then - return filter_candidates(platforms) + elseif args[2] == 'credentials' then + return filter_candidates({ 'set', 'clear' }) elseif args[2] == 'race' then local candidates = { 'stop' } vim.list_extend(candidates, platforms) @@ -126,6 +126,8 @@ end, { cache.load() local contests = cache.get_cached_contest_ids(args[3]) return filter_candidates(contests) + elseif args[2] == 'credentials' and vim.tbl_contains({ 'set', 'clear' }, args[3]) then + return filter_candidates(platforms) elseif args[2] == 'cache' and args[3] == 'clear' then local candidates = vim.list_extend({}, platforms) table.insert(candidates, '') From 488260f769f1e7e703d3f7ba2e6a776d69a41d5c Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 16:45:59 -0500 Subject: [PATCH 65/86] ci: format --- lua/cp/credentials.lua | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lua/cp/credentials.lua b/lua/cp/credentials.lua index f9b18b9..c7d0f49 100644 --- a/lua/cp/credentials.lua +++ b/lua/cp/credentials.lua @@ -7,10 +7,7 @@ local state = require('cp.state') function M.set(platform) platform = platform or state.get_platform() if not platform then - logger.log( - 'No platform specified. Usage: :CP credentials set ', - vim.log.levels.ERROR - ) + logger.log('No platform specified. Usage: :CP credentials set ', vim.log.levels.ERROR) return end From 217476f5f39dbf7792bf2b1550d4241a7298127a Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Wed, 4 Mar 2026 00:23:35 -0500 Subject: [PATCH 66/86] fix(scraper): coerce vim.NIL precision to nil before cache write Problem: vim.json.decode maps JSON null to vim.NIL (userdata), but cache.set_test_cases validates precision as number|nil, causing a type error on every scrape where precision is absent. Solution: guard the precision field when building the callback table, converting vim.NIL to nil. --- lua/cp/scraper.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/cp/scraper.lua b/lua/cp/scraper.lua index 6c1242d..194e671 100644 --- a/lua/cp/scraper.lua +++ b/lua/cp/scraper.lua @@ -237,7 +237,7 @@ function M.scrape_all_tests(platform, contest_id, callback) memory_mb = ev.memory_mb or 0, interactive = ev.interactive or false, multi_test = ev.multi_test or false, - precision = ev.precision, + precision = ev.precision ~= vim.NIL and ev.precision or nil, problem_id = ev.problem_id, }) end From 4f88b19a8292682aeee5ed0b1e69668164effcf9 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Wed, 4 Mar 2026 00:23:52 -0500 Subject: [PATCH 67/86] refactor(run): remove I/O view test navigation keymaps Problem: / in the I/O view buffers required the cursor to leave the source file to work, re-ran the solution on each press, and gave no indication of which test was active. The workflow is better served by :CP run for a specific test or :CP panel for full inspection. Solution: remove navigate_test, next_test_key/prev_test_key config options, and the associated current_test_index state field. --- lua/cp/config.lua | 18 ------------------ lua/cp/state.lua | 1 - lua/cp/ui/views.lua | 44 -------------------------------------------- 3 files changed, 63 deletions(-) diff --git a/lua/cp/config.lua b/lua/cp/config.lua index fcebc0e..ec6d75b 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -78,8 +78,6 @@ ---@class RunConfig ---@field width number ----@field next_test_key string|nil ----@field prev_test_key string|nil ---@field format_verdict VerdictFormatter ---@class EditConfig @@ -196,8 +194,6 @@ M.defaults = { ansi = true, run = { width = 0.3, - next_test_key = '', - prev_test_key = '', format_verdict = helpers.default_verdict_formatter, }, edit = { @@ -448,20 +444,6 @@ function M.setup(user_config) end, 'decimal between 0 and 1', }, - next_test_key = { - cfg.ui.run.next_test_key, - function(v) - return v == nil or (type(v) == 'string' and #v > 0) - end, - 'nil or non-empty string', - }, - prev_test_key = { - cfg.ui.run.prev_test_key, - function(v) - return v == nil or (type(v) == 'string' and #v > 0) - end, - 'nil or non-empty string', - }, format_verdict = { cfg.ui.run.format_verdict, 'function', diff --git a/lua/cp/state.lua b/lua/cp/state.lua index 6d99cbf..f0f4850 100644 --- a/lua/cp/state.lua +++ b/lua/cp/state.lua @@ -9,7 +9,6 @@ ---@class cp.IoViewState ---@field output_buf integer ---@field input_buf integer ----@field current_test_index integer? ---@field source_buf integer? ---@class cp.State diff --git a/lua/cp/ui/views.lua b/lua/cp/ui/views.lua index 041200e..0849164 100644 --- a/lua/cp/ui/views.lua +++ b/lua/cp/ui/views.lua @@ -240,7 +240,6 @@ local function get_or_create_io_buffers() state.set_io_view_state({ output_buf = output_buf, input_buf = input_buf, - current_test_index = 1, source_buf = current_source_buf, }) @@ -305,49 +304,6 @@ local function get_or_create_io_buffers() end, }) - local cfg = config_module.get_config() - local platform = state.get_platform() - local contest_id = state.get_contest_id() - local problem_id = state.get_problem_id() - - local function navigate_test(delta) - local io_view_state = state.get_io_view_state() - if not io_view_state then - return - end - if not platform or not contest_id or not problem_id then - return - end - local test_cases = cache.get_test_cases(platform, contest_id, problem_id) - if not test_cases or #test_cases == 0 then - return - end - local new_index = (io_view_state.current_test_index or 1) + delta - if new_index < 1 or new_index > #test_cases then - return - end - io_view_state.current_test_index = new_index - M.run_io_view(new_index) - end - - if cfg.ui.run.next_test_key then - vim.keymap.set('n', cfg.ui.run.next_test_key, function() - navigate_test(1) - end, { buffer = output_buf, silent = true, desc = 'Next test' }) - vim.keymap.set('n', cfg.ui.run.next_test_key, function() - navigate_test(1) - end, { buffer = input_buf, silent = true, desc = 'Next test' }) - end - - if cfg.ui.run.prev_test_key then - vim.keymap.set('n', cfg.ui.run.prev_test_key, function() - navigate_test(-1) - end, { buffer = output_buf, silent = true, desc = 'Previous test' }) - vim.keymap.set('n', cfg.ui.run.prev_test_key, function() - navigate_test(-1) - end, { buffer = input_buf, silent = true, desc = 'Previous test' }) - end - return output_buf, input_buf end From f17eb32e8c79156638cd17021acab174ac947a79 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Wed, 4 Mar 2026 00:29:33 -0500 Subject: [PATCH 68/86] fix: pass in index to :CP panel --- lua/cp/ui/views.lua | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lua/cp/ui/views.lua b/lua/cp/ui/views.lua index 0849164..5e658c4 100644 --- a/lua/cp/ui/views.lua +++ b/lua/cp/ui/views.lua @@ -2,6 +2,7 @@ local M = {} ---@class PanelOpts ---@field debug? boolean +---@field test_index? integer local cache = require('cp.cache') local config_module = require('cp.config') @@ -806,6 +807,13 @@ function M.toggle_panel(panel_opts) return end + if panel_opts and panel_opts.test_index then + local test_state = run.get_panel_state() + if panel_opts.test_index >= 1 and panel_opts.test_index <= #test_state.test_cases then + test_state.current_index = panel_opts.test_index + end + end + local io_state = state.get_io_view_state() if io_state then for _, win in ipairs(vim.api.nvim_list_wins()) do From 900fd70935b5a6f7acfd2958cdb4bc492f7af57b Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Wed, 4 Mar 2026 00:43:33 -0500 Subject: [PATCH 69/86] fix(edit): clean up buffers on close and support :w to save Problem: closing the test editor left cp://test-N-* buffers alive, causing E95 on reopen. The nofile buftype also rejected :w, which was counterintuitive in an editable grid. Solution: delete all test buffers in toggle_edit teardown. Switch buftype to acwrite with a BufWriteCmd autocmd that persists test cases and clears the modified flag. Hoist save_all_tests above setup_keybindings so the autocmd closure can reference it. --- lua/cp/ui/edit.lua | 175 +++++++++++++++++++++++++-------------------- 1 file changed, 97 insertions(+), 78 deletions(-) diff --git a/lua/cp/ui/edit.lua b/lua/cp/ui/edit.lua index 886c50a..5157b2e 100644 --- a/lua/cp/ui/edit.lua +++ b/lua/cp/ui/edit.lua @@ -144,7 +144,7 @@ local function add_new_test() vim.api.nvim_win_set_buf(input_win, input_buf) vim.bo[input_buf].modifiable = true vim.bo[input_buf].readonly = false - vim.bo[input_buf].buftype = 'nofile' + vim.bo[input_buf].buftype = 'acwrite' vim.bo[input_buf].buflisted = false helpers.clearcol(input_buf) @@ -155,7 +155,7 @@ local function add_new_test() vim.api.nvim_win_set_buf(expected_win, expected_buf) vim.bo[expected_buf].modifiable = true vim.bo[expected_buf].readonly = false - vim.bo[expected_buf].buftype = 'nofile' + vim.bo[expected_buf].buftype = 'acwrite' vim.bo[expected_buf].buflisted = false helpers.clearcol(expected_buf) @@ -177,6 +177,80 @@ local function add_new_test() logger.log(('Added test %d'):format(new_index)) end +local function save_all_tests() + if not edit_state then + return + end + + local platform = state.get_platform() + local contest_id = state.get_contest_id() + local problem_id = state.get_problem_id() + + if not platform or not contest_id or not problem_id then + return + end + + for i, pair in ipairs(edit_state.test_buffers) do + if + vim.api.nvim_buf_is_valid(pair.input_buf) and vim.api.nvim_buf_is_valid(pair.expected_buf) + then + local input_lines = vim.api.nvim_buf_get_lines(pair.input_buf, 0, -1, false) + local expected_lines = vim.api.nvim_buf_get_lines(pair.expected_buf, 0, -1, false) + + edit_state.test_cases[i].input = table.concat(input_lines, '\n') + edit_state.test_cases[i].expected = table.concat(expected_lines, '\n') + end + end + + local contest_data = cache.get_contest_data(platform, contest_id) + local is_multi_test = contest_data.problems[contest_data.index_map[problem_id]].multi_test + or false + + local combined_input = table.concat( + vim.tbl_map(function(tc) + return tc.input + end, edit_state.test_cases), + '\n' + ) + local combined_expected = table.concat( + vim.tbl_map(function(tc) + return tc.expected + end, edit_state.test_cases), + '\n' + ) + + cache.set_test_cases( + platform, + contest_id, + problem_id, + { input = combined_input, expected = combined_expected }, + edit_state.test_cases, + edit_state.constraints and edit_state.constraints.timeout_ms or 0, + edit_state.constraints and edit_state.constraints.memory_mb or 0, + false, + is_multi_test + ) + + local config = config_module.get_config() + local base_name = config.filename and config.filename(platform, contest_id, problem_id, config) + or config_module.default_filename(contest_id, problem_id) + + vim.fn.mkdir('io', 'p') + + for i, tc in ipairs(edit_state.test_cases) do + local input_file = string.format('io/%s.%d.cpin', base_name, i) + local expected_file = string.format('io/%s.%d.cpout', base_name, i) + + local input_content = (tc.input or ''):gsub('\r', '') + local expected_content = (tc.expected or ''):gsub('\r', '') + + vim.fn.writefile(vim.split(input_content, '\n', { trimempty = true }), input_file) + vim.fn.writefile(vim.split(expected_content, '\n', { trimempty = true }), expected_file) + end + + logger.log('Saved all test cases') +end + ---@param buf integer setup_keybindings = function(buf) local config = config_module.get_config() @@ -243,86 +317,31 @@ setup_keybindings = function(buf) end) end, }) + + vim.api.nvim_create_autocmd('BufWriteCmd', { + group = augroup, + buffer = buf, + callback = function() + save_all_tests() + vim.bo[buf].modified = false + end, + }) end -local function save_all_tests() - if not edit_state then - return - end - - local platform = state.get_platform() - local contest_id = state.get_contest_id() - local problem_id = state.get_problem_id() - - if not platform or not contest_id or not problem_id then - return - end - - for i, pair in ipairs(edit_state.test_buffers) do - if - vim.api.nvim_buf_is_valid(pair.input_buf) and vim.api.nvim_buf_is_valid(pair.expected_buf) - then - local input_lines = vim.api.nvim_buf_get_lines(pair.input_buf, 0, -1, false) - local expected_lines = vim.api.nvim_buf_get_lines(pair.expected_buf, 0, -1, false) - - edit_state.test_cases[i].input = table.concat(input_lines, '\n') - edit_state.test_cases[i].expected = table.concat(expected_lines, '\n') - end - end - - local contest_data = cache.get_contest_data(platform, contest_id) - local is_multi_test = contest_data.problems[contest_data.index_map[problem_id]].multi_test - or false - - -- Generate combined test from individual test cases - local combined_input = table.concat( - vim.tbl_map(function(tc) - return tc.input - end, edit_state.test_cases), - '\n' - ) - local combined_expected = table.concat( - vim.tbl_map(function(tc) - return tc.expected - end, edit_state.test_cases), - '\n' - ) - - cache.set_test_cases( - platform, - contest_id, - problem_id, - { input = combined_input, expected = combined_expected }, - edit_state.test_cases, - edit_state.constraints and edit_state.constraints.timeout_ms or 0, - edit_state.constraints and edit_state.constraints.memory_mb or 0, - false, - is_multi_test - ) - - local config = config_module.get_config() - local base_name = config.filename and config.filename(platform, contest_id, problem_id, config) - or config_module.default_filename(contest_id, problem_id) - - vim.fn.mkdir('io', 'p') - - for i, tc in ipairs(edit_state.test_cases) do - local input_file = string.format('io/%s.%d.cpin', base_name, i) - local expected_file = string.format('io/%s.%d.cpout', base_name, i) - - local input_content = (tc.input or ''):gsub('\r', '') - local expected_content = (tc.expected or ''):gsub('\r', '') - - vim.fn.writefile(vim.split(input_content, '\n', { trimempty = true }), input_file) - vim.fn.writefile(vim.split(expected_content, '\n', { trimempty = true }), expected_file) - end - - logger.log('Saved all test cases') -end function M.toggle_edit(test_index) if edit_state then save_all_tests() + + for _, pair in ipairs(edit_state.test_buffers) do + if vim.api.nvim_buf_is_valid(pair.input_buf) then + vim.api.nvim_buf_delete(pair.input_buf, { force = true }) + end + if vim.api.nvim_buf_is_valid(pair.expected_buf) then + vim.api.nvim_buf_delete(pair.expected_buf, { force = true }) + end + end + edit_state = nil pcall(vim.api.nvim_clear_autocmds, { group = 'cp_edit_guard' }) @@ -411,7 +430,7 @@ function M.toggle_edit(test_index) vim.api.nvim_win_set_buf(input_win, input_buf) vim.bo[input_buf].modifiable = true vim.bo[input_buf].readonly = false - vim.bo[input_buf].buftype = 'nofile' + vim.bo[input_buf].buftype = 'acwrite' vim.bo[input_buf].buflisted = false helpers.clearcol(input_buf) @@ -421,7 +440,7 @@ function M.toggle_edit(test_index) vim.api.nvim_win_set_buf(expected_win, expected_buf) vim.bo[expected_buf].modifiable = true vim.bo[expected_buf].readonly = false - vim.bo[expected_buf].buftype = 'nofile' + vim.bo[expected_buf].buftype = 'acwrite' vim.bo[expected_buf].buflisted = false helpers.clearcol(expected_buf) From baaaa95b27b5cf461116a89d6992f13db0103512 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Wed, 4 Mar 2026 00:50:10 -0500 Subject: [PATCH 70/86] ci: format --- lua/cp/ui/edit.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/lua/cp/ui/edit.lua b/lua/cp/ui/edit.lua index 5157b2e..20d4e83 100644 --- a/lua/cp/ui/edit.lua +++ b/lua/cp/ui/edit.lua @@ -328,7 +328,6 @@ setup_keybindings = function(buf) }) end - function M.toggle_edit(test_index) if edit_state then save_all_tests() From 18a60da2d896398299385a2e278245303d443cce Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:47:48 -0500 Subject: [PATCH 71/86] misc (#290) fix atcoder :CP logins propagate scraper error codes --- lua/cp/scraper.lua | 16 +++++----- scrapers/atcoder.py | 73 +++++++++++++++++++++++++++++++++++++-------- 2 files changed, 68 insertions(+), 21 deletions(-) diff --git a/lua/cp/scraper.lua b/lua/cp/scraper.lua index 194e671..7f774c3 100644 --- a/lua/cp/scraper.lua +++ b/lua/cp/scraper.lua @@ -5,19 +5,19 @@ local logger = require('cp.log') local utils = require('cp.utils') local function syshandle(result) + local ok, data = pcall(vim.json.decode, result.stdout or '') + if ok then + return { success = true, data = data } + end + if result.code ~= 0 then local msg = 'Scraper failed: ' .. (result.stderr or 'Unknown error') return { success = false, error = msg } end - local ok, data = pcall(vim.json.decode, result.stdout) - if not ok then - local msg = 'Failed to parse scraper output: ' .. tostring(data) - logger.log(msg, vim.log.levels.ERROR) - return { success = false, error = msg } - end - - return { success = true, data = data } + local msg = 'Failed to parse scraper output: ' .. tostring(data) + logger.log(msg, vim.log.levels.ERROR) + return { success = false, error = msg } end ---@param env_map table diff --git a/scrapers/atcoder.py b/scrapers/atcoder.py index 14debdc..f7124ed 100644 --- a/scrapers/atcoder.py +++ b/scrapers/atcoder.py @@ -2,6 +2,7 @@ import asyncio import json +import os import re import sys import time @@ -15,6 +16,7 @@ from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry from .base import BaseScraper, extract_precision +from .language_ids import get_language_id from .models import ( CombinedTest, ContestListResult, @@ -378,10 +380,12 @@ class AtcoderScraper(BaseScraper): credentials: dict[str, str], ) -> SubmitResult: def _submit_sync() -> SubmitResult: + from curl_cffi import requests as curl_requests + try: - login_page = _session.get( - f"{BASE_URL}/login", headers=HEADERS, timeout=TIMEOUT_SECONDS - ) + session = curl_requests.Session(impersonate="chrome") + + login_page = session.get(f"{BASE_URL}/login", timeout=TIMEOUT_SECONDS) login_page.raise_for_status() soup = BeautifulSoup(login_page.text, "html.parser") csrf_input = soup.find("input", {"name": "csrf_token"}) @@ -391,21 +395,29 @@ class AtcoderScraper(BaseScraper): ) csrf_token = csrf_input.get("value", "") or "" # type: ignore[union-attr] - login_resp = _session.post( + login_resp = session.post( f"{BASE_URL}/login", data={ "username": credentials.get("username", ""), "password": credentials.get("password", ""), "csrf_token": csrf_token, }, - headers=HEADERS, timeout=TIMEOUT_SECONDS, + allow_redirects=False, ) - login_resp.raise_for_status() + if login_resp.status_code in (301, 302): + location = login_resp.headers.get("Location", "") + if "/login" in location: + return SubmitResult( + success=False, + error="Login failed: incorrect username or password", + ) + session.get(BASE_URL + location, timeout=TIMEOUT_SECONDS) + else: + login_resp.raise_for_status() - submit_page = _session.get( + submit_page = session.get( f"{BASE_URL}/contests/{contest_id}/submit", - headers=HEADERS, timeout=TIMEOUT_SECONDS, ) submit_page.raise_for_status() @@ -418,7 +430,7 @@ class AtcoderScraper(BaseScraper): csrf_token = csrf_input.get("value", "") or "" # type: ignore[union-attr] task_screen_name = f"{contest_id}_{problem_id}" - submit_resp = _session.post( + submit_resp = session.post( f"{BASE_URL}/contests/{contest_id}/submit", data={ "data.TaskScreenName": task_screen_name, @@ -426,13 +438,26 @@ class AtcoderScraper(BaseScraper): "sourceCode": source_code, "csrf_token": csrf_token, }, - headers=HEADERS, timeout=TIMEOUT_SECONDS, + allow_redirects=False, ) + if submit_resp.status_code in (301, 302): + location = submit_resp.headers.get("Location", "") + if "/submissions/me" in location: + return SubmitResult( + success=True, + error="", + submission_id="", + verdict="submitted", + ) + return SubmitResult( + success=False, + error=f"Submit may have failed: redirected to {location}", + ) submit_resp.raise_for_status() - return SubmitResult( - success=True, error="", submission_id="", verdict="submitted" + success=False, + error="Unexpected response from submit (expected redirect)", ) except Exception as e: return SubmitResult(success=False, error=str(e)) @@ -495,9 +520,31 @@ async def main_async() -> int: print(contest_result.model_dump_json()) return 0 if contest_result.success else 1 + if mode == "submit": + if len(sys.argv) != 5: + print( + SubmitResult( + success=False, + error="Usage: atcoder.py submit ", + ).model_dump_json() + ) + return 1 + source_code = sys.stdin.read() + creds_raw = os.environ.get("CP_CREDENTIALS", "{}") + try: + credentials = json.loads(creds_raw) + except json.JSONDecodeError: + credentials = {} + language_id = get_language_id("atcoder", sys.argv[4]) or sys.argv[4] + submit_result = await scraper.submit( + sys.argv[2], sys.argv[3], source_code, language_id, credentials + ) + print(submit_result.model_dump_json()) + return 0 if submit_result.success else 1 + result = MetadataResult( success=False, - error="Unknown mode. Use 'metadata ', 'tests ', or 'contests'", + error="Unknown mode. Use 'metadata ', 'tests ', 'contests', or 'submit '", url="", ) print(result.model_dump_json()) From 98ac0aa7a77eafb14df86bdb8a71e3effc8f65ca Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:53:37 -0500 Subject: [PATCH 72/86] refactor(credentials): rename set/clear to login/logout/clear (#291) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The `set` and `clear` subcommands don't clearly convey their intent — `set` reads like a generic setter rather than an auth action, and `clear` overloads single-platform and all-platform semantics in one subcommand. ## Solution Rename `set` to `login`, split `clear` into `logout` (per-platform, defaults to active) and `clear` (all platforms). New API: - `:CP credentials login [platform]` — prompt and save credentials - `:CP credentials logout [platform]` — remove credentials for one platform - `:CP credentials clear` — remove all stored credentials --- doc/cp.nvim.txt | 34 ++++++++++++++++++++-------------- lua/cp/commands/init.lua | 20 +++++++++++++++----- lua/cp/credentials.lua | 29 +++++++++++++++++++++-------- plugin/cp.lua | 4 ++-- 4 files changed, 58 insertions(+), 29 deletions(-) diff --git a/doc/cp.nvim.txt b/doc/cp.nvim.txt index ef4135e..31b9d8a 100644 --- a/doc/cp.nvim.txt +++ b/doc/cp.nvim.txt @@ -433,27 +433,29 @@ COMMANDS *cp-commands* Cancel an active race countdown. Credential Commands ~ - :CP credentials set [platform] + :CP credentials login [platform] Set or update stored credentials for a platform. Always prompts for username and password, overwriting any previously saved credentials. If [platform] is omitted, uses the active platform. Examples: > - :CP credentials set atcoder - :CP credentials set codeforces + :CP credentials login atcoder + :CP credentials login codeforces < - :CP credentials clear [platform] - Remove stored credentials. Without [platform], - clears credentials for all platforms. + :CP credentials logout [platform] + Remove stored credentials for a platform. + If [platform] is omitted, uses the active platform. Examples: > - :CP credentials clear atcoder - :CP credentials clear + :CP credentials logout atcoder +< + :CP credentials clear + Remove stored credentials for all platforms. < Submit Commands ~ :CP submit [--lang {language}] Submit the current solution to the online judge. Uses stored credentials (set via - :CP credentials set). Prompts on first use + :CP credentials login). Prompts on first use if no credentials are saved. --lang: Submit solution for a specific language. @@ -714,7 +716,7 @@ Example: Setting up and solving AtCoder contest ABC324 8. Submit solution: > :CP submit -< Prompts for credentials on first use and submits to AtCoder. +< Uses stored credentials and submits to AtCoder. ============================================================================== I/O VIEW *cp-io-view* @@ -968,13 +970,17 @@ Manage stored login credentials for platform submission. Credentials are stored under _credentials in the main cache file (stdpath('data')/cp-nvim.json). Use :CP cache read to inspect them. -:CP credentials set [platform] +:CP credentials login [platform] Set or update credentials for a platform. Always prompts for username and password, overwriting any previously saved values. Omit [platform] to use the currently active platform. -:CP credentials clear [platform] - Remove stored credentials. Without [platform], clears all platforms. +:CP credentials logout [platform] + Remove stored credentials for a platform. + Omit [platform] to use the currently active platform. + +:CP credentials clear + Remove stored credentials for all platforms. ============================================================================== SUBMIT *cp-submit* @@ -983,7 +989,7 @@ Submit the current solution to the online judge. :CP submit [--lang {language}] Submit the current solution. Uses stored credentials (set via - :CP credentials set). Prompts on first use if no credentials + :CP credentials login). Prompts on first use if no credentials are saved. --lang: Override the language to submit. diff --git a/lua/cp/commands/init.lua b/lua/cp/commands/init.lua index 2a6be0a..96a3db3 100644 --- a/lua/cp/commands/init.lua +++ b/lua/cp/commands/init.lua @@ -86,14 +86,22 @@ local function parse_command(args) elseif first == 'credentials' then local subcommand = args[2] if not subcommand then - return { type = 'error', message = 'credentials command requires subcommand (set, clear)' } + return { + type = 'error', + message = 'credentials command requires subcommand (login, logout, clear)', + } end - if vim.tbl_contains({ 'set', 'clear' }, subcommand) then + if vim.tbl_contains({ 'login', 'logout' }, subcommand) then return { type = 'credentials', subcommand = subcommand, platform = args[3], } + elseif subcommand == 'clear' then + return { + type = 'credentials', + subcommand = 'clear', + } else return { type = 'error', message = 'unknown credentials subcommand: ' .. subcommand } end @@ -368,10 +376,12 @@ function M.handle_command(opts) setup.setup_contest(platform, contest_id, problem_id, cmd.language) elseif cmd.type == 'credentials' then local creds = require('cp.credentials') - if cmd.subcommand == 'set' then - creds.set(cmd.platform) + if cmd.subcommand == 'login' then + creds.login(cmd.platform) + elseif cmd.subcommand == 'logout' then + creds.logout(cmd.platform) elseif cmd.subcommand == 'clear' then - creds.clear(cmd.platform) + creds.clear() end elseif cmd.type == 'cache' then local cache_commands = require('cp.commands.cache') diff --git a/lua/cp/credentials.lua b/lua/cp/credentials.lua index c7d0f49..3054344 100644 --- a/lua/cp/credentials.lua +++ b/lua/cp/credentials.lua @@ -4,10 +4,13 @@ local cache = require('cp.cache') local logger = require('cp.log') local state = require('cp.state') -function M.set(platform) +function M.login(platform) platform = platform or state.get_platform() if not platform then - logger.log('No platform specified. Usage: :CP credentials set ', vim.log.levels.ERROR) + logger.log( + 'No platform specified. Usage: :CP credentials login ', + vim.log.levels.ERROR + ) return end @@ -29,14 +32,24 @@ function M.set(platform) end) end -function M.clear(platform) +function M.logout(platform) + platform = platform or state.get_platform() + if not platform then + logger.log( + 'No platform specified. Usage: :CP credentials logout ', + vim.log.levels.ERROR + ) + return + end cache.load() cache.clear_credentials(platform) - if platform then - logger.log(platform .. ' credentials cleared', vim.log.levels.INFO, true) - else - logger.log('all credentials cleared', vim.log.levels.INFO, true) - end + logger.log(platform .. ' credentials cleared', vim.log.levels.INFO, true) +end + +function M.clear() + cache.load() + cache.clear_credentials(nil) + logger.log('all credentials cleared', vim.log.levels.INFO, true) end return M diff --git a/plugin/cp.lua b/plugin/cp.lua index 41e474f..a1d3c05 100644 --- a/plugin/cp.lua +++ b/plugin/cp.lua @@ -104,7 +104,7 @@ end, { end return filter_candidates(candidates) elseif args[2] == 'credentials' then - return filter_candidates({ 'set', 'clear' }) + return filter_candidates({ 'login', 'logout', 'clear' }) elseif args[2] == 'race' then local candidates = { 'stop' } vim.list_extend(candidates, platforms) @@ -126,7 +126,7 @@ end, { cache.load() local contests = cache.get_cached_contest_ids(args[3]) return filter_candidates(contests) - elseif args[2] == 'credentials' and vim.tbl_contains({ 'set', 'clear' }, args[3]) then + elseif args[2] == 'credentials' and vim.tbl_contains({ 'login', 'logout' }, args[3]) then return filter_candidates(platforms) elseif args[2] == 'cache' and args[3] == 'clear' then local candidates = vim.list_extend({}, platforms) From 49e0ae388578059283be7e285da2a6b49d64f3f0 Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:09:32 -0500 Subject: [PATCH 73/86] refactor(credentials): promote login/logout to top-level actions (#292) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem `:CP credentials login/logout/clear` is verbose and inconsistent with other actions that are all top-level (`:CP run`, `:CP submit`, etc.). The clear-all subcommand is also unnecessary since re-logging in overwrites existing credentials. ## Solution Replace `:CP credentials {login,logout,clear}` with `:CP login [platform]` and `:CP logout [platform]`. Remove the clear-all command and the credentials subcommand dispatch — login/logout are now regular actions routed through the standard action dispatcher. --- doc/cp.nvim.txt | 39 ++++++++++++++++----------------------- lua/cp/commands/init.lua | 37 ++++++------------------------------- lua/cp/constants.lua | 3 ++- lua/cp/credentials.lua | 16 ++-------------- plugin/cp.lua | 6 ++---- 5 files changed, 28 insertions(+), 73 deletions(-) diff --git a/doc/cp.nvim.txt b/doc/cp.nvim.txt index 31b9d8a..f61d39e 100644 --- a/doc/cp.nvim.txt +++ b/doc/cp.nvim.txt @@ -433,30 +433,27 @@ COMMANDS *cp-commands* Cancel an active race countdown. Credential Commands ~ - :CP credentials login [platform] + :CP login [platform] Set or update stored credentials for a platform. - Always prompts for username and password, - overwriting any previously saved credentials. + Prompts for username and password, overwriting + any previously saved credentials. If [platform] is omitted, uses the active platform. Examples: > - :CP credentials login atcoder - :CP credentials login codeforces + :CP login atcoder + :CP login codeforces < - :CP credentials logout [platform] + :CP logout [platform] Remove stored credentials for a platform. If [platform] is omitted, uses the active platform. Examples: > - :CP credentials logout atcoder -< - :CP credentials clear - Remove stored credentials for all platforms. + :CP logout atcoder < Submit Commands ~ :CP submit [--lang {language}] Submit the current solution to the online judge. Uses stored credentials (set via - :CP credentials login). Prompts on first use - if no credentials are saved. + :CP login). Prompts on first use if no + credentials are saved. --lang: Submit solution for a specific language. State Restoration ~ @@ -963,34 +960,30 @@ Count down to a contest's start time and automatically run setup at T=0. Statusline integration: see |cp-race-status|. ============================================================================== -CREDENTIALS *cp-credentials* +CREDENTIALS *cp-credentials* Manage stored login credentials for platform submission. Credentials are stored under _credentials in the main cache file (stdpath('data')/cp-nvim.json). Use :CP cache read to inspect them. -:CP credentials login [platform] - Set or update credentials for a platform. Always prompts for - username and password, overwriting any previously saved values. +:CP login [platform] + Set or update credentials for a platform. Prompts for username + and password, overwriting any previously saved values. Omit [platform] to use the currently active platform. -:CP credentials logout [platform] +:CP logout [platform] Remove stored credentials for a platform. Omit [platform] to use the currently active platform. -:CP credentials clear - Remove stored credentials for all platforms. - ============================================================================== -SUBMIT *cp-submit* +SUBMIT *cp-submit* Submit the current solution to the online judge. :CP submit [--lang {language}] Submit the current solution. Uses stored credentials (set via - :CP credentials login). Prompts on first use if no credentials - are saved. + :CP login). Prompts on first use if no credentials are saved. --lang: Override the language to submit. Platform support: diff --git a/lua/cp/commands/init.lua b/lua/cp/commands/init.lua index 96a3db3..56a473f 100644 --- a/lua/cp/commands/init.lua +++ b/lua/cp/commands/init.lua @@ -83,28 +83,8 @@ local function parse_command(args) else return { type = 'action', action = 'interact' } end - elseif first == 'credentials' then - local subcommand = args[2] - if not subcommand then - return { - type = 'error', - message = 'credentials command requires subcommand (login, logout, clear)', - } - end - if vim.tbl_contains({ 'login', 'logout' }, subcommand) then - return { - type = 'credentials', - subcommand = subcommand, - platform = args[3], - } - elseif subcommand == 'clear' then - return { - type = 'credentials', - subcommand = 'clear', - } - else - return { type = 'error', message = 'unknown credentials subcommand: ' .. subcommand } - end + elseif first == 'login' or first == 'logout' then + return { type = 'action', action = first, platform = args[2] } elseif first == 'stress' then return { type = 'action', @@ -345,6 +325,10 @@ function M.handle_command(opts) require('cp.race').start(cmd.platform, cmd.contest, cmd.language) elseif cmd.action == 'race_stop' then require('cp.race').stop() + elseif cmd.action == 'login' then + require('cp.credentials').login(cmd.platform) + elseif cmd.action == 'logout' then + require('cp.credentials').logout(cmd.platform) end elseif cmd.type == 'problem_jump' then local platform = state.get_platform() @@ -374,15 +358,6 @@ function M.handle_command(opts) local setup = require('cp.setup') setup.setup_contest(platform, contest_id, problem_id, cmd.language) - elseif cmd.type == 'credentials' then - local creds = require('cp.credentials') - if cmd.subcommand == 'login' then - creds.login(cmd.platform) - elseif cmd.subcommand == 'logout' then - creds.logout(cmd.platform) - elseif cmd.subcommand == 'clear' then - creds.clear() - end elseif cmd.type == 'cache' then local cache_commands = require('cp.commands.cache') cache_commands.handle_cache_command(cmd) diff --git a/lua/cp/constants.lua b/lua/cp/constants.lua index c1add6a..5d1c3c5 100644 --- a/lua/cp/constants.lua +++ b/lua/cp/constants.lua @@ -13,7 +13,8 @@ M.ACTIONS = { 'race', 'stress', 'submit', - 'credentials', + 'login', + 'logout', } M.PLATFORM_DISPLAY_NAMES = { diff --git a/lua/cp/credentials.lua b/lua/cp/credentials.lua index 3054344..8f80743 100644 --- a/lua/cp/credentials.lua +++ b/lua/cp/credentials.lua @@ -7,10 +7,7 @@ local state = require('cp.state') function M.login(platform) platform = platform or state.get_platform() if not platform then - logger.log( - 'No platform specified. Usage: :CP credentials login ', - vim.log.levels.ERROR - ) + logger.log('No platform specified. Usage: :CP login ', vim.log.levels.ERROR) return end @@ -35,10 +32,7 @@ end function M.logout(platform) platform = platform or state.get_platform() if not platform then - logger.log( - 'No platform specified. Usage: :CP credentials logout ', - vim.log.levels.ERROR - ) + logger.log('No platform specified. Usage: :CP logout ', vim.log.levels.ERROR) return end cache.load() @@ -46,10 +40,4 @@ function M.logout(platform) logger.log(platform .. ' credentials cleared', vim.log.levels.INFO, true) end -function M.clear() - cache.load() - cache.clear_credentials(nil) - logger.log('all credentials cleared', vim.log.levels.INFO, true) -end - return M diff --git a/plugin/cp.lua b/plugin/cp.lua index a1d3c05..db9a8bd 100644 --- a/plugin/cp.lua +++ b/plugin/cp.lua @@ -103,8 +103,8 @@ end, { end end return filter_candidates(candidates) - elseif args[2] == 'credentials' then - return filter_candidates({ 'login', 'logout', 'clear' }) + elseif args[2] == 'login' or args[2] == 'logout' then + return filter_candidates(platforms) elseif args[2] == 'race' then local candidates = { 'stop' } vim.list_extend(candidates, platforms) @@ -126,8 +126,6 @@ end, { cache.load() local contests = cache.get_cached_contest_ids(args[3]) return filter_candidates(contests) - elseif args[2] == 'credentials' and vim.tbl_contains({ 'login', 'logout' }, args[3]) then - return filter_candidates(platforms) elseif args[2] == 'cache' and args[3] == 'clear' then local candidates = vim.list_extend({}, platforms) table.insert(candidates, '') From 1bc0aa41b673801e7b8e0fd2e31905ecc787c500 Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:37:22 -0500 Subject: [PATCH 74/86] refactor(cache): nest credentials under platform namespace (#293) ## Problem Credentials lived in a top-level `_credentials` namespace, requiring special preservation logic in `clear_all()` and a separate key hierarchy from the platform data they belong to. ## Solution Move credentials from `_credentials.` to `._credentials`. Migrate v1 caches on load, skip underscore-prefixed keys when enumerating contest IDs and summaries, and simplify `clear_all()` now that no special preservation is needed. Stacked on #292. --- lua/cp/cache.lua | 66 ++++++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/lua/cp/cache.lua b/lua/cp/cache.lua index baea9b9..eb82afa 100644 --- a/lua/cp/cache.lua +++ b/lua/cp/cache.lua @@ -40,7 +40,7 @@ local M = {} -local CACHE_VERSION = 1 +local CACHE_VERSION = 2 local cache_file = vim.fn.stdpath('data') .. '/cp-nvim.json' local cache_data = {} @@ -67,13 +67,27 @@ function M.load() end local ok, decoded = pcall(vim.json.decode, table.concat(content, '\n')) - if ok then - if decoded._version ~= CACHE_VERSION then - cache_data = {} - M.save() - else - cache_data = decoded + if not ok then + cache_data = {} + M.save() + loaded = true + return + end + + if decoded._version == 1 then + local old_creds = decoded._credentials + decoded._credentials = nil + if old_creds then + for platform, creds in pairs(old_creds) do + decoded[platform] = decoded[platform] or {} + decoded[platform]._credentials = creds + end end + decoded._version = CACHE_VERSION + cache_data = decoded + M.save() + elseif decoded._version == CACHE_VERSION then + cache_data = decoded else cache_data = {} M.save() @@ -122,7 +136,9 @@ function M.get_cached_contest_ids(platform) local contest_ids = {} for contest_id, _ in pairs(cache_data[platform]) do - table.insert(contest_ids, contest_id) + if contest_id:sub(1, 1) ~= '_' then + table.insert(contest_ids, contest_id) + end end table.sort(contest_ids) return contest_ids @@ -336,11 +352,13 @@ end function M.get_contest_summaries(platform) local contest_list = {} for contest_id, contest_data in pairs(cache_data[platform] or {}) do - table.insert(contest_list, { - id = contest_id, - name = contest_data.name, - display_name = contest_data.display_name, - }) + if contest_id:sub(1, 1) ~= '_' then + table.insert(contest_list, { + id = contest_id, + name = contest_data.name, + display_name = contest_data.display_name, + }) + end end return contest_list end @@ -374,38 +392,30 @@ end ---@param platform string ---@return table? function M.get_credentials(platform) - if not cache_data._credentials then + if not cache_data[platform] then return nil end - return cache_data._credentials[platform] + return cache_data[platform]._credentials end ---@param platform string ---@param creds table function M.set_credentials(platform, creds) - cache_data._credentials = cache_data._credentials or {} - cache_data._credentials[platform] = creds + cache_data[platform] = cache_data[platform] or {} + cache_data[platform]._credentials = creds M.save() end ----@param platform string? +---@param platform string function M.clear_credentials(platform) - if platform then - if cache_data._credentials then - cache_data._credentials[platform] = nil - end - else - cache_data._credentials = nil + if cache_data[platform] then + cache_data[platform]._credentials = nil end M.save() end function M.clear_all() - local creds = cache_data._credentials cache_data = {} - if creds then - cache_data._credentials = creds - end M.save() end From c194f12eeeef673920670928cbb2e32f46524c3a Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:27:29 -0500 Subject: [PATCH 75/86] feat(atcoder): extract submit helpers; add live status notifications (#294) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem `_submit_sync` was a 170-line nested closure with `_solve_turnstile` and the browser-install block further nested inside it. Status events went to stderr, which `run_scraper()` silently discards, leaving the user with a 10–30s silent hang after credential entry. The NDJSON spawn path also lacked stdin support, so submit had no streaming path at all. ## Solution Extract `_TURNSTILE_JS`, `_solve_turnstile`, `_ensure_browser`, and `_submit_headless` to module level in `atcoder.py`; status events (`installing_browser`, `checking_login`, `logging_in`, `submitting`) now print to stdout as NDJSON. Add stdin pipe support to the NDJSON spawn path in `scraper.lua` and switch `M.submit` to streaming with an `on_status` callback. Wire `on_status` in `submit.lua` to fire `vim.notify` for each phase transition. --- .luarc.json | 9 +- .styluaignore | 1 + flake.nix | 67 +++++- lua/cp/scraper.lua | 81 +++++-- lua/cp/submit.lua | 14 ++ lua/cp/utils.lua | 77 +++++++ pyproject.toml | 1 + scrapers/atcoder.py | 271 ++++++++++++++++-------- scrapers/language_ids.py | 4 +- scripts/ci.sh | 13 ++ uv.lock | 441 +++++++++++++++++++++++++++++++++++++-- 11 files changed, 863 insertions(+), 116 deletions(-) create mode 100644 .styluaignore create mode 100755 scripts/ci.sh diff --git a/.luarc.json b/.luarc.json index e0f7a7c..727793d 100644 --- a/.luarc.json +++ b/.luarc.json @@ -7,8 +7,13 @@ "globals": ["vim"] }, "workspace": { - "library": ["$VIMRUNTIME/lua", "${3rd}/luv/library", "${3rd}/busted/library"], - "checkThirdParty": false + "library": [ + "$VIMRUNTIME/lua", + "${3rd}/luv/library", + "${3rd}/busted/library" + ], + "checkThirdParty": false, + "ignoreDir": [".direnv"] }, "completion": { "callSnippet": "Replace" diff --git a/.styluaignore b/.styluaignore new file mode 100644 index 0000000..9b42106 --- /dev/null +++ b/.styluaignore @@ -0,0 +1 @@ +.direnv/ diff --git a/flake.nix b/flake.nix index 202b13b..1c416c6 100644 --- a/flake.nix +++ b/flake.nix @@ -26,10 +26,65 @@ ps.requests ]); + mkDevPythonEnv = + pkgs: + pkgs.python312.withPackages (ps: [ + ps.backoff + ps.beautifulsoup4 + ps.curl-cffi + ps.httpx + ps.ndjson + ps.pydantic + ps.requests + ps.pytest + ps.pytest-mock + ]); + + mkSubmitEnv = + pkgs: + pkgs.buildFHSEnv { + name = "cp-nvim-submit"; + targetPkgs = + pkgs: with pkgs; [ + uv + alsa-lib + at-spi2-atk + cairo + cups + dbus + fontconfig + freetype + gdk-pixbuf + glib + gtk3 + libdrm + libxkbcommon + mesa + libGL + nspr + nss + pango + libx11 + libxcomposite + libxdamage + libxext + libxfixes + libxrandr + libxcb + at-spi2-core + expat + libgbm + systemdLibs + zlib + ]; + runScript = "${pkgs.uv}/bin/uv"; + }; + mkPlugin = pkgs: let pythonEnv = mkPythonEnv pkgs; + submitEnv = mkSubmitEnv pkgs; in pkgs.vimUtils.buildVimPlugin { pname = "cp-nvim"; @@ -39,12 +94,15 @@ substituteInPlace lua/cp/utils.lua \ --replace-fail "local _nix_python = nil" \ "local _nix_python = '${pythonEnv.interpreter}'" + substituteInPlace lua/cp/utils.lua \ + --replace-fail "local _nix_submit_cmd = nil" \ + "local _nix_submit_cmd = '${submitEnv}/bin/cp-nvim-submit'" ''; nvimSkipModule = [ "cp.pickers.telescope" "cp.version" ]; - passthru = { inherit pythonEnv; }; + passthru = { inherit pythonEnv submitEnv; }; meta.description = "Competitive programming plugin for Neovim"; }; in @@ -58,17 +116,22 @@ packages = eachSystem (system: { default = mkPlugin (pkgsFor system); pythonEnv = mkPythonEnv (pkgsFor system); + submitEnv = mkSubmitEnv (pkgsFor system); }); + formatter = eachSystem (system: (pkgsFor system).nixfmt-tree); + devShells = eachSystem (system: { default = (pkgsFor system).mkShell { packages = with (pkgsFor system); [ uv - python312 + (mkDevPythonEnv (pkgsFor system)) prettier + ruff stylua selene lua-language-server + ty ]; }; }); diff --git a/lua/cp/scraper.lua b/lua/cp/scraper.lua index 7f774c3..7ddca0a 100644 --- a/lua/cp/scraper.lua +++ b/lua/cp/scraper.lua @@ -44,8 +44,17 @@ local function run_scraper(platform, subcommand, args, opts) return { success = false, error = msg } end + if subcommand == 'submit' then + utils.setup_nix_submit_env() + end + local plugin_path = utils.get_plugin_path() - local cmd = utils.get_python_cmd(platform, plugin_path) + local cmd + if subcommand == 'submit' then + cmd = utils.get_python_submit_cmd(platform, plugin_path) + else + cmd = utils.get_python_cmd(platform, plugin_path) + end vim.list_extend(cmd, { subcommand }) vim.list_extend(cmd, args) @@ -62,8 +71,16 @@ local function run_scraper(platform, subcommand, args, opts) end end + if subcommand == 'submit' and utils.is_nix_build() then + env.UV_PROJECT_ENVIRONMENT = vim.fn.stdpath('cache') .. '/cp-nvim/submit-env' + end + if opts and opts.ndjson then local uv = vim.uv + local stdin_pipe = nil + if opts.stdin then + stdin_pipe = uv.new_pipe(false) + end local stdout = uv.new_pipe(false) local stderr = uv.new_pipe(false) local buf = '' @@ -71,7 +88,7 @@ local function run_scraper(platform, subcommand, args, opts) local handle handle = uv.spawn(cmd[1], { args = vim.list_slice(cmd, 2), - stdio = { nil, stdout, stderr }, + stdio = { stdin_pipe, stdout, stderr }, env = spawn_env_list(env), cwd = plugin_path, }, function(code, signal) @@ -85,6 +102,9 @@ local function run_scraper(platform, subcommand, args, opts) if opts.on_exit then opts.on_exit({ success = (code == 0), code = code, signal = signal }) end + if stdin_pipe and not stdin_pipe:is_closing() then + stdin_pipe:close() + end if not stdout:is_closing() then stdout:close() end @@ -97,10 +117,21 @@ local function run_scraper(platform, subcommand, args, opts) end) if not handle then + if stdin_pipe and not stdin_pipe:is_closing() then + stdin_pipe:close() + end logger.log('Failed to start scraper process', vim.log.levels.ERROR) return { success = false, error = 'spawn failed' } end + if stdin_pipe then + uv.write(stdin_pipe, opts.stdin, function() + uv.shutdown(stdin_pipe, function() + stdin_pipe:close() + end) + end) + end + uv.read_start(stdout, function(_, data) if data == nil then if buf ~= '' and opts.on_event then @@ -131,7 +162,12 @@ local function run_scraper(platform, subcommand, args, opts) return end - local sysopts = { text = true, timeout = 30000, env = env, cwd = plugin_path } + local sysopts = { + text = true, + timeout = (subcommand == 'submit') and 120000 or 30000, + env = env, + cwd = plugin_path, + } if opts and opts.stdin then sysopts.stdin = opts.stdin end @@ -246,18 +282,39 @@ function M.scrape_all_tests(platform, contest_id, callback) }) end -function M.submit(platform, contest_id, problem_id, language, source_code, credentials, callback) - local creds_json = vim.json.encode(credentials) +function M.submit( + platform, + contest_id, + problem_id, + language, + source_code, + credentials, + on_status, + callback +) + local done = false run_scraper(platform, 'submit', { contest_id, problem_id, language }, { + ndjson = true, stdin = source_code, - env_extra = { CP_CREDENTIALS = creds_json }, - on_exit = function(result) - if type(callback) == 'function' then - if result and result.success then - callback(result.data or { success = true }) - else - callback({ success = false, error = result and result.error or 'unknown' }) + env_extra = { CP_CREDENTIALS = vim.json.encode(credentials) }, + on_event = function(ev) + if ev.status ~= nil then + if type(on_status) == 'function' then + on_status(ev.status) end + elseif ev.success ~= nil then + done = true + if type(callback) == 'function' then + callback(ev) + end + end + end, + on_exit = function(proc) + if not done and type(callback) == 'function' then + callback({ + success = false, + error = 'submit process exited (code=' .. tostring(proc.code) .. ')', + }) end end, }) diff --git a/lua/cp/submit.lua b/lua/cp/submit.lua index d7f91fa..4efe25e 100644 --- a/lua/cp/submit.lua +++ b/lua/cp/submit.lua @@ -4,6 +4,13 @@ local cache = require('cp.cache') local logger = require('cp.log') local state = require('cp.state') +local STATUS_MSGS = { + installing_browser = 'Installing browser (first time setup)...', + checking_login = 'Checking login...', + logging_in = 'Logging in...', + submitting = 'Submitting...', +} + local function prompt_credentials(platform, callback) local saved = cache.get_credentials(platform) if saved and saved.username and saved.password then @@ -48,6 +55,8 @@ function M.submit(opts) local source_lines = vim.fn.readfile(source_file) local source_code = table.concat(source_lines, '\n') + vim.notify('[cp.nvim] Submitting...', vim.log.levels.INFO) + require('cp.scraper').submit( platform, contest_id, @@ -55,6 +64,11 @@ function M.submit(opts) language, source_code, creds, + function(status) + vim.schedule(function() + vim.notify('[cp.nvim] ' .. (STATUS_MSGS[status] or status), vim.log.levels.INFO) + end) + end, function(result) vim.schedule(function() if result and result.success then diff --git a/lua/cp/utils.lua b/lua/cp/utils.lua index d300ee7..3111fec 100644 --- a/lua/cp/utils.lua +++ b/lua/cp/utils.lua @@ -3,6 +3,7 @@ local M = {} local logger = require('cp.log') local _nix_python = nil +local _nix_submit_cmd = nil local _nix_discovered = false local uname = vim.uv.os_uname() @@ -111,7 +112,83 @@ function M.get_python_cmd(module, plugin_path) return { 'uv', 'run', '--directory', plugin_path, '-m', 'scrapers.' .. module } end +---@param module string +---@param plugin_path string +---@return string[] +function M.get_python_submit_cmd(module, plugin_path) + if _nix_submit_cmd then + return { _nix_submit_cmd, 'run', '--directory', plugin_path, '-m', 'scrapers.' .. module } + end + return { 'uv', 'run', '--directory', plugin_path, '-m', 'scrapers.' .. module } +end + local python_env_setup = false +local _nix_submit_attempted = false + +---@return boolean +local function discover_nix_submit_cmd() + local cache_dir = vim.fn.stdpath('cache') .. '/cp-nvim' + local cache_file = cache_dir .. '/nix-submit' + + local f = io.open(cache_file, 'r') + if f then + local cached = f:read('*l') + f:close() + if cached and vim.fn.executable(cached) == 1 then + _nix_submit_cmd = cached + return true + end + end + + local plugin_path = M.get_plugin_path() + vim.cmd.redraw() + vim.notify('Building submit environment...', vim.log.levels.INFO) + vim.cmd.redraw() + local result = vim + .system( + { 'nix', 'build', plugin_path .. '#submitEnv', '--no-link', '--print-out-paths' }, + { text = true } + ) + :wait() + + if result.code ~= 0 then + logger.log('nix build #submitEnv failed: ' .. (result.stderr or ''), vim.log.levels.WARN) + return false + end + + local store_path = result.stdout:gsub('%s+$', '') + local submit_cmd = store_path .. '/bin/cp-nvim-submit' + + if vim.fn.executable(submit_cmd) ~= 1 then + logger.log('nix submit cmd not executable at ' .. submit_cmd, vim.log.levels.WARN) + return false + end + + vim.fn.mkdir(cache_dir, 'p') + f = io.open(cache_file, 'w') + if f then + f:write(submit_cmd) + f:close() + end + + _nix_submit_cmd = submit_cmd + return true +end + +---@return boolean +function M.setup_nix_submit_env() + if _nix_submit_cmd then + return true + end + if _nix_submit_attempted then + return false + end + _nix_submit_attempted = true + if vim.fn.executable('nix') == 1 then + return discover_nix_submit_cmd() + end + return false +end ---@return boolean local function discover_nix_python() diff --git a/pyproject.toml b/pyproject.toml index b18e7aa..9ffc00c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ requires-python = ">=3.11" dependencies = [ "backoff>=2.2.1", "beautifulsoup4>=4.13.5", + "scrapling[fetchers]>=0.4", "curl-cffi>=0.13.0", "httpx>=0.28.1", "ndjson>=0.3.1", diff --git a/scrapers/atcoder.py b/scrapers/atcoder.py index f7124ed..8be75ff 100644 --- a/scrapers/atcoder.py +++ b/scrapers/atcoder.py @@ -4,7 +4,9 @@ import asyncio import json import os import re +import subprocess import sys +import tempfile import time from typing import Any @@ -233,6 +235,183 @@ def _extract_samples(html: str) -> list[TestCase]: return cases +_TURNSTILE_JS = "() => { const el = document.querySelector('[name=\"cf-turnstile-response\"]'); return el && el.value.length > 0; }" + + +def _solve_turnstile(page) -> None: + for _ in range(6): + has_token = page.evaluate(_TURNSTILE_JS) + if has_token: + return + try: + box = page.locator( + 'iframe[src*="challenges.cloudflare.com"]' + ).first.bounding_box() + if box: + page.mouse.click( + box["x"] + box["width"] * 0.15, + box["y"] + box["height"] * 0.5, + ) + except Exception: + pass + try: + page.wait_for_function(_TURNSTILE_JS, timeout=5000) + return + except Exception: + pass + raise RuntimeError("Turnstile not solved after multiple attempts") + + +def _ensure_browser() -> None: + try: + from patchright._impl._driver import compute_driver_executable # type: ignore[import-untyped,unresolved-import] + + node, cli = compute_driver_executable() + browser_info = subprocess.run( + [node, cli, "install", "--dry-run", "chromium"], + capture_output=True, + text=True, + ) + for line in browser_info.stdout.splitlines(): + if "Install location:" in line: + install_dir = line.split(":", 1)[1].strip() + if not os.path.isdir(install_dir): + print(json.dumps({"status": "installing_browser"}), flush=True) + subprocess.run( + [node, cli, "install", "chromium"], + check=True, + ) + break + except Exception: + pass + + +def _submit_headless( + contest_id: str, + problem_id: str, + source_code: str, + language_id: str, + credentials: dict[str, str], +) -> "SubmitResult": + from pathlib import Path + + try: + from scrapling.fetchers import StealthySession # type: ignore[import-untyped,unresolved-import] + except ImportError: + return SubmitResult( + success=False, + error="scrapling is required for AtCoder submit. Install it: uv add 'scrapling[fetchers]>=0.4'", + ) + + _ensure_browser() + + cookie_cache = Path.home() / ".cache" / "cp-nvim" / "atcoder-cookies.json" + cookie_cache.parent.mkdir(parents=True, exist_ok=True) + saved_cookies: list[dict[str, Any]] = [] + if cookie_cache.exists(): + try: + saved_cookies = json.loads(cookie_cache.read_text()) + except Exception: + pass + + logged_in = False + login_error: str | None = None + submit_error: str | None = None + + def check_login(page): + nonlocal logged_in + logged_in = page.evaluate( + "() => Array.from(document.querySelectorAll('a')).some(a => a.textContent.trim() === 'Sign Out')" + ) + + def login_action(page): + nonlocal login_error + try: + _solve_turnstile(page) + page.fill('input[name="username"]', credentials.get("username", "")) + page.fill('input[name="password"]', credentials.get("password", "")) + page.click("#submit") + page.wait_for_url(lambda url: "/login" not in url, timeout=60000) + except Exception as e: + login_error = str(e) + + def submit_action(page): + nonlocal submit_error + try: + _solve_turnstile(page) + page.select_option( + 'select[name="data.TaskScreenName"]', + f"{contest_id}_{problem_id}", + ) + page.locator( + f'select[name="data.LanguageId"] option[value="{language_id}"]' + ).wait_for(state="attached", timeout=15000) + page.select_option('select[name="data.LanguageId"]', language_id) + with tempfile.NamedTemporaryFile( + mode="w", suffix=".cpp", delete=False, prefix="atcoder_" + ) as tf: + tf.write(source_code) + tmp_path = tf.name + try: + page.set_input_files("#input-open-file", tmp_path) + page.wait_for_timeout(500) + finally: + os.unlink(tmp_path) + page.locator('button[type="submit"]').click() + page.wait_for_url(lambda url: "/submissions/me" in url, timeout=60000) + except Exception as e: + submit_error = str(e) + + try: + with StealthySession( + headless=True, + timeout=60000, + google_search=False, + cookies=saved_cookies, + ) as session: + print(json.dumps({"status": "checking_login"}), flush=True) + session.fetch( + f"{BASE_URL}/home", + page_action=check_login, + network_idle=True, + ) + + if not logged_in: + print(json.dumps({"status": "logging_in"}), flush=True) + session.fetch( + f"{BASE_URL}/login", + page_action=login_action, + solve_cloudflare=True, + ) + if login_error: + return SubmitResult( + success=False, error=f"Login failed: {login_error}" + ) + + print(json.dumps({"status": "submitting"}), flush=True) + session.fetch( + f"{BASE_URL}/contests/{contest_id}/submit", + page_action=submit_action, + solve_cloudflare=True, + ) + + try: + browser_cookies = session.context.cookies() + if any(c["name"] == "REVEL_SESSION" for c in browser_cookies): + cookie_cache.write_text(json.dumps(browser_cookies)) + except Exception: + pass + + if submit_error: + return SubmitResult(success=False, error=submit_error) + + return SubmitResult( + success=True, error="", submission_id="", verdict="submitted" + ) + except Exception as e: + return SubmitResult(success=False, error=str(e)) + + def _scrape_tasks_sync(contest_id: str) -> list[dict[str, str]]: html = _fetch(f"{BASE_URL}/contests/{contest_id}/tasks") return _parse_tasks_list(html) @@ -379,90 +558,14 @@ class AtcoderScraper(BaseScraper): language_id: str, credentials: dict[str, str], ) -> SubmitResult: - def _submit_sync() -> SubmitResult: - from curl_cffi import requests as curl_requests - - try: - session = curl_requests.Session(impersonate="chrome") - - login_page = session.get(f"{BASE_URL}/login", timeout=TIMEOUT_SECONDS) - login_page.raise_for_status() - soup = BeautifulSoup(login_page.text, "html.parser") - csrf_input = soup.find("input", {"name": "csrf_token"}) - if not csrf_input or not hasattr(csrf_input, "get"): - return SubmitResult( - success=False, error="Could not find CSRF token on login page" - ) - csrf_token = csrf_input.get("value", "") or "" # type: ignore[union-attr] - - login_resp = session.post( - f"{BASE_URL}/login", - data={ - "username": credentials.get("username", ""), - "password": credentials.get("password", ""), - "csrf_token": csrf_token, - }, - timeout=TIMEOUT_SECONDS, - allow_redirects=False, - ) - if login_resp.status_code in (301, 302): - location = login_resp.headers.get("Location", "") - if "/login" in location: - return SubmitResult( - success=False, - error="Login failed: incorrect username or password", - ) - session.get(BASE_URL + location, timeout=TIMEOUT_SECONDS) - else: - login_resp.raise_for_status() - - submit_page = session.get( - f"{BASE_URL}/contests/{contest_id}/submit", - timeout=TIMEOUT_SECONDS, - ) - submit_page.raise_for_status() - soup = BeautifulSoup(submit_page.text, "html.parser") - csrf_input = soup.find("input", {"name": "csrf_token"}) - if not csrf_input or not hasattr(csrf_input, "get"): - return SubmitResult( - success=False, error="Could not find CSRF token on submit page" - ) - csrf_token = csrf_input.get("value", "") or "" # type: ignore[union-attr] - - task_screen_name = f"{contest_id}_{problem_id}" - submit_resp = session.post( - f"{BASE_URL}/contests/{contest_id}/submit", - data={ - "data.TaskScreenName": task_screen_name, - "data.LanguageId": language_id, - "sourceCode": source_code, - "csrf_token": csrf_token, - }, - timeout=TIMEOUT_SECONDS, - allow_redirects=False, - ) - if submit_resp.status_code in (301, 302): - location = submit_resp.headers.get("Location", "") - if "/submissions/me" in location: - return SubmitResult( - success=True, - error="", - submission_id="", - verdict="submitted", - ) - return SubmitResult( - success=False, - error=f"Submit may have failed: redirected to {location}", - ) - submit_resp.raise_for_status() - return SubmitResult( - success=False, - error="Unexpected response from submit (expected redirect)", - ) - except Exception as e: - return SubmitResult(success=False, error=str(e)) - - return await asyncio.to_thread(_submit_sync) + return await asyncio.to_thread( + _submit_headless, + contest_id, + problem_id, + source_code, + language_id, + credentials, + ) async def main_async() -> int: diff --git a/scrapers/language_ids.py b/scrapers/language_ids.py index d0a47e6..d6a0ae4 100644 --- a/scrapers/language_ids.py +++ b/scrapers/language_ids.py @@ -1,7 +1,7 @@ LANGUAGE_IDS = { "atcoder": { - "cpp": "5028", - "python": "5078", + "cpp": "6017", + "python": "6082", }, "codeforces": { "cpp": "89", diff --git a/scripts/ci.sh b/scripts/ci.sh new file mode 100755 index 0000000..03f7da1 --- /dev/null +++ b/scripts/ci.sh @@ -0,0 +1,13 @@ +#!/bin/sh +set -eu + +nix develop --command stylua --check . +git ls-files '*.lua' | xargs nix develop --command selene --display-style quiet +nix develop --command prettier --check . +nix fmt +git diff --exit-code -- '*.nix' +nix develop --command lua-language-server --check . --checklevel=Warning +nix develop --command ruff format --check . +nix develop --command ruff check . +nix develop --command ty check . +nix develop --command python -m pytest tests/ -v diff --git a/uv.lock b/uv.lock index 66bd81e..d3334ab 100644 --- a/uv.lock +++ b/uv.lock @@ -13,15 +13,24 @@ wheels = [ [[package]] name = "anyio" -version = "4.12.0" +version = "4.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "apify-fingerprint-datapoints" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/a9/586b7ebdd682c047cd0b551dc7e154bb1480f8f6548154708e9a6c7844db/apify_fingerprint_datapoints-0.11.0.tar.gz", hash = "sha256:3f905c392b11a27fb59ccfe40891c166abd737ab9c6209733f102bbb3b302515", size = 969830, upload-time = "2026-03-01T01:00:04.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/38/9483eb52fc0f00039c684af627f8a8f994a8a99e8eceb869ba93b3fd740b/apify_fingerprint_datapoints-0.11.0-py3-none-any.whl", hash = "sha256:333340ccc3e520f19b5561e95d7abe2b31702e61d34b6247b328c9b8c93fbe1d", size = 726498, upload-time = "2026-03-01T01:00:03.103Z" }, ] [[package]] @@ -58,6 +67,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, ] +[[package]] +name = "browserforge" +version = "1.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apify-fingerprint-datapoints" }, + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/6f/8975af88d203efd70cc69477ebac702babef38201d04621c9583f2508f25/browserforge-1.2.4.tar.gz", hash = "sha256:05686473793769856ebd3528c69071f5be0e511260993e8b2ba839863711a0c4", size = 36700, upload-time = "2026-02-03T02:52:09.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/35/ce962f738ae28ffce6293e7607b129075633e6bb185a5ab87e49246eedc2/browserforge-1.2.4-py3-none-any.whl", hash = "sha256:fb1c14e62ac09de221dcfc73074200269f697596c642cb200ceaab1127a17542", size = 37890, upload-time = "2026-02-03T02:52:08.745Z" }, +] + [[package]] name = "certifi" version = "2025.11.12" @@ -219,6 +241,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -228,25 +262,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "cssselect" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/2e/cdfd8b01c37cbf4f9482eefd455853a3cf9c995029a46acd31dfaa9c1dd6/cssselect-1.4.0.tar.gz", hash = "sha256:fdaf0a1425e17dfe8c5cf66191d211b357cf7872ae8afc4c6762ddd8ac47fc92", size = 40589, upload-time = "2026-01-29T07:00:26.701Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/0c/7bb51e3acfafd16c48875bf3db03607674df16f5b6ef8d056586af7e2b8b/cssselect-1.4.0-py3-none-any.whl", hash = "sha256:c0ec5c0191c8ee39fcc8afc1540331d8b55b0183478c50e9c8a79d44dbceb1d8", size = 18540, upload-time = "2026-01-29T07:00:24.994Z" }, +] + [[package]] name = "curl-cffi" -version = "0.13.0" +version = "0.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "cffi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4e/3d/f39ca1f8fdf14408888e7c25e15eed63eac5f47926e206fb93300d28378c/curl_cffi-0.13.0.tar.gz", hash = "sha256:62ecd90a382bd5023750e3606e0aa7cb1a3a8ba41c14270b8e5e149ebf72c5ca", size = 151303, upload-time = "2025-08-06T13:05:42.988Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/c9/0067d9a25ed4592b022d4558157fcdb6e123516083700786d38091688767/curl_cffi-0.14.0.tar.gz", hash = "sha256:5ffbc82e59f05008ec08ea432f0e535418823cda44178ee518906a54f27a5f0f", size = 162633, upload-time = "2025-12-16T03:25:07.931Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/d1/acabfd460f1de26cad882e5ef344d9adde1507034528cb6f5698a2e6a2f1/curl_cffi-0.13.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:434cadbe8df2f08b2fc2c16dff2779fb40b984af99c06aa700af898e185bb9db", size = 5686337, upload-time = "2025-08-06T13:05:28.985Z" }, - { url = "https://files.pythonhosted.org/packages/2c/1c/cdb4fb2d16a0e9de068e0e5bc02094e105ce58a687ff30b4c6f88e25a057/curl_cffi-0.13.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:59afa877a9ae09efa04646a7d068eeea48915a95d9add0a29854e7781679fcd7", size = 2994613, upload-time = "2025-08-06T13:05:31.027Z" }, - { url = "https://files.pythonhosted.org/packages/04/3e/fdf617c1ec18c3038b77065d484d7517bb30f8fb8847224eb1f601a4e8bc/curl_cffi-0.13.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d06ed389e45a7ca97b17c275dbedd3d6524560270e675c720e93a2018a766076", size = 7931353, upload-time = "2025-08-06T13:05:32.273Z" }, - { url = "https://files.pythonhosted.org/packages/3d/10/6f30c05d251cf03ddc2b9fd19880f3cab8c193255e733444a2df03b18944/curl_cffi-0.13.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4e0de45ab3b7a835c72bd53640c2347415111b43421b5c7a1a0b18deae2e541", size = 7486378, upload-time = "2025-08-06T13:05:33.672Z" }, - { url = "https://files.pythonhosted.org/packages/77/81/5bdb7dd0d669a817397b2e92193559bf66c3807f5848a48ad10cf02bf6c7/curl_cffi-0.13.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8eb4083371bbb94e9470d782de235fb5268bf43520de020c9e5e6be8f395443f", size = 8328585, upload-time = "2025-08-06T13:05:35.28Z" }, - { url = "https://files.pythonhosted.org/packages/ce/c1/df5c6b4cfad41c08442e0f727e449f4fb5a05f8aa564d1acac29062e9e8e/curl_cffi-0.13.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:28911b526e8cd4aa0e5e38401bfe6887e8093907272f1f67ca22e6beb2933a51", size = 8739831, upload-time = "2025-08-06T13:05:37.078Z" }, - { url = "https://files.pythonhosted.org/packages/1a/91/6dd1910a212f2e8eafe57877bcf97748eb24849e1511a266687546066b8a/curl_cffi-0.13.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6d433ffcb455ab01dd0d7bde47109083aa38b59863aa183d29c668ae4c96bf8e", size = 8711908, upload-time = "2025-08-06T13:05:38.741Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e4/15a253f9b4bf8d008c31e176c162d2704a7e0c5e24d35942f759df107b68/curl_cffi-0.13.0-cp39-abi3-win_amd64.whl", hash = "sha256:66a6b75ce971de9af64f1b6812e275f60b88880577bac47ef1fa19694fa21cd3", size = 1614510, upload-time = "2025-08-06T13:05:40.451Z" }, - { url = "https://files.pythonhosted.org/packages/f9/0f/9c5275f17ad6ff5be70edb8e0120fdc184a658c9577ca426d4230f654beb/curl_cffi-0.13.0-cp39-abi3-win_arm64.whl", hash = "sha256:d438a3b45244e874794bc4081dc1e356d2bb926dcc7021e5a8fef2e2105ef1d8", size = 1365753, upload-time = "2025-08-06T13:05:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f0/0f21e9688eaac85e705537b3a87a5588d0cefb2f09d83e83e0e8be93aa99/curl_cffi-0.14.0-cp39-abi3-macosx_14_0_arm64.whl", hash = "sha256:e35e89c6a69872f9749d6d5fda642ed4fc159619329e99d577d0104c9aad5893", size = 3087277, upload-time = "2025-12-16T03:24:49.607Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a3/0419bd48fce5b145cb6a2344c6ac17efa588f5b0061f212c88e0723da026/curl_cffi-0.14.0-cp39-abi3-macosx_15_0_x86_64.whl", hash = "sha256:5945478cd28ad7dfb5c54473bcfb6743ee1d66554d57951fdf8fc0e7d8cf4e45", size = 5804650, upload-time = "2025-12-16T03:24:51.518Z" }, + { url = "https://files.pythonhosted.org/packages/e2/07/a238dd062b7841b8caa2fa8a359eb997147ff3161288f0dd46654d898b4d/curl_cffi-0.14.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c42e8fa3c667db9ccd2e696ee47adcd3cd5b0838d7282f3fc45f6c0ef3cfdfa7", size = 8231918, upload-time = "2025-12-16T03:24:52.862Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d2/ce907c9b37b5caf76ac08db40cc4ce3d9f94c5500db68a195af3513eacbc/curl_cffi-0.14.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:060fe2c99c41d3cb7f894de318ddf4b0301b08dca70453d769bd4e74b36b8483", size = 8654624, upload-time = "2025-12-16T03:24:54.579Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ae/6256995b18c75e6ef76b30753a5109e786813aa79088b27c8eabb1ef85c9/curl_cffi-0.14.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b158c41a25388690dd0d40b5bc38d1e0f512135f17fdb8029868cbc1993d2e5b", size = 8010654, upload-time = "2025-12-16T03:24:56.507Z" }, + { url = "https://files.pythonhosted.org/packages/fb/10/ff64249e516b103cb762e0a9dca3ee0f04cf25e2a1d5d9838e0f1273d071/curl_cffi-0.14.0-cp39-abi3-manylinux_2_28_i686.whl", hash = "sha256:1439fbef3500fb723333c826adf0efb0e2e5065a703fb5eccce637a2250db34a", size = 7781969, upload-time = "2025-12-16T03:24:57.885Z" }, + { url = "https://files.pythonhosted.org/packages/51/76/d6f7bb76c2d12811aa7ff16f5e17b678abdd1b357b9a8ac56310ceccabd5/curl_cffi-0.14.0-cp39-abi3-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e7176f2c2d22b542e3cf261072a81deb018cfa7688930f95dddef215caddb469", size = 7969133, upload-time = "2025-12-16T03:24:59.261Z" }, + { url = "https://files.pythonhosted.org/packages/23/7c/cca39c0ed4e1772613d3cba13091c0e9d3b89365e84b9bf9838259a3cd8f/curl_cffi-0.14.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:03f21ade2d72978c2bb8670e9b6de5260e2755092b02d94b70b906813662998d", size = 9080167, upload-time = "2025-12-16T03:25:00.946Z" }, + { url = "https://files.pythonhosted.org/packages/75/03/a942d7119d3e8911094d157598ae0169b1c6ca1bd3f27d7991b279bcc45b/curl_cffi-0.14.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:58ebf02de64ee5c95613209ddacb014c2d2f86298d7080c0a1c12ed876ee0690", size = 9520464, upload-time = "2025-12-16T03:25:02.922Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/78900e9b0833066d2274bda75cba426fdb4cef7fbf6a4f6a6ca447607bec/curl_cffi-0.14.0-cp39-abi3-win_amd64.whl", hash = "sha256:6e503f9a103f6ae7acfb3890c843b53ec030785a22ae7682a22cc43afb94123e", size = 1677416, upload-time = "2025-12-16T03:25:04.902Z" }, + { url = "https://files.pythonhosted.org/packages/5c/7c/d2ba86b0b3e1e2830bd94163d047de122c69a8df03c5c7c36326c456ad82/curl_cffi-0.14.0-cp39-abi3-win_arm64.whl", hash = "sha256:2eed50a969201605c863c4c31269dfc3e0da52916086ac54553cfa353022425c", size = 1425067, upload-time = "2025-12-16T03:25:06.454Z" }, ] [[package]] @@ -267,6 +312,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, ] +[[package]] +name = "greenlet" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" }, + { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" }, + { url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8b/1430a04657735a3f23116c2e0d5eb10220928846e4537a938a41b350bed6/greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2", size = 605046, upload-time = "2026-02-20T21:02:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" }, + { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" }, + { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3a/efb2cf697fbccdf75b24e2c18025e7dfa54c4f31fab75c51d0fe79942cef/greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5", size = 230389, upload-time = "2026-02-20T20:17:18.772Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a1/65bbc059a43a7e2143ec4fc1f9e3f673e04f9c7b371a494a101422ac4fd5/greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd", size = 229645, upload-time = "2026-02-20T20:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" }, + { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, + { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, + { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, + { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, + { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, + { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, + { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371, upload-time = "2026-02-20T21:02:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581, upload-time = "2026-02-20T21:02:51.526Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -331,6 +428,156 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "lxml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" }, + { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" }, + { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" }, + { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" }, + { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" }, + { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, + { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, + { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, + { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, + { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, + { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, + { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, + { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, + { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, + { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, + { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, + { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, + { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" }, + { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" }, + { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" }, +] + +[[package]] +name = "msgspec" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/9c/bfbd12955a49180cbd234c5d29ec6f74fe641698f0cd9df154a854fc8a15/msgspec-0.20.0.tar.gz", hash = "sha256:692349e588fde322875f8d3025ac01689fead5901e7fb18d6870a44519d62a29", size = 317862, upload-time = "2025-11-24T03:56:28.934Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/59/fdcb3af72f750a8de2bcf39d62ada70b5eb17b06d7f63860e0a679cb656b/msgspec-0.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:09e0efbf1ac641fedb1d5496c59507c2f0dc62a052189ee62c763e0aae217520", size = 193345, upload-time = "2025-11-24T03:55:20.613Z" }, + { url = "https://files.pythonhosted.org/packages/5a/15/3c225610da9f02505d37d69a77f4a2e7daae2a125f99d638df211ba84e59/msgspec-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23ee3787142e48f5ee746b2909ce1b76e2949fbe0f97f9f6e70879f06c218b54", size = 186867, upload-time = "2025-11-24T03:55:22.4Z" }, + { url = "https://files.pythonhosted.org/packages/81/36/13ab0c547e283bf172f45491edfdea0e2cecb26ae61e3a7b1ae6058b326d/msgspec-0.20.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:81f4ac6f0363407ac0465eff5c7d4d18f26870e00674f8fcb336d898a1e36854", size = 215351, upload-time = "2025-11-24T03:55:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/6b/96/5c095b940de3aa6b43a71ec76275ac3537b21bd45c7499b5a17a429110fa/msgspec-0.20.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb4d873f24ae18cd1334f4e37a178ed46c9d186437733351267e0a269bdf7e53", size = 219896, upload-time = "2025-11-24T03:55:25.356Z" }, + { url = "https://files.pythonhosted.org/packages/98/7a/81a7b5f01af300761087b114dafa20fb97aed7184d33aab64d48874eb187/msgspec-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b92b8334427b8393b520c24ff53b70f326f79acf5f74adb94fd361bcff8a1d4e", size = 220389, upload-time = "2025-11-24T03:55:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/70/c0/3d0cce27db9a9912421273d49eab79ce01ecd2fed1a2f1b74af9b445f33c/msgspec-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:562c44b047c05cc0384e006fae7a5e715740215c799429e0d7e3e5adf324285a", size = 223348, upload-time = "2025-11-24T03:55:28.311Z" }, + { url = "https://files.pythonhosted.org/packages/89/5e/406b7d578926b68790e390d83a1165a9bfc2d95612a1a9c1c4d5c72ea815/msgspec-0.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:d1dcc93a3ce3d3195985bfff18a48274d0b5ffbc96fa1c5b89da6f0d9af81b29", size = 188713, upload-time = "2025-11-24T03:55:29.553Z" }, + { url = "https://files.pythonhosted.org/packages/47/87/14fe2316624ceedf76a9e94d714d194cbcb699720b210ff189f89ca4efd7/msgspec-0.20.0-cp311-cp311-win_arm64.whl", hash = "sha256:aa387aa330d2e4bd69995f66ea8fdc87099ddeedf6fdb232993c6a67711e7520", size = 174229, upload-time = "2025-11-24T03:55:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6f/1e25eee957e58e3afb2a44b94fa95e06cebc4c236193ed0de3012fff1e19/msgspec-0.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2aba22e2e302e9231e85edc24f27ba1f524d43c223ef5765bd8624c7df9ec0a5", size = 196391, upload-time = "2025-11-24T03:55:32.677Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ee/af51d090ada641d4b264992a486435ba3ef5b5634bc27e6eb002f71cef7d/msgspec-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:716284f898ab2547fedd72a93bb940375de9fbfe77538f05779632dc34afdfde", size = 188644, upload-time = "2025-11-24T03:55:33.934Z" }, + { url = "https://files.pythonhosted.org/packages/49/d6/9709ee093b7742362c2934bfb1bbe791a1e09bed3ea5d8a18ce552fbfd73/msgspec-0.20.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:558ed73315efa51b1538fa8f1d3b22c8c5ff6d9a2a62eff87d25829b94fc5054", size = 218852, upload-time = "2025-11-24T03:55:35.575Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a2/488517a43ccf5a4b6b6eca6dd4ede0bd82b043d1539dd6bb908a19f8efd3/msgspec-0.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:509ac1362a1d53aa66798c9b9fd76872d7faa30fcf89b2fba3bcbfd559d56eb0", size = 224937, upload-time = "2025-11-24T03:55:36.859Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e8/49b832808aa23b85d4f090d1d2e48a4e3834871415031ed7c5fe48723156/msgspec-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1353c2c93423602e7dea1aa4c92f3391fdfc25ff40e0bacf81d34dbc68adb870", size = 222858, upload-time = "2025-11-24T03:55:38.187Z" }, + { url = "https://files.pythonhosted.org/packages/9f/56/1dc2fa53685dca9c3f243a6cbecd34e856858354e455b77f47ebd76cf5bf/msgspec-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb33b5eb5adb3c33d749684471c6a165468395d7aa02d8867c15103b81e1da3e", size = 227248, upload-time = "2025-11-24T03:55:39.496Z" }, + { url = "https://files.pythonhosted.org/packages/5a/51/aba940212c23b32eedce752896205912c2668472ed5b205fc33da28a6509/msgspec-0.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:fb1d934e435dd3a2b8cf4bbf47a8757100b4a1cfdc2afdf227541199885cdacb", size = 190024, upload-time = "2025-11-24T03:55:40.829Z" }, + { url = "https://files.pythonhosted.org/packages/41/ad/3b9f259d94f183daa9764fef33fdc7010f7ecffc29af977044fa47440a83/msgspec-0.20.0-cp312-cp312-win_arm64.whl", hash = "sha256:00648b1e19cf01b2be45444ba9dc961bd4c056ffb15706651e64e5d6ec6197b7", size = 175390, upload-time = "2025-11-24T03:55:42.05Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d1/b902d38b6e5ba3bdddbec469bba388d647f960aeed7b5b3623a8debe8a76/msgspec-0.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c1ff8db03be7598b50dd4b4a478d6fe93faae3bd54f4f17aa004d0e46c14c46", size = 196463, upload-time = "2025-11-24T03:55:43.405Z" }, + { url = "https://files.pythonhosted.org/packages/57/b6/eff0305961a1d9447ec2b02f8c73c8946f22564d302a504185b730c9a761/msgspec-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f6532369ece217fd37c5ebcfd7e981f2615628c21121b7b2df9d3adcf2fd69b8", size = 188650, upload-time = "2025-11-24T03:55:44.761Z" }, + { url = "https://files.pythonhosted.org/packages/99/93/f2ec1ae1de51d3fdee998a1ede6b2c089453a2ee82b5c1b361ed9095064a/msgspec-0.20.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9a1697da2f85a751ac3cc6a97fceb8e937fc670947183fb2268edaf4016d1ee", size = 218834, upload-time = "2025-11-24T03:55:46.441Z" }, + { url = "https://files.pythonhosted.org/packages/28/83/36557b04cfdc317ed8a525c4993b23e43a8fbcddaddd78619112ca07138c/msgspec-0.20.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fac7e9c92eddcd24c19d9e5f6249760941485dff97802461ae7c995a2450111", size = 224917, upload-time = "2025-11-24T03:55:48.06Z" }, + { url = "https://files.pythonhosted.org/packages/8f/56/362037a1ed5be0b88aced59272442c4b40065c659700f4b195a7f4d0ac88/msgspec-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f953a66f2a3eb8d5ea64768445e2bb301d97609db052628c3e1bcb7d87192a9f", size = 222821, upload-time = "2025-11-24T03:55:49.388Z" }, + { url = "https://files.pythonhosted.org/packages/92/75/fa2370ec341cedf663731ab7042e177b3742645c5dd4f64dc96bd9f18a6b/msgspec-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:247af0313ae64a066d3aea7ba98840f6681ccbf5c90ba9c7d17f3e39dbba679c", size = 227227, upload-time = "2025-11-24T03:55:51.125Z" }, + { url = "https://files.pythonhosted.org/packages/f1/25/5e8080fe0117f799b1b68008dc29a65862077296b92550632de015128579/msgspec-0.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:67d5e4dfad52832017018d30a462604c80561aa62a9d548fc2bd4e430b66a352", size = 189966, upload-time = "2025-11-24T03:55:52.458Z" }, + { url = "https://files.pythonhosted.org/packages/79/b6/63363422153937d40e1cb349c5081338401f8529a5a4e216865decd981bf/msgspec-0.20.0-cp313-cp313-win_arm64.whl", hash = "sha256:91a52578226708b63a9a13de287b1ec3ed1123e4a088b198143860c087770458", size = 175378, upload-time = "2025-11-24T03:55:53.721Z" }, + { url = "https://files.pythonhosted.org/packages/bb/18/62dc13ab0260c7d741dda8dc7f481495b93ac9168cd887dda5929880eef8/msgspec-0.20.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:eead16538db1b3f7ec6e3ed1f6f7c5dec67e90f76e76b610e1ffb5671815633a", size = 196407, upload-time = "2025-11-24T03:55:55.001Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1d/b9949e4ad6953e9f9a142c7997b2f7390c81e03e93570c7c33caf65d27e1/msgspec-0.20.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:703c3bb47bf47801627fb1438f106adbfa2998fe586696d1324586a375fca238", size = 188889, upload-time = "2025-11-24T03:55:56.311Z" }, + { url = "https://files.pythonhosted.org/packages/1e/19/f8bb2dc0f1bfe46cc7d2b6b61c5e9b5a46c62298e8f4d03bbe499c926180/msgspec-0.20.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6cdb227dc585fb109305cee0fd304c2896f02af93ecf50a9c84ee54ee67dbb42", size = 219691, upload-time = "2025-11-24T03:55:57.908Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8e/6b17e43f6eb9369d9858ee32c97959fcd515628a1df376af96c11606cf70/msgspec-0.20.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27d35044dd8818ac1bd0fedb2feb4fbdff4e3508dd7c5d14316a12a2d96a0de0", size = 224918, upload-time = "2025-11-24T03:55:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/1c/db/0e833a177db1a4484797adba7f429d4242585980b90882cc38709e1b62df/msgspec-0.20.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4296393a29ee42dd25947981c65506fd4ad39beaf816f614146fa0c5a6c91ae", size = 223436, upload-time = "2025-11-24T03:56:00.716Z" }, + { url = "https://files.pythonhosted.org/packages/c3/30/d2ee787f4c918fd2b123441d49a7707ae9015e0e8e1ab51aa7967a97b90e/msgspec-0.20.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:205fbdadd0d8d861d71c8f3399fe1a82a2caf4467bc8ff9a626df34c12176980", size = 227190, upload-time = "2025-11-24T03:56:02.371Z" }, + { url = "https://files.pythonhosted.org/packages/ff/37/9c4b58ff11d890d788e700b827db2366f4d11b3313bf136780da7017278b/msgspec-0.20.0-cp314-cp314-win_amd64.whl", hash = "sha256:7dfebc94fe7d3feec6bc6c9df4f7e9eccc1160bb5b811fbf3e3a56899e398a6b", size = 193950, upload-time = "2025-11-24T03:56:03.668Z" }, + { url = "https://files.pythonhosted.org/packages/e9/4e/cab707bf2fa57408e2934e5197fc3560079db34a1e3cd2675ff2e47e07de/msgspec-0.20.0-cp314-cp314-win_arm64.whl", hash = "sha256:2ad6ae36e4a602b24b4bf4eaf8ab5a441fec03e1f1b5931beca8ebda68f53fc0", size = 179018, upload-time = "2025-11-24T03:56:05.038Z" }, + { url = "https://files.pythonhosted.org/packages/4c/06/3da3fc9aaa55618a8f43eb9052453cfe01f82930bca3af8cea63a89f3a11/msgspec-0.20.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f84703e0e6ef025663dd1de828ca028774797b8155e070e795c548f76dde65d5", size = 200389, upload-time = "2025-11-24T03:56:06.375Z" }, + { url = "https://files.pythonhosted.org/packages/83/3b/cc4270a5ceab40dfe1d1745856951b0a24fd16ac8539a66ed3004a60c91e/msgspec-0.20.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7c83fc24dd09cf1275934ff300e3951b3adc5573f0657a643515cc16c7dee131", size = 193198, upload-time = "2025-11-24T03:56:07.742Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ae/4c7905ac53830c8e3c06fdd60e3cdcfedc0bbc993872d1549b84ea21a1bd/msgspec-0.20.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f13ccb1c335a124e80c4562573b9b90f01ea9521a1a87f7576c2e281d547f56", size = 225973, upload-time = "2025-11-24T03:56:09.18Z" }, + { url = "https://files.pythonhosted.org/packages/d9/da/032abac1de4d0678d99eaeadb1323bd9d247f4711c012404ba77ed6f15ca/msgspec-0.20.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17c2b5ca19f19306fc83c96d85e606d2cc107e0caeea85066b5389f664e04846", size = 229509, upload-time = "2025-11-24T03:56:10.898Z" }, + { url = "https://files.pythonhosted.org/packages/69/52/fdc7bdb7057a166f309e0b44929e584319e625aaba4771b60912a9321ccd/msgspec-0.20.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d931709355edabf66c2dd1a756b2d658593e79882bc81aae5964969d5a291b63", size = 230434, upload-time = "2025-11-24T03:56:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/cb/fe/1dfd5f512b26b53043884e4f34710c73e294e7cc54278c3fe28380e42c37/msgspec-0.20.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:565f915d2e540e8a0c93a01ff67f50aebe1f7e22798c6a25873f9fda8d1325f8", size = 231758, upload-time = "2025-11-24T03:56:13.765Z" }, + { url = "https://files.pythonhosted.org/packages/97/f6/9ba7121b8e0c4e0beee49575d1dbc804e2e72467692f0428cf39ceba1ea5/msgspec-0.20.0-cp314-cp314t-win_amd64.whl", hash = "sha256:726f3e6c3c323f283f6021ebb6c8ccf58d7cd7baa67b93d73bfbe9a15c34ab8d", size = 206540, upload-time = "2025-11-24T03:56:15.029Z" }, + { url = "https://files.pythonhosted.org/packages/c8/3e/c5187de84bb2c2ca334ab163fcacf19a23ebb1d876c837f81a1b324a15bf/msgspec-0.20.0-cp314-cp314t-win_arm64.whl", hash = "sha256:93f23528edc51d9f686808a361728e903d6f2be55c901d6f5c92e44c6d546bfc", size = 183011, upload-time = "2025-11-24T03:56:16.442Z" }, +] + [[package]] name = "ndjson" version = "0.3.1" @@ -365,6 +612,74 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ad/b7/bc0cdbc2cc3a66fcac82c79912e135a0110b37b790a14c477f18e18d90cd/nodejs_wheel_binaries-24.11.1-py2.py3-none-win_arm64.whl", hash = "sha256:376b9ea1c4bc1207878975dfeb604f7aa5668c260c6154dcd2af9d42f7734116", size = 39026497, upload-time = "2025-11-18T18:21:54.634Z" }, ] +[[package]] +name = "orjson" +version = "3.11.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664, upload-time = "2026-02-02T15:37:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344, upload-time = "2026-02-02T15:37:26.92Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404, upload-time = "2026-02-02T15:37:28.108Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677, upload-time = "2026-02-02T15:37:30.287Z" }, + { url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950, upload-time = "2026-02-02T15:37:31.595Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756, upload-time = "2026-02-02T15:37:32.985Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812, upload-time = "2026-02-02T15:37:34.204Z" }, + { url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444, upload-time = "2026-02-02T15:37:35.446Z" }, + { url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609, upload-time = "2026-02-02T15:37:36.657Z" }, + { url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918, upload-time = "2026-02-02T15:37:38.076Z" }, + { url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998, upload-time = "2026-02-02T15:37:39.706Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802, upload-time = "2026-02-02T15:37:41.002Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828, upload-time = "2026-02-02T15:37:42.241Z" }, + { url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941, upload-time = "2026-02-02T15:37:43.444Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245, upload-time = "2026-02-02T15:37:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, + { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, + { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, + { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, + { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, + { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, + { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, + { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, + { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" }, + { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" }, + { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" }, + { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" }, + { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" }, + { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" }, + { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" }, + { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" }, + { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" }, + { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" }, + { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" }, + { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -374,6 +689,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] +[[package]] +name = "patchright" +version = "1.56.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet" }, + { name = "pyee" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/d6/ac03353f1b3138541356a044c2d7d3e01b73f9555c7281dab3423c5d44da/patchright-1.56.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:9df1e0b2c35a298ad2807044699476d3e872f76117c319077b68d7b8ac8bffae", size = 40595069, upload-time = "2025-11-11T18:59:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f2/e33cd3e6916ed959f5b9be1182363e19771be81541a77b2c5615a6434cc8/patchright-1.56.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e0d8e86d743f4da99f83e9962f9317bd3aaabe4e9db86d3e12cb1d0f39e2f469", size = 39383869, upload-time = "2025-11-11T18:59:23.829Z" }, + { url = "https://files.pythonhosted.org/packages/5f/49/510b8fb761a7e6a2cbd8381f2254fd9442055e27961c0565944d5634ccb6/patchright-1.56.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:e939a9651ebad6ed5afb84e6cf8b874fd80e4b03a912509dc5159f0fa2d4a752", size = 40595069, upload-time = "2025-11-11T18:59:26.9Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ad/17f72b906a1d3d9750b26964d4481340defbfc02f039c888756061f41a33/patchright-1.56.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:cef0f31334dece118fa8dbe20e0c0ac97ea539a2a3e7dd007a67d05b4c3e0512", size = 46246766, upload-time = "2025-11-11T18:59:29.985Z" }, + { url = "https://files.pythonhosted.org/packages/53/b4/6546fb868f464c888cee8e95d5b0cc4e276afc5e531bc3ca737e2a910f77/patchright-1.56.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02329457d4f9e92b49209adceec33097042b6a6f03de31185a2f99721aeded6a", size = 46094291, upload-time = "2025-11-11T18:59:32.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/4b/a43f86570cb72f732723c19ba71f1c02200ed1cc762d71daa2c35718a529/patchright-1.56.0-py3-none-win32.whl", hash = "sha256:5502899cd8764ccc7a18c3cb47a76e49b25ec4eae155ec5ebea9e5e0022cc811", size = 35623487, upload-time = "2025-11-11T18:59:36.156Z" }, + { url = "https://files.pythonhosted.org/packages/95/8d/37cc3127c6085ffa6482832a07f84587f2f04dbbaf194cc395bb2afa811c/patchright-1.56.0-py3-none-win_amd64.whl", hash = "sha256:0fe85d8f6b541689185115f3ce69e87a7f5dd964ec6aaeac635d4db5a906aec1", size = 35623491, upload-time = "2025-11-11T18:59:38.853Z" }, + { url = "https://files.pythonhosted.org/packages/c5/37/0b79ddfc6a2ba16e9f258670ea40a74c791d925f1847cf4ad0e739bc7506/patchright-1.56.0-py3-none-win_arm64.whl", hash = "sha256:8bbf1c753e7b07b889a3ff43109ce3cea457605ea6b0477e65af4d9a2c5585cf", size = 31232602, upload-time = "2025-11-11T18:59:41.537Z" }, +] + [[package]] name = "platformdirs" version = "4.5.1" @@ -383,6 +717,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, ] +[[package]] +name = "playwright" +version = "1.56.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet" }, + { name = "pyee" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/31/a5362cee43f844509f1f10d8a27c9cc0e2f7bdce5353d304d93b2151c1b1/playwright-1.56.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:b33eb89c516cbc6723f2e3523bada4a4eb0984a9c411325c02d7016a5d625e9c", size = 40611424, upload-time = "2025-11-11T18:39:10.175Z" }, + { url = "https://files.pythonhosted.org/packages/ef/95/347eef596d8778fb53590dc326c344d427fa19ba3d42b646fce2a4572eb3/playwright-1.56.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b228b3395212b9472a4ee5f1afe40d376eef9568eb039fcb3e563de8f4f4657b", size = 39400228, upload-time = "2025-11-11T18:39:13.915Z" }, + { url = "https://files.pythonhosted.org/packages/b9/54/6ad97b08b2ca1dfcb4fbde4536c4f45c0d9d8b1857a2d20e7bbfdf43bf15/playwright-1.56.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:0ef7e6fd653267798a8a968ff7aa2dcac14398b7dd7440ef57524e01e0fbbd65", size = 40611424, upload-time = "2025-11-11T18:39:17.093Z" }, + { url = "https://files.pythonhosted.org/packages/e4/76/6d409e37e82cdd5dda3df1ab958130ae32b46e42458bd4fc93d7eb8749cb/playwright-1.56.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:404be089b49d94bc4c1fe0dfb07664bda5ffe87789034a03bffb884489bdfb5c", size = 46263122, upload-time = "2025-11-11T18:39:20.619Z" }, + { url = "https://files.pythonhosted.org/packages/4f/84/fb292cc5d45f3252e255ea39066cd1d2385c61c6c1596548dfbf59c88605/playwright-1.56.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64cda7cf4e51c0d35dab55190841bfcdfb5871685ec22cb722cd0ad2df183e34", size = 46110645, upload-time = "2025-11-11T18:39:24.005Z" }, + { url = "https://files.pythonhosted.org/packages/61/bd/8c02c3388ae14edc374ac9f22cbe4e14826c6a51b2d8eaf86e89fabee264/playwright-1.56.0-py3-none-win32.whl", hash = "sha256:d87b79bcb082092d916a332c27ec9732e0418c319755d235d93cc6be13bdd721", size = 35639837, upload-time = "2025-11-11T18:39:27.174Z" }, + { url = "https://files.pythonhosted.org/packages/64/27/f13b538fbc6b7a00152f4379054a49f6abc0bf55ac86f677ae54bc49fb82/playwright-1.56.0-py3-none-win_amd64.whl", hash = "sha256:3c7fc49bb9e673489bf2622855f9486d41c5101bbed964638552b864c4591f94", size = 35639843, upload-time = "2025-11-11T18:39:30.851Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c7/3ee8b556107995846576b4fe42a08ed49b8677619421f2afacf6ee421138/playwright-1.56.0-py3-none-win_arm64.whl", hash = "sha256:2745490ae8dd58d27e5ea4d9aa28402e8e2991eb84fb4b2fd5fbde2106716f6f", size = 31248959, upload-time = "2025-11-11T18:39:33.998Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -529,6 +882,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] +[[package]] +name = "pyee" +version = "13.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/04/e7c1fe4dc78a6fdbfd6c337b1c3732ff543b8a397683ab38378447baa331/pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", size = 31655, upload-time = "2026-02-14T21:12:28.044Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -674,6 +1039,7 @@ dependencies = [ { name = "ndjson" }, { name = "pydantic" }, { name = "requests" }, + { name = "scrapling", extra = ["fetchers"] }, ] [package.dev-dependencies] @@ -697,6 +1063,7 @@ requires-dist = [ { name = "ndjson", specifier = ">=0.3.1" }, { name = "pydantic", specifier = ">=2.11.10" }, { name = "requests", specifier = ">=2.32.5" }, + { name = "scrapling", extras = ["fetchers"], specifier = ">=0.4" }, ] [package.metadata.requires-dev] @@ -711,6 +1078,34 @@ dev = [ { name = "types-requests", specifier = ">=2.32.4.20250913" }, ] +[[package]] +name = "scrapling" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cssselect" }, + { name = "lxml" }, + { name = "orjson" }, + { name = "tld" }, + { name = "typing-extensions" }, + { name = "w3lib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/fa/375d4b78b0c320c46b81464d685c2c3c3138370ae6ac86c785aaab781d5a/scrapling-0.4.1.tar.gz", hash = "sha256:e2c82b6ce9dee2c06060420f862d14c4e2a3c4e8e37e9df76cdbf90dc9d90505", size = 105687, upload-time = "2026-02-27T04:13:19.317Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/7c/7496abccf70473f469521643160b16f0dd2a17bdea18c6825e7240d8dc79/scrapling-0.4.1-py3-none-any.whl", hash = "sha256:b74696b32abcea559ef2dd23989143ac8e733959af2fdd4fb01fcd31ad8cdece", size = 116071, upload-time = "2026-02-27T04:13:17.674Z" }, +] + +[package.optional-dependencies] +fetchers = [ + { name = "anyio" }, + { name = "browserforge" }, + { name = "click" }, + { name = "curl-cffi" }, + { name = "msgspec" }, + { name = "patchright" }, + { name = "playwright" }, +] + [[package]] name = "soupsieve" version = "2.8" @@ -720,6 +1115,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, ] +[[package]] +name = "tld" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/a1/5723b07a70c1841a80afc9ac572fdf53488306848d844cd70519391b0d26/tld-0.13.1.tar.gz", hash = "sha256:75ec00936cbcf564f67361c41713363440b6c4ef0f0c1592b5b0fbe72c17a350", size = 462000, upload-time = "2025-05-21T22:18:29.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/70/b2f38360c3fc4bc9b5e8ef429e1fde63749144ac583c2dbdf7e21e27a9ad/tld-0.13.1-py2.py3-none-any.whl", hash = "sha256:a2d35109433ac83486ddf87e3c4539ab2c5c2478230e5d9c060a18af4b03aa7c", size = 274718, upload-time = "2025-05-21T22:18:25.811Z" }, +] + [[package]] name = "ty" version = "0.0.1a32" @@ -833,3 +1237,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846 wheels = [ { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, ] + +[[package]] +name = "w3lib" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/91/186665abf1a6d16c0c5ea1f0e681d9c852b45c3a750aa8657f8f956690a8/w3lib-2.4.0.tar.gz", hash = "sha256:e233ad21649b69d0e047a10f30181ae9677524a29f6f71f6f3c758dc0c8d2648", size = 48302, upload-time = "2026-01-29T07:05:07.504Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/f5/ce3ab627e0cb51591c9e3dc4b9b173f15d7f2bec1c0010420b15fc442940/w3lib-2.4.0-py3-none-any.whl", hash = "sha256:260b5a22aeb86ae73213857f69ed20829a45150f8d5b12050b1f02ada414db79", size = 21603, upload-time = "2026-01-29T07:05:05.841Z" }, +] From e67426552739a2b52ec1192418a3c12ab934c02f Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Thu, 5 Mar 2026 00:37:29 -0500 Subject: [PATCH 76/86] fix(setup): prevent spurious swap file warnings on `:CP` (#297) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem `setup_problem` explicitly set `swapfile = true` on provisional buffers, overriding the user's global `noswapfile` setting. The resulting `.swp` files triggered E325 warnings on subsequent `:e` calls — especially during the restore path, which redundantly re-opened the current buffer. ## Solution Remove the `swapfile` override so the user's setting is respected, and skip the `:e` call in `setup_problem` when the current buffer already matches the target source file. --- lua/cp/setup.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lua/cp/setup.lua b/lua/cp/setup.lua index e96c37f..19fa776 100644 --- a/lua/cp/setup.lua +++ b/lua/cp/setup.lua @@ -294,7 +294,6 @@ function M.setup_problem(problem_id, language) state.set_provisional(nil) else vim.api.nvim_buf_set_name(prov.bufnr, source_file) - vim.bo[prov.bufnr].swapfile = true -- selene: allow(mixed_table) vim.cmd.write({ vim.fn.fnameescape(source_file), @@ -343,7 +342,10 @@ function M.setup_problem(problem_id, language) end vim.cmd.only({ mods = { silent = true } }) - vim.cmd.e(source_file) + local current_file = vim.fn.expand('%:p') + if current_file ~= vim.fn.fnamemodify(source_file, ':p') then + vim.cmd.e(source_file) + end local bufnr = vim.api.nvim_get_current_buf() state.set_solution_win(vim.api.nvim_get_current_win()) require('cp.ui.views').ensure_io_view() From e9f72dfbbc81c40361af60c38f8a64eadf94962c Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:07:57 -0500 Subject: [PATCH 77/86] feat(cses): implement submit via REST API (#299) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem CSES submit was a stub returning "not yet implemented". ## Solution Authenticate via web login + API token bridge (POST `/login` form, then POST `/api/login` and confirm the auth page), submit source to `/api/courses/problemset/submissions` with base64-encoded content, and poll for verdict. Uses the same username/password credential model as AtCoder — no browser dependencies needed. Tested end-to-end with a real CSES account (verdict: `ACCEPTED`). Also updates `scraper.lua` to pass the full ndjson event object to `on_status` and handle `credentials` events for future platform use. --- lua/cp/scraper.lua | 5 +- lua/cp/submit.lua | 5 +- scrapers/cses.py | 155 +++++++++++++++++++++++++++++++++++++++++++-- t/1068.cc | 54 ++++++++++++++++ 4 files changed, 210 insertions(+), 9 deletions(-) create mode 100644 t/1068.cc diff --git a/lua/cp/scraper.lua b/lua/cp/scraper.lua index 7ddca0a..fc8ba69 100644 --- a/lua/cp/scraper.lua +++ b/lua/cp/scraper.lua @@ -298,9 +298,12 @@ function M.submit( stdin = source_code, env_extra = { CP_CREDENTIALS = vim.json.encode(credentials) }, on_event = function(ev) + if ev.credentials ~= nil then + require('cp.cache').set_credentials(platform, ev.credentials) + end if ev.status ~= nil then if type(on_status) == 'function' then - on_status(ev.status) + on_status(ev) end elseif ev.success ~= nil then done = true diff --git a/lua/cp/submit.lua b/lua/cp/submit.lua index 4efe25e..7dc9a71 100644 --- a/lua/cp/submit.lua +++ b/lua/cp/submit.lua @@ -25,6 +25,7 @@ local function prompt_credentials(platform, callback) vim.fn.inputsave() local password = vim.fn.inputsecret(platform .. ' password: ') vim.fn.inputrestore() + vim.cmd.redraw() if not password or password == '' then logger.log('Submit cancelled', vim.log.levels.WARN) return @@ -64,9 +65,9 @@ function M.submit(opts) language, source_code, creds, - function(status) + function(ev) vim.schedule(function() - vim.notify('[cp.nvim] ' .. (STATUS_MSGS[status] or status), vim.log.levels.INFO) + vim.notify('[cp.nvim] ' .. (STATUS_MSGS[ev.status] or ev.status), vim.log.levels.INFO) end) end, function(result) diff --git a/scrapers/cses.py b/scrapers/cses.py index 473558f..b2e845a 100644 --- a/scrapers/cses.py +++ b/scrapers/cses.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import asyncio +import base64 import json import re from typing import Any @@ -18,6 +19,8 @@ from .models import ( ) BASE_URL = "https://cses.fi" +API_URL = "https://cses.fi/api" +SUBMIT_SCOPE = "courses/problemset" INDEX_PATH = "/problemset" TASK_PATH = "/problemset/task/{id}" HEADERS = { @@ -26,6 +29,16 @@ HEADERS = { TIMEOUT_S = 15.0 CONNECTIONS = 8 +CSES_LANGUAGES: dict[str, dict[str, str]] = { + "C++17": {"name": "C++", "option": "C++17"}, + "Python3": {"name": "Python", "option": "CPython3"}, +} + +EXTENSIONS: dict[str, str] = { + "C++17": "cpp", + "Python3": "py", +} + def normalize_category_name(category_name: str) -> str: return category_name.lower().replace(" ", "_").replace("&", "and") @@ -270,6 +283,65 @@ class CSESScraper(BaseScraper): payload = await coro print(json.dumps(payload), flush=True) + async def _web_login( + self, + client: httpx.AsyncClient, + username: str, + password: str, + ) -> str | None: + login_page = await client.get( + f"{BASE_URL}/login", headers=HEADERS, timeout=TIMEOUT_S + ) + csrf_match = re.search(r'name="csrf_token" value="([^"]+)"', login_page.text) + if not csrf_match: + return None + + login_resp = await client.post( + f"{BASE_URL}/login", + data={ + "csrf_token": csrf_match.group(1), + "nick": username, + "pass": password, + }, + headers=HEADERS, + timeout=TIMEOUT_S, + ) + + if "Invalid username or password" in login_resp.text: + return None + + api_resp = await client.post( + f"{API_URL}/login", headers=HEADERS, timeout=TIMEOUT_S + ) + api_data = api_resp.json() + token: str = api_data["X-Auth-Token"] + auth_url: str = api_data["authentication_url"] + + auth_page = await client.get(auth_url, headers=HEADERS, timeout=TIMEOUT_S) + auth_csrf = re.search(r'name="csrf_token" value="([^"]+)"', auth_page.text) + form_token = re.search(r'name="token" value="([^"]+)"', auth_page.text) + if not auth_csrf or not form_token: + return None + + await client.post( + auth_url, + data={ + "csrf_token": auth_csrf.group(1), + "token": form_token.group(1), + }, + headers=HEADERS, + timeout=TIMEOUT_S, + ) + + check = await client.get( + f"{API_URL}/login", + headers={"X-Auth-Token": token, **HEADERS}, + timeout=TIMEOUT_S, + ) + if check.status_code != 200: + return None + return token + async def submit( self, contest_id: str, @@ -278,12 +350,83 @@ class CSESScraper(BaseScraper): language_id: str, credentials: dict[str, str], ) -> SubmitResult: - return SubmitResult( - success=False, - error="CSES submit not yet implemented", - submission_id="", - verdict="", - ) + username = credentials.get("username", "") + password = credentials.get("password", "") + if not username or not password: + return self._submit_error("Missing credentials. Use :CP login cses") + + async with httpx.AsyncClient(follow_redirects=True) as client: + print(json.dumps({"status": "logging_in"}), flush=True) + + token = await self._web_login(client, username, password) + if not token: + return self._submit_error("Login failed (bad credentials?)") + + print(json.dumps({"status": "submitting"}), flush=True) + + ext = EXTENSIONS.get(language_id, "cpp") + lang = CSES_LANGUAGES.get(language_id, {}) + content_b64 = base64.b64encode(source_code.encode()).decode() + + payload: dict[str, Any] = { + "language": lang, + "filename": f"{problem_id}.{ext}", + "content": content_b64, + } + + r = await client.post( + f"{API_URL}/{SUBMIT_SCOPE}/submissions", + json=payload, + params={"task": problem_id}, + headers={ + "X-Auth-Token": token, + "Content-Type": "application/json", + **HEADERS, + }, + timeout=TIMEOUT_S, + ) + + if r.status_code not in range(200, 300): + try: + err = r.json().get("message", r.text) + except Exception: + err = r.text + return self._submit_error(f"Submit request failed: {err}") + + info = r.json() + submission_id = str(info.get("id", "")) + + for _ in range(60): + await asyncio.sleep(2) + try: + r = await client.get( + f"{API_URL}/{SUBMIT_SCOPE}/submissions/{submission_id}", + params={"poll": "true"}, + headers={ + "X-Auth-Token": token, + **HEADERS, + }, + timeout=30.0, + ) + if r.status_code == 200: + info = r.json() + if not info.get("pending", True): + verdict = info.get("result", "unknown") + return SubmitResult( + success=True, + error="", + submission_id=submission_id, + verdict=verdict, + ) + except Exception: + pass + + return SubmitResult( + success=True, + error="", + submission_id=submission_id, + verdict="submitted (poll timed out)", + ) if __name__ == "__main__": diff --git a/t/1068.cc b/t/1068.cc new file mode 100644 index 0000000..5d3fe37 --- /dev/null +++ b/t/1068.cc @@ -0,0 +1,54 @@ +#include // {{{ + +#include +#ifdef __cpp_lib_ranges_enumerate +#include +namespace rv = std::views; +namespace rs = std::ranges; +#endif + +#pragma GCC optimize("O2,unroll-loops") +#pragma GCC target("avx2,bmi,bmi2,lzcnt,popcnt") + +using namespace std; + +using i32 = int32_t; +using u32 = uint32_t; +using i64 = int64_t; +using u64 = uint64_t; +using f64 = double; +using f128 = long double; + +#if __cplusplus >= 202002L +template +constexpr T MIN = std::numeric_limits::min(); + +template +constexpr T MAX = std::numeric_limits::max(); +#endif + +#ifdef LOCAL +#define db(...) std::print(__VA_ARGS__) +#define dbln(...) std::println(__VA_ARGS__) +#else +#define db(...) +#define dbln(...) +#endif +// }}} + +void solve() { + cout << "hi\n"; +} + +int main() { // {{{ + std::cin.exceptions(std::cin.failbit); +#ifdef LOCAL + std::cerr.rdbuf(std::cout.rdbuf()); + std::cout.setf(std::ios::unitbuf); + std::cerr.setf(std::ios::unitbuf); +#else + std::cin.tie(nullptr)->sync_with_stdio(false); +#endif + solve(); + return 0; +} // }}} From 08593d828db7bac9d6291bde092dc2f9a2ef7254 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 5 Mar 2026 01:18:09 -0500 Subject: [PATCH 78/86] docs: add table of contents to vimdoc --- doc/cp.nvim.txt | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/doc/cp.nvim.txt b/doc/cp.nvim.txt index f61d39e..2c0cc5c 100644 --- a/doc/cp.nvim.txt +++ b/doc/cp.nvim.txt @@ -3,6 +3,37 @@ Author: Barrett Ruth License: Same terms as Vim itself (see |license|) +============================================================================== +CONTENTS *cp-contents* + + 1. Introduction .................................................. |cp.nvim| + 2. Requirements ........................................ |cp-requirements| + 3. Setup ........................................................ |cp-setup| + 4. Configuration ................................................ |cp-config| + 5. Commands .................................................. |cp-commands| + 6. Mappings .................................................. |cp-mappings| + 7. Language Selection .................................. |cp-lang-selection| + 8. Workflow .................................................. |cp-workflow| + 9. Workflow Example ............................................ |cp-example| + 10. Verdict Formatting ................................. |cp-verdict-format| + 11. Picker Integration .......................................... |cp-picker| + 12. Picker Keymaps ........................................ |cp-picker-keys| + 13. Panel ........................................................ |cp-panel| + 14. Interactive Mode .......................................... |cp-interact| + 15. Stress Testing .............................................. |cp-stress| + 16. Race .......................................................... |cp-race| + 17. Credentials ............................................ |cp-credentials| + 18. Submit ...................................................... |cp-submit| + 19. ANSI Colors ................................................... |cp-ansi| + 20. Highlight Groups ........................................ |cp-highlights| + 21. Terminal Colors .................................... |cp-terminal-colors| + 22. Highlight Customization .......................... |cp-highlight-custom| + 23. Helpers .................................................... |cp-helpers| + 24. Statusline Integration .................................. |cp-statusline| + 25. Panel Keymaps .......................................... |cp-panel-keys| + 26. File Structure ................................................ |cp-files| + 27. Health Check ................................................ |cp-health| + ============================================================================== INTRODUCTION *cp.nvim* From 027fae65a477b50f416d36edd84ad3bdcb71cede Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 5 Mar 2026 01:18:16 -0500 Subject: [PATCH 79/86] perf(cses): cache API token across submits Problem: every `:CP submit` on CSES ran the full 5-request login flow (~1.5 s overhead) even when the token from a previous submit was still valid. Solution: persist the API token in credentials via a `credentials` ndjson event. On subsequent submits, validate the cached token with a single GET before falling back to the full login. --- scrapers/cses.py | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/scrapers/cses.py b/scrapers/cses.py index b2e845a..2c2c2ce 100644 --- a/scrapers/cses.py +++ b/scrapers/cses.py @@ -342,6 +342,19 @@ class CSESScraper(BaseScraper): return None return token + async def _check_token( + self, client: httpx.AsyncClient, token: str + ) -> bool: + try: + r = await client.get( + f"{API_URL}/login", + headers={"X-Auth-Token": token, **HEADERS}, + timeout=TIMEOUT_S, + ) + return r.status_code == 200 + except Exception: + return False + async def submit( self, contest_id: str, @@ -356,11 +369,30 @@ class CSESScraper(BaseScraper): return self._submit_error("Missing credentials. Use :CP login cses") async with httpx.AsyncClient(follow_redirects=True) as client: - print(json.dumps({"status": "logging_in"}), flush=True) + token = credentials.get("token") + + if token: + print(json.dumps({"status": "checking_login"}), flush=True) + if not await self._check_token(client, token): + token = None - token = await self._web_login(client, username, password) if not token: - return self._submit_error("Login failed (bad credentials?)") + print(json.dumps({"status": "logging_in"}), flush=True) + token = await self._web_login(client, username, password) + if not token: + return self._submit_error("Login failed (bad credentials?)") + print( + json.dumps( + { + "credentials": { + "username": username, + "password": password, + "token": token, + } + } + ), + flush=True, + ) print(json.dumps({"status": "submitting"}), flush=True) From f4055b071b17abdbe111abf44aecd5fd8a978a3b Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 5 Mar 2026 01:18:23 -0500 Subject: [PATCH 80/86] feat(codeforces): implement submit via headless browser Problem: Codeforces submit was a stub returning "not yet implemented". Solution: use StealthySession (same pattern as AtCoder) to handle Cloudflare Turnstile on the login page, fill credentials, navigate to the contest submit form, upload source via file input, and cache cookies at `~/.cache/cp-nvim/codeforces-cookies.json` so repeat submits skip the login entirely. Uses a single browser page action that checks for the submit form before navigating, avoiding redundant page loads and Turnstile challenges. --- scrapers/codeforces.py | 163 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 160 insertions(+), 3 deletions(-) diff --git a/scrapers/codeforces.py b/scrapers/codeforces.py index c0495d8..7fc5c1c 100644 --- a/scrapers/codeforces.py +++ b/scrapers/codeforces.py @@ -2,7 +2,9 @@ import asyncio import json +import os import re +import tempfile from typing import Any import requests @@ -10,6 +12,7 @@ from bs4 import BeautifulSoup, Tag from curl_cffi import requests as curl_requests from .base import BaseScraper, extract_precision +from .language_ids import get_language_id from .models import ( ContestListResult, ContestSummary, @@ -289,13 +292,167 @@ class CodeforcesScraper(BaseScraper): language_id: str, credentials: dict[str, str], ) -> SubmitResult: + return await asyncio.to_thread( + _submit_headless, + contest_id, + problem_id, + source_code, + language_id, + credentials, + ) + + +def _submit_headless( + contest_id: str, + problem_id: str, + source_code: str, + language_id: str, + credentials: dict[str, str], +) -> SubmitResult: + from pathlib import Path + + try: + from scrapling.fetchers import StealthySession # type: ignore[import-untyped,unresolved-import] + except ImportError: return SubmitResult( success=False, - error="Codeforces submit not yet implemented", - submission_id="", - verdict="", + error="scrapling is required for Codeforces submit", ) + from .atcoder import _ensure_browser, _solve_turnstile + + _ensure_browser() + + cookie_cache = ( + Path.home() / ".cache" / "cp-nvim" / "codeforces-cookies.json" + ) + cookie_cache.parent.mkdir(parents=True, exist_ok=True) + saved_cookies: list[dict[str, Any]] = [] + if cookie_cache.exists(): + try: + saved_cookies = json.loads(cookie_cache.read_text()) + except Exception: + pass + + login_error: str | None = None + submit_error: str | None = None + + def do_login_and_submit(page): + nonlocal login_error, submit_error + + has_submit_form = page.evaluate( + "() => !!document.querySelector('form.submit-form')" + ) + + if not has_submit_form: + if "/enter" not in page.url: + page.goto( + f"{BASE_URL}/enter", + wait_until="domcontentloaded", + timeout=10000, + ) + + try: + _solve_turnstile(page) + except Exception: + pass + + print(json.dumps({"status": "logging_in"}), flush=True) + try: + page.fill( + 'input[name="handleOrEmail"]', + credentials.get("username", ""), + ) + page.fill( + 'input[name="password"]', + credentials.get("password", ""), + ) + page.locator( + '#enterForm input[type="submit"]' + ).click() + page.wait_for_url( + lambda url: "/enter" not in url, timeout=10000 + ) + except Exception as e: + login_error = str(e) + return + + page.goto( + f"{BASE_URL}/contest/{contest_id}/submit", + wait_until="domcontentloaded", + timeout=10000, + ) + + print(json.dumps({"status": "submitting"}), flush=True) + try: + page.select_option( + 'select[name="submittedProblemIndex"]', + problem_id.upper(), + ) + page.select_option( + 'select[name="programTypeId"]', language_id + ) + with tempfile.NamedTemporaryFile( + mode="w", suffix=".cpp", delete=False, prefix="cf_" + ) as tf: + tf.write(source_code) + tmp_path = tf.name + try: + page.set_input_files( + 'input[name="sourceFile"]', tmp_path + ) + page.wait_for_timeout(500) + except Exception: + page.fill('textarea[name="source"]', source_code) + finally: + os.unlink(tmp_path) + page.locator('form.submit-form input.submit').click() + page.wait_for_url( + lambda url: "/my" in url or "/status" in url, + timeout=10000, + ) + except Exception as e: + submit_error = str(e) + + try: + with StealthySession( + headless=True, + timeout=15000, + google_search=False, + cookies=saved_cookies, + ) as session: + print(json.dumps({"status": "checking_login"}), flush=True) + session.fetch( + f"{BASE_URL}/contest/{contest_id}/submit", + page_action=do_login_and_submit, + solve_cloudflare=True, + ) + + try: + browser_cookies = session.context.cookies() + if any( + c["name"] == "JSESSIONID" for c in browser_cookies + ): + cookie_cache.write_text(json.dumps(browser_cookies)) + except Exception: + pass + + if login_error: + return SubmitResult( + success=False, error=f"Login failed: {login_error}" + ) + if submit_error: + return SubmitResult(success=False, error=submit_error) + + return SubmitResult( + success=True, + error="", + submission_id="", + verdict="submitted", + ) + except Exception as e: + return SubmitResult(success=False, error=str(e)) + if __name__ == "__main__": CodeforcesScraper().run_cli() From 2cdde85d36d1c60ab75c6af03edb4cfa344ae9e6 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 5 Mar 2026 01:35:40 -0500 Subject: [PATCH 81/86] refactor: centralize timeout constants in `scrapers/timeouts.py` Problem: each scraper defined its own timeout constants (`TIMEOUT_S`, `TIMEOUT_SECONDS`) with inconsistent values (15s vs 30s) and browser timeouts were scattered as magic numbers (60000, 15000, 5000, 500). Solution: introduce `scrapers/timeouts.py` with named constants for HTTP requests, browser session/navigation/element/turnstile/settle timeouts, and submission polling. All six scrapers now import from the shared module. --- scrapers/atcoder.py | 25 ++++++++++++++++--------- scrapers/codechef.py | 6 +++--- scrapers/codeforces.py | 23 ++++++++++++++--------- scrapers/cses.py | 22 +++++++++++----------- scrapers/kattis.py | 6 +++--- scrapers/timeouts.py | 9 +++++++++ scrapers/usaco.py | 4 ++-- 7 files changed, 58 insertions(+), 37 deletions(-) create mode 100644 scrapers/timeouts.py diff --git a/scrapers/atcoder.py b/scrapers/atcoder.py index 8be75ff..719135e 100644 --- a/scrapers/atcoder.py +++ b/scrapers/atcoder.py @@ -29,11 +29,18 @@ from .models import ( TestCase, TestsResult, ) +from .timeouts import ( + BROWSER_ELEMENT_WAIT, + BROWSER_NAV_TIMEOUT, + BROWSER_SESSION_TIMEOUT, + BROWSER_SETTLE_DELAY, + BROWSER_TURNSTILE_POLL, + HTTP_TIMEOUT, +) MIB_TO_MB = 1.048576 BASE_URL = "https://atcoder.jp" ARCHIVE_URL = f"{BASE_URL}/contests/archive" -TIMEOUT_SECONDS = 30 HEADERS = { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" } @@ -76,7 +83,7 @@ def _retry_after_requests(details): on_backoff=_retry_after_requests, ) def _fetch(url: str) -> str: - r = _session.get(url, headers=HEADERS, timeout=TIMEOUT_SECONDS) + r = _session.get(url, headers=HEADERS, timeout=HTTP_TIMEOUT) if r.status_code in RETRY_STATUS: raise requests.HTTPError(response=r) r.raise_for_status() @@ -99,7 +106,7 @@ def _giveup_httpx(exc: Exception) -> bool: giveup=_giveup_httpx, ) async def _get_async(client: httpx.AsyncClient, url: str) -> str: - r = await client.get(url, headers=HEADERS, timeout=TIMEOUT_SECONDS) + r = await client.get(url, headers=HEADERS, timeout=HTTP_TIMEOUT) r.raise_for_status() return r.text @@ -255,7 +262,7 @@ def _solve_turnstile(page) -> None: except Exception: pass try: - page.wait_for_function(_TURNSTILE_JS, timeout=5000) + page.wait_for_function(_TURNSTILE_JS, timeout=BROWSER_TURNSTILE_POLL) return except Exception: pass @@ -331,7 +338,7 @@ def _submit_headless( page.fill('input[name="username"]', credentials.get("username", "")) page.fill('input[name="password"]', credentials.get("password", "")) page.click("#submit") - page.wait_for_url(lambda url: "/login" not in url, timeout=60000) + page.wait_for_url(lambda url: "/login" not in url, timeout=BROWSER_NAV_TIMEOUT) except Exception as e: login_error = str(e) @@ -345,7 +352,7 @@ def _submit_headless( ) page.locator( f'select[name="data.LanguageId"] option[value="{language_id}"]' - ).wait_for(state="attached", timeout=15000) + ).wait_for(state="attached", timeout=BROWSER_ELEMENT_WAIT) page.select_option('select[name="data.LanguageId"]', language_id) with tempfile.NamedTemporaryFile( mode="w", suffix=".cpp", delete=False, prefix="atcoder_" @@ -354,18 +361,18 @@ def _submit_headless( tmp_path = tf.name try: page.set_input_files("#input-open-file", tmp_path) - page.wait_for_timeout(500) + page.wait_for_timeout(BROWSER_SETTLE_DELAY) finally: os.unlink(tmp_path) page.locator('button[type="submit"]').click() - page.wait_for_url(lambda url: "/submissions/me" in url, timeout=60000) + page.wait_for_url(lambda url: "/submissions/me" in url, timeout=BROWSER_NAV_TIMEOUT) except Exception as e: submit_error = str(e) try: with StealthySession( headless=True, - timeout=60000, + timeout=BROWSER_SESSION_TIMEOUT, google_search=False, cookies=saved_cookies, ) as session: diff --git a/scrapers/codechef.py b/scrapers/codechef.py index 57ce33e..c4b9d37 100644 --- a/scrapers/codechef.py +++ b/scrapers/codechef.py @@ -9,6 +9,7 @@ import httpx from curl_cffi import requests as curl_requests from .base import BaseScraper, extract_precision +from .timeouts import HTTP_TIMEOUT from .models import ( ContestListResult, ContestSummary, @@ -26,7 +27,6 @@ PROBLEM_URL = "https://www.codechef.com/problems/{problem_id}" HEADERS = { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" } -TIMEOUT_S = 15.0 CONNECTIONS = 8 MEMORY_LIMIT_RE = re.compile( r"Memory\s+[Ll]imit.*?([0-9.]+)\s*(MB|GB)", re.IGNORECASE | re.DOTALL @@ -34,7 +34,7 @@ MEMORY_LIMIT_RE = re.compile( async def fetch_json(client: httpx.AsyncClient, path: str) -> dict: - r = await client.get(BASE_URL + path, headers=HEADERS, timeout=TIMEOUT_S) + r = await client.get(BASE_URL + path, headers=HEADERS, timeout=HTTP_TIMEOUT) r.raise_for_status() return r.json() @@ -51,7 +51,7 @@ def _extract_memory_limit(html: str) -> float: def _fetch_html_sync(url: str) -> str: - response = curl_requests.get(url, impersonate="chrome", timeout=TIMEOUT_S) + response = curl_requests.get(url, impersonate="chrome", timeout=HTTP_TIMEOUT) response.raise_for_status() return response.text diff --git a/scrapers/codeforces.py b/scrapers/codeforces.py index 7fc5c1c..05e4ba0 100644 --- a/scrapers/codeforces.py +++ b/scrapers/codeforces.py @@ -21,10 +21,15 @@ from .models import ( SubmitResult, TestCase, ) +from .timeouts import ( + BROWSER_NAV_TIMEOUT, + BROWSER_SESSION_TIMEOUT, + BROWSER_SETTLE_DELAY, + HTTP_TIMEOUT, +) BASE_URL = "https://codeforces.com" API_CONTEST_LIST_URL = f"{BASE_URL}/api/contest.list" -TIMEOUT_SECONDS = 30 HEADERS = { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" } @@ -139,7 +144,7 @@ def _is_interactive(block: Tag) -> bool: def _fetch_problems_html(contest_id: str) -> str: url = f"{BASE_URL}/contest/{contest_id}/problems" - response = curl_requests.get(url, impersonate="chrome", timeout=TIMEOUT_SECONDS) + response = curl_requests.get(url, impersonate="chrome", timeout=HTTP_TIMEOUT) response.raise_for_status() return response.text @@ -226,7 +231,7 @@ class CodeforcesScraper(BaseScraper): async def scrape_contest_list(self) -> ContestListResult: try: - r = requests.get(API_CONTEST_LIST_URL, timeout=TIMEOUT_SECONDS) + r = requests.get(API_CONTEST_LIST_URL, timeout=HTTP_TIMEOUT) r.raise_for_status() data = r.json() if data.get("status") != "OK": @@ -349,7 +354,7 @@ def _submit_headless( page.goto( f"{BASE_URL}/enter", wait_until="domcontentloaded", - timeout=10000, + timeout=BROWSER_NAV_TIMEOUT, ) try: @@ -371,7 +376,7 @@ def _submit_headless( '#enterForm input[type="submit"]' ).click() page.wait_for_url( - lambda url: "/enter" not in url, timeout=10000 + lambda url: "/enter" not in url, timeout=BROWSER_NAV_TIMEOUT ) except Exception as e: login_error = str(e) @@ -380,7 +385,7 @@ def _submit_headless( page.goto( f"{BASE_URL}/contest/{contest_id}/submit", wait_until="domcontentloaded", - timeout=10000, + timeout=BROWSER_NAV_TIMEOUT, ) print(json.dumps({"status": "submitting"}), flush=True) @@ -401,7 +406,7 @@ def _submit_headless( page.set_input_files( 'input[name="sourceFile"]', tmp_path ) - page.wait_for_timeout(500) + page.wait_for_timeout(BROWSER_SETTLE_DELAY) except Exception: page.fill('textarea[name="source"]', source_code) finally: @@ -409,7 +414,7 @@ def _submit_headless( page.locator('form.submit-form input.submit').click() page.wait_for_url( lambda url: "/my" in url or "/status" in url, - timeout=10000, + timeout=BROWSER_NAV_TIMEOUT, ) except Exception as e: submit_error = str(e) @@ -417,7 +422,7 @@ def _submit_headless( try: with StealthySession( headless=True, - timeout=15000, + timeout=BROWSER_SESSION_TIMEOUT, google_search=False, cookies=saved_cookies, ) as session: diff --git a/scrapers/cses.py b/scrapers/cses.py index 2c2c2ce..fe819fc 100644 --- a/scrapers/cses.py +++ b/scrapers/cses.py @@ -9,6 +9,7 @@ from typing import Any import httpx from .base import BaseScraper, extract_precision +from .timeouts import HTTP_TIMEOUT, SUBMIT_POLL_TIMEOUT from .models import ( ContestListResult, ContestSummary, @@ -26,7 +27,6 @@ TASK_PATH = "/problemset/task/{id}" HEADERS = { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" } -TIMEOUT_S = 15.0 CONNECTIONS = 8 CSES_LANGUAGES: dict[str, dict[str, str]] = { @@ -78,7 +78,7 @@ def snake_to_title(name: str) -> str: async def fetch_text(client: httpx.AsyncClient, path: str) -> str: - r = await client.get(BASE_URL + path, headers=HEADERS, timeout=TIMEOUT_S) + r = await client.get(BASE_URL + path, headers=HEADERS, timeout=HTTP_TIMEOUT) r.raise_for_status() return r.text @@ -290,7 +290,7 @@ class CSESScraper(BaseScraper): password: str, ) -> str | None: login_page = await client.get( - f"{BASE_URL}/login", headers=HEADERS, timeout=TIMEOUT_S + f"{BASE_URL}/login", headers=HEADERS, timeout=HTTP_TIMEOUT ) csrf_match = re.search(r'name="csrf_token" value="([^"]+)"', login_page.text) if not csrf_match: @@ -304,20 +304,20 @@ class CSESScraper(BaseScraper): "pass": password, }, headers=HEADERS, - timeout=TIMEOUT_S, + timeout=HTTP_TIMEOUT, ) if "Invalid username or password" in login_resp.text: return None api_resp = await client.post( - f"{API_URL}/login", headers=HEADERS, timeout=TIMEOUT_S + f"{API_URL}/login", headers=HEADERS, timeout=HTTP_TIMEOUT ) api_data = api_resp.json() token: str = api_data["X-Auth-Token"] auth_url: str = api_data["authentication_url"] - auth_page = await client.get(auth_url, headers=HEADERS, timeout=TIMEOUT_S) + auth_page = await client.get(auth_url, headers=HEADERS, timeout=HTTP_TIMEOUT) auth_csrf = re.search(r'name="csrf_token" value="([^"]+)"', auth_page.text) form_token = re.search(r'name="token" value="([^"]+)"', auth_page.text) if not auth_csrf or not form_token: @@ -330,13 +330,13 @@ class CSESScraper(BaseScraper): "token": form_token.group(1), }, headers=HEADERS, - timeout=TIMEOUT_S, + timeout=HTTP_TIMEOUT, ) check = await client.get( f"{API_URL}/login", headers={"X-Auth-Token": token, **HEADERS}, - timeout=TIMEOUT_S, + timeout=HTTP_TIMEOUT, ) if check.status_code != 200: return None @@ -349,7 +349,7 @@ class CSESScraper(BaseScraper): r = await client.get( f"{API_URL}/login", headers={"X-Auth-Token": token, **HEADERS}, - timeout=TIMEOUT_S, + timeout=HTTP_TIMEOUT, ) return r.status_code == 200 except Exception: @@ -415,7 +415,7 @@ class CSESScraper(BaseScraper): "Content-Type": "application/json", **HEADERS, }, - timeout=TIMEOUT_S, + timeout=HTTP_TIMEOUT, ) if r.status_code not in range(200, 300): @@ -438,7 +438,7 @@ class CSESScraper(BaseScraper): "X-Auth-Token": token, **HEADERS, }, - timeout=30.0, + timeout=SUBMIT_POLL_TIMEOUT, ) if r.status_code == 200: info = r.json() diff --git a/scrapers/kattis.py b/scrapers/kattis.py index d1675bf..2bfd2d6 100644 --- a/scrapers/kattis.py +++ b/scrapers/kattis.py @@ -10,6 +10,7 @@ from datetime import datetime import httpx from .base import BaseScraper +from .timeouts import HTTP_TIMEOUT from .models import ( ContestListResult, ContestSummary, @@ -23,7 +24,6 @@ BASE_URL = "https://open.kattis.com" HEADERS = { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" } -TIMEOUT_S = 15.0 CONNECTIONS = 8 TIME_RE = re.compile( @@ -37,13 +37,13 @@ MEM_RE = re.compile( async def _fetch_text(client: httpx.AsyncClient, url: str) -> str: - r = await client.get(url, headers=HEADERS, timeout=TIMEOUT_S) + r = await client.get(url, headers=HEADERS, timeout=HTTP_TIMEOUT) r.raise_for_status() return r.text async def _fetch_bytes(client: httpx.AsyncClient, url: str) -> bytes: - r = await client.get(url, headers=HEADERS, timeout=TIMEOUT_S) + r = await client.get(url, headers=HEADERS, timeout=HTTP_TIMEOUT) r.raise_for_status() return r.content diff --git a/scrapers/timeouts.py b/scrapers/timeouts.py new file mode 100644 index 0000000..a21ad0d --- /dev/null +++ b/scrapers/timeouts.py @@ -0,0 +1,9 @@ +HTTP_TIMEOUT = 15.0 + +BROWSER_SESSION_TIMEOUT = 15000 +BROWSER_NAV_TIMEOUT = 10000 +BROWSER_TURNSTILE_POLL = 5000 +BROWSER_ELEMENT_WAIT = 10000 +BROWSER_SETTLE_DELAY = 500 + +SUBMIT_POLL_TIMEOUT = 30.0 diff --git a/scrapers/usaco.py b/scrapers/usaco.py index 565f1b5..b78f88e 100644 --- a/scrapers/usaco.py +++ b/scrapers/usaco.py @@ -8,6 +8,7 @@ from typing import Any, cast import httpx from .base import BaseScraper +from .timeouts import HTTP_TIMEOUT from .models import ( ContestListResult, ContestSummary, @@ -21,7 +22,6 @@ BASE_URL = "http://www.usaco.org" HEADERS = { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" } -TIMEOUT_S = 15.0 CONNECTIONS = 4 MONTHS = [ @@ -58,7 +58,7 @@ RESULTS_PAGE_RE = re.compile( async def _fetch_text(client: httpx.AsyncClient, url: str) -> str: - r = await client.get(url, headers=HEADERS, timeout=TIMEOUT_S, follow_redirects=True) + r = await client.get(url, headers=HEADERS, timeout=HTTP_TIMEOUT, follow_redirects=True) r.raise_for_status() return r.text From 1afe41103fb1f28d868e423cfe41ed0c3acb1d5f Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 5 Mar 2026 01:39:59 -0500 Subject: [PATCH 82/86] ci: format --- scrapers/atcoder.py | 8 ++++++-- scrapers/codeforces.py | 26 +++++++------------------- scrapers/cses.py | 4 +--- scrapers/usaco.py | 4 +++- 4 files changed, 17 insertions(+), 25 deletions(-) diff --git a/scrapers/atcoder.py b/scrapers/atcoder.py index 719135e..16eba40 100644 --- a/scrapers/atcoder.py +++ b/scrapers/atcoder.py @@ -338,7 +338,9 @@ def _submit_headless( page.fill('input[name="username"]', credentials.get("username", "")) page.fill('input[name="password"]', credentials.get("password", "")) page.click("#submit") - page.wait_for_url(lambda url: "/login" not in url, timeout=BROWSER_NAV_TIMEOUT) + page.wait_for_url( + lambda url: "/login" not in url, timeout=BROWSER_NAV_TIMEOUT + ) except Exception as e: login_error = str(e) @@ -365,7 +367,9 @@ def _submit_headless( finally: os.unlink(tmp_path) page.locator('button[type="submit"]').click() - page.wait_for_url(lambda url: "/submissions/me" in url, timeout=BROWSER_NAV_TIMEOUT) + page.wait_for_url( + lambda url: "/submissions/me" in url, timeout=BROWSER_NAV_TIMEOUT + ) except Exception as e: submit_error = str(e) diff --git a/scrapers/codeforces.py b/scrapers/codeforces.py index 05e4ba0..8eaa874 100644 --- a/scrapers/codeforces.py +++ b/scrapers/codeforces.py @@ -328,9 +328,7 @@ def _submit_headless( _ensure_browser() - cookie_cache = ( - Path.home() / ".cache" / "cp-nvim" / "codeforces-cookies.json" - ) + cookie_cache = Path.home() / ".cache" / "cp-nvim" / "codeforces-cookies.json" cookie_cache.parent.mkdir(parents=True, exist_ok=True) saved_cookies: list[dict[str, Any]] = [] if cookie_cache.exists(): @@ -372,9 +370,7 @@ def _submit_headless( 'input[name="password"]', credentials.get("password", ""), ) - page.locator( - '#enterForm input[type="submit"]' - ).click() + page.locator('#enterForm input[type="submit"]').click() page.wait_for_url( lambda url: "/enter" not in url, timeout=BROWSER_NAV_TIMEOUT ) @@ -394,24 +390,20 @@ def _submit_headless( 'select[name="submittedProblemIndex"]', problem_id.upper(), ) - page.select_option( - 'select[name="programTypeId"]', language_id - ) + page.select_option('select[name="programTypeId"]', language_id) with tempfile.NamedTemporaryFile( mode="w", suffix=".cpp", delete=False, prefix="cf_" ) as tf: tf.write(source_code) tmp_path = tf.name try: - page.set_input_files( - 'input[name="sourceFile"]', tmp_path - ) + page.set_input_files('input[name="sourceFile"]', tmp_path) page.wait_for_timeout(BROWSER_SETTLE_DELAY) except Exception: page.fill('textarea[name="source"]', source_code) finally: os.unlink(tmp_path) - page.locator('form.submit-form input.submit').click() + page.locator("form.submit-form input.submit").click() page.wait_for_url( lambda url: "/my" in url or "/status" in url, timeout=BROWSER_NAV_TIMEOUT, @@ -435,17 +427,13 @@ def _submit_headless( try: browser_cookies = session.context.cookies() - if any( - c["name"] == "JSESSIONID" for c in browser_cookies - ): + if any(c["name"] == "JSESSIONID" for c in browser_cookies): cookie_cache.write_text(json.dumps(browser_cookies)) except Exception: pass if login_error: - return SubmitResult( - success=False, error=f"Login failed: {login_error}" - ) + return SubmitResult(success=False, error=f"Login failed: {login_error}") if submit_error: return SubmitResult(success=False, error=submit_error) diff --git a/scrapers/cses.py b/scrapers/cses.py index fe819fc..7d9f4f0 100644 --- a/scrapers/cses.py +++ b/scrapers/cses.py @@ -342,9 +342,7 @@ class CSESScraper(BaseScraper): return None return token - async def _check_token( - self, client: httpx.AsyncClient, token: str - ) -> bool: + async def _check_token(self, client: httpx.AsyncClient, token: str) -> bool: try: r = await client.get( f"{API_URL}/login", diff --git a/scrapers/usaco.py b/scrapers/usaco.py index b78f88e..9e4d7da 100644 --- a/scrapers/usaco.py +++ b/scrapers/usaco.py @@ -58,7 +58,9 @@ RESULTS_PAGE_RE = re.compile( async def _fetch_text(client: httpx.AsyncClient, url: str) -> str: - r = await client.get(url, headers=HEADERS, timeout=HTTP_TIMEOUT, follow_redirects=True) + r = await client.get( + url, headers=HEADERS, timeout=HTTP_TIMEOUT, follow_redirects=True + ) r.raise_for_status() return r.text From 73687479460aff950606dafce1cc63e55eaf149b Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 5 Mar 2026 10:35:27 -0500 Subject: [PATCH 83/86] perf(atcoder): bail out early from `_solve_turnstile` when no iframe present Problem: `_solve_turnstile` looped 6 times with ~20s per iteration (15s bounding_box timeout + 5s wait_for_function) when no Turnstile iframe existed on the page, causing a 120-second delay on pages that don't require Turnstile verification. Solution: check for existing token and iframe presence before entering the retry loop. `iframe_loc.count()` returns immediately when no matching elements exist, avoiding the expensive timeout cascade. --- scrapers/atcoder.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scrapers/atcoder.py b/scrapers/atcoder.py index 16eba40..45a2195 100644 --- a/scrapers/atcoder.py +++ b/scrapers/atcoder.py @@ -246,14 +246,14 @@ _TURNSTILE_JS = "() => { const el = document.querySelector('[name=\"cf-turnstile def _solve_turnstile(page) -> None: + if page.evaluate(_TURNSTILE_JS): + return + iframe_loc = page.locator('iframe[src*="challenges.cloudflare.com"]') + if not iframe_loc.count(): + return for _ in range(6): - has_token = page.evaluate(_TURNSTILE_JS) - if has_token: - return try: - box = page.locator( - 'iframe[src*="challenges.cloudflare.com"]' - ).first.bounding_box() + box = iframe_loc.first.bounding_box() if box: page.mouse.click( box["x"] + box["width"] * 0.15, From 3ecd200da7c16fdd78a35921e1432a898dc75df1 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 5 Mar 2026 10:35:36 -0500 Subject: [PATCH 84/86] refactor(codeforces): use separate fetches for login and submit Problem: the single `do_login_and_submit` page action navigated between pages within one `session.fetch` call, which was fragile and couldn't leverage `solve_cloudflare` for the Turnstile gate on the submit page. The submit button click also blocked on navigation completion, causing timeouts when CF was slow to process. Solution: split into three separate `session.fetch` calls (homepage login check, `/enter` login, `/contest/{id}/submit`) with `solve_cloudflare=True` on login and submit. Use `no_wait_after=True` on the submit click with a doubled nav timeout. Extract `span.error` text on submit failure instead of a generic timeout message. --- scrapers/codeforces.py | 135 +++++++++++++++++++++++++---------------- 1 file changed, 84 insertions(+), 51 deletions(-) diff --git a/scrapers/codeforces.py b/scrapers/codeforces.py index 8eaa874..8002398 100644 --- a/scrapers/codeforces.py +++ b/scrapers/codeforces.py @@ -22,6 +22,7 @@ from .models import ( TestCase, ) from .timeouts import ( + BROWSER_ELEMENT_WAIT, BROWSER_NAV_TIMEOUT, BROWSER_SESSION_TIMEOUT, BROWSER_SETTLE_DELAY, @@ -307,6 +308,18 @@ class CodeforcesScraper(BaseScraper): ) +def _wait_for_gate_reload(page, wait_selector: str) -> None: + from .atcoder import _solve_turnstile + + if "Verification" not in page.title(): + return + _solve_turnstile(page) + page.wait_for_function( + f"() => !!document.querySelector('{wait_selector}')", + timeout=BROWSER_ELEMENT_WAIT, + ) + + def _submit_headless( contest_id: str, problem_id: str, @@ -337,54 +350,46 @@ def _submit_headless( except Exception: pass + logged_in = False login_error: str | None = None submit_error: str | None = None - def do_login_and_submit(page): - nonlocal login_error, submit_error - - has_submit_form = page.evaluate( - "() => !!document.querySelector('form.submit-form')" + def check_login(page): + nonlocal logged_in + logged_in = page.evaluate( + "() => Array.from(document.querySelectorAll('a'))" + ".some(a => a.textContent.includes('Logout'))" ) - if not has_submit_form: - if "/enter" not in page.url: - page.goto( - f"{BASE_URL}/enter", - wait_until="domcontentloaded", - timeout=BROWSER_NAV_TIMEOUT, - ) - - try: - _solve_turnstile(page) - except Exception: - pass - - print(json.dumps({"status": "logging_in"}), flush=True) - try: - page.fill( - 'input[name="handleOrEmail"]', - credentials.get("username", ""), - ) - page.fill( - 'input[name="password"]', - credentials.get("password", ""), - ) - page.locator('#enterForm input[type="submit"]').click() - page.wait_for_url( - lambda url: "/enter" not in url, timeout=BROWSER_NAV_TIMEOUT - ) - except Exception as e: - login_error = str(e) - return - - page.goto( - f"{BASE_URL}/contest/{contest_id}/submit", - wait_until="domcontentloaded", - timeout=BROWSER_NAV_TIMEOUT, + def login_action(page): + nonlocal login_error + try: + _wait_for_gate_reload(page, "#enterForm") + except Exception: + pass + try: + page.fill( + 'input[name="handleOrEmail"]', + credentials.get("username", ""), ) + page.fill( + 'input[name="password"]', + credentials.get("password", ""), + ) + page.locator('#enterForm input[type="submit"]').click() + page.wait_for_url( + lambda url: "/enter" not in url, timeout=BROWSER_NAV_TIMEOUT + ) + except Exception as e: + login_error = str(e) - print(json.dumps({"status": "submitting"}), flush=True) + def submit_action(page): + nonlocal submit_error + try: + _solve_turnstile(page) + except Exception: + pass + tmp_path: str | None = None try: page.select_option( 'select[name="submittedProblemIndex"]', @@ -401,15 +406,26 @@ def _submit_headless( page.wait_for_timeout(BROWSER_SETTLE_DELAY) except Exception: page.fill('textarea[name="source"]', source_code) - finally: - os.unlink(tmp_path) - page.locator("form.submit-form input.submit").click() - page.wait_for_url( - lambda url: "/my" in url or "/status" in url, - timeout=BROWSER_NAV_TIMEOUT, - ) + page.locator("form.submit-form input.submit").click(no_wait_after=True) + try: + page.wait_for_url( + lambda url: "/my" in url or "/status" in url, + timeout=BROWSER_NAV_TIMEOUT * 2, + ) + except Exception: + err_el = page.query_selector("span.error") + if err_el: + submit_error = err_el.inner_text().strip() + else: + submit_error = "Submit failed: page did not navigate" except Exception as e: submit_error = str(e) + finally: + if tmp_path: + try: + os.unlink(tmp_path) + except OSError: + pass try: with StealthySession( @@ -419,9 +435,28 @@ def _submit_headless( cookies=saved_cookies, ) as session: print(json.dumps({"status": "checking_login"}), flush=True) + session.fetch( + f"{BASE_URL}/", + page_action=check_login, + network_idle=True, + ) + + if not logged_in: + print(json.dumps({"status": "logging_in"}), flush=True) + session.fetch( + f"{BASE_URL}/enter", + page_action=login_action, + solve_cloudflare=True, + ) + if login_error: + return SubmitResult( + success=False, error=f"Login failed: {login_error}" + ) + + print(json.dumps({"status": "submitting"}), flush=True) session.fetch( f"{BASE_URL}/contest/{contest_id}/submit", - page_action=do_login_and_submit, + page_action=submit_action, solve_cloudflare=True, ) @@ -432,8 +467,6 @@ def _submit_headless( except Exception: pass - if login_error: - return SubmitResult(success=False, error=f"Login failed: {login_error}") if submit_error: return SubmitResult(success=False, error=submit_error) From 38cd0482f05993e06a69caa89a563a1106c3f985 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 5 Mar 2026 10:37:13 -0500 Subject: [PATCH 85/86] ci: remove unused var --- scrapers/codeforces.py | 1 - t/{1068.cc => a.cc} | 8 +++-- t/cf_cli_debug.py | 67 ++++++++++++++++++++++++++++++++++++++++ t/cf_cli_real.py | 30 ++++++++++++++++++ t/cf_exact.py | 13 ++++++++ t/cf_hang_debug.py | 70 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 186 insertions(+), 3 deletions(-) rename t/{1068.cc => a.cc} (90%) create mode 100644 t/cf_cli_debug.py create mode 100644 t/cf_cli_real.py create mode 100644 t/cf_exact.py create mode 100644 t/cf_hang_debug.py diff --git a/scrapers/codeforces.py b/scrapers/codeforces.py index 8002398..fd0c129 100644 --- a/scrapers/codeforces.py +++ b/scrapers/codeforces.py @@ -12,7 +12,6 @@ from bs4 import BeautifulSoup, Tag from curl_cffi import requests as curl_requests from .base import BaseScraper, extract_precision -from .language_ids import get_language_id from .models import ( ContestListResult, ContestSummary, diff --git a/t/1068.cc b/t/a.cc similarity index 90% rename from t/1068.cc rename to t/a.cc index 5d3fe37..b8f7123 100644 --- a/t/1068.cc +++ b/t/a.cc @@ -37,7 +37,7 @@ constexpr T MAX = std::numeric_limits::max(); // }}} void solve() { - cout << "hi\n"; + std::cout << "change\n"; } int main() { // {{{ @@ -49,6 +49,10 @@ int main() { // {{{ #else std::cin.tie(nullptr)->sync_with_stdio(false); #endif - solve(); + u32 tc = 1; + std::cin >> tc; + for (u32 t = 0; t < tc; ++t) { + solve(); + } return 0; } // }}} diff --git a/t/cf_cli_debug.py b/t/cf_cli_debug.py new file mode 100644 index 0000000..515df76 --- /dev/null +++ b/t/cf_cli_debug.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""Reproduce CLI hang: go through asyncio.to_thread like the real code.""" +import asyncio +import json +import sys +from pathlib import Path + +sys.path.insert(0, ".") + +from scrapers.atcoder import _ensure_browser, _solve_turnstile +from scrapers.codeforces import BASE_URL, _wait_for_gate_reload +from scrapers.timeouts import BROWSER_SESSION_TIMEOUT + + +def _test_submit(): + from scrapling.fetchers import StealthySession + + _ensure_browser() + + cookie_cache = Path.home() / ".cache" / "cp-nvim" / "codeforces-cookies.json" + saved_cookies = [] + if cookie_cache.exists(): + try: + saved_cookies = json.loads(cookie_cache.read_text()) + except Exception: + pass + + logged_in = False + + def check_login(page): + nonlocal logged_in + logged_in = page.evaluate( + "() => Array.from(document.querySelectorAll('a'))" + ".some(a => a.textContent.includes('Logout'))" + ) + print(f"logged_in: {logged_in}", flush=True) + + def submit_action(page): + print(f"ENTERED submit_action: url={page.url}", flush=True) + + with StealthySession( + headless=True, + timeout=BROWSER_SESSION_TIMEOUT, + google_search=False, + cookies=saved_cookies, + ) as session: + print("fetch homepage...", flush=True) + session.fetch(f"{BASE_URL}/", page_action=check_login, network_idle=True) + + print("fetch submit page...", flush=True) + session.fetch( + f"{BASE_URL}/contest/1933/submit", + page_action=submit_action, + ) + print("DONE", flush=True) + + return "ok" + + +async def main(): + print("Running via asyncio.to_thread...", flush=True) + result = await asyncio.to_thread(_test_submit) + print(f"Result: {result}", flush=True) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/t/cf_cli_real.py b/t/cf_cli_real.py new file mode 100644 index 0000000..448dab1 --- /dev/null +++ b/t/cf_cli_real.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +"""Simulate exactly what the CLI does.""" +import asyncio +import json +import os +import sys + +sys.path.insert(0, ".") + +SOURCE = '#include \nusing namespace std;\nint main() { cout << 42; }\n' + + +async def main(): + from scrapers.codeforces import CodeforcesScraper + from scrapers.language_ids import get_language_id + + scraper = CodeforcesScraper() + credentials = json.loads(os.environ.get("CP_CREDENTIALS", "{}")) + language_id = get_language_id("codeforces", "cpp") or "89" + + print(f"source length: {len(SOURCE)}", flush=True) + print(f"credentials keys: {list(credentials.keys())}", flush=True) + print(f"language_id: {language_id}", flush=True) + + result = await scraper.submit("1933", "a", SOURCE, language_id, credentials) + print(result.model_dump_json(indent=2), flush=True) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/t/cf_exact.py b/t/cf_exact.py new file mode 100644 index 0000000..e65bbdb --- /dev/null +++ b/t/cf_exact.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +"""Call _submit_headless directly, no asyncio.""" +import json +import os +import sys + +sys.path.insert(0, ".") + +from scrapers.codeforces import _submit_headless + +creds = json.loads(os.environ.get("CP_CREDENTIALS", "{}")) +result = _submit_headless("1933", "a", "int main(){}", "89", creds) +print(result.model_dump_json(indent=2)) diff --git a/t/cf_hang_debug.py b/t/cf_hang_debug.py new file mode 100644 index 0000000..0c3ba0e --- /dev/null +++ b/t/cf_hang_debug.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Pinpoint where session.fetch hangs on the submit page.""" +import json +import sys +import threading +from pathlib import Path + +sys.path.insert(0, ".") + +from scrapers.atcoder import _ensure_browser +from scrapers.codeforces import BASE_URL +from scrapers.timeouts import BROWSER_SESSION_TIMEOUT + + +def watchdog(label, timeout=20): + import time + time.sleep(timeout) + print(f"WATCHDOG: {label} timed out after {timeout}s", flush=True) + import os + os._exit(1) + + +def main(): + from scrapling.fetchers import StealthySession + + _ensure_browser() + + cookie_cache = Path.home() / ".cache" / "cp-nvim" / "codeforces-cookies.json" + saved_cookies = [] + if cookie_cache.exists(): + try: + saved_cookies = json.loads(cookie_cache.read_text()) + except Exception: + pass + + def check_login(page): + logged_in = page.evaluate( + "() => Array.from(document.querySelectorAll('a'))" + ".some(a => a.textContent.includes('Logout'))" + ) + print(f"logged_in: {logged_in}", flush=True) + + def submit_action(page): + print(f"submit_action ENTERED: url={page.url} title={page.title()}", flush=True) + + try: + with StealthySession( + headless=True, + timeout=BROWSER_SESSION_TIMEOUT, + google_search=False, + cookies=saved_cookies, + ) as session: + print("1. Homepage...", flush=True) + session.fetch(f"{BASE_URL}/", page_action=check_login, network_idle=True) + + print("2. Submit page (no network_idle, no solve_cloudflare)...", flush=True) + t = threading.Thread(target=watchdog, args=("session.fetch submit", 30), daemon=True) + t.start() + + session.fetch( + f"{BASE_URL}/contest/1933/submit", + page_action=submit_action, + ) + print("3. Done!", flush=True) + except Exception as e: + print(f"FATAL: {type(e).__name__}: {e}", flush=True) + + +if __name__ == "__main__": + main() From c95f7f4c536baa7de7cadfdafa17fc085deb6210 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 5 Mar 2026 10:37:26 -0500 Subject: [PATCH 86/86] chore: remove accidentally committed files --- t/a.cc | 58 -------------------------------------- t/cf_cli_debug.py | 67 -------------------------------------------- t/cf_cli_real.py | 30 -------------------- t/cf_exact.py | 13 --------- t/cf_hang_debug.py | 70 ---------------------------------------------- 5 files changed, 238 deletions(-) delete mode 100644 t/a.cc delete mode 100644 t/cf_cli_debug.py delete mode 100644 t/cf_cli_real.py delete mode 100644 t/cf_exact.py delete mode 100644 t/cf_hang_debug.py diff --git a/t/a.cc b/t/a.cc deleted file mode 100644 index b8f7123..0000000 --- a/t/a.cc +++ /dev/null @@ -1,58 +0,0 @@ -#include // {{{ - -#include -#ifdef __cpp_lib_ranges_enumerate -#include -namespace rv = std::views; -namespace rs = std::ranges; -#endif - -#pragma GCC optimize("O2,unroll-loops") -#pragma GCC target("avx2,bmi,bmi2,lzcnt,popcnt") - -using namespace std; - -using i32 = int32_t; -using u32 = uint32_t; -using i64 = int64_t; -using u64 = uint64_t; -using f64 = double; -using f128 = long double; - -#if __cplusplus >= 202002L -template -constexpr T MIN = std::numeric_limits::min(); - -template -constexpr T MAX = std::numeric_limits::max(); -#endif - -#ifdef LOCAL -#define db(...) std::print(__VA_ARGS__) -#define dbln(...) std::println(__VA_ARGS__) -#else -#define db(...) -#define dbln(...) -#endif -// }}} - -void solve() { - std::cout << "change\n"; -} - -int main() { // {{{ - std::cin.exceptions(std::cin.failbit); -#ifdef LOCAL - std::cerr.rdbuf(std::cout.rdbuf()); - std::cout.setf(std::ios::unitbuf); - std::cerr.setf(std::ios::unitbuf); -#else - std::cin.tie(nullptr)->sync_with_stdio(false); -#endif - u32 tc = 1; - std::cin >> tc; - for (u32 t = 0; t < tc; ++t) { - solve(); - } - return 0; -} // }}} diff --git a/t/cf_cli_debug.py b/t/cf_cli_debug.py deleted file mode 100644 index 515df76..0000000 --- a/t/cf_cli_debug.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python3 -"""Reproduce CLI hang: go through asyncio.to_thread like the real code.""" -import asyncio -import json -import sys -from pathlib import Path - -sys.path.insert(0, ".") - -from scrapers.atcoder import _ensure_browser, _solve_turnstile -from scrapers.codeforces import BASE_URL, _wait_for_gate_reload -from scrapers.timeouts import BROWSER_SESSION_TIMEOUT - - -def _test_submit(): - from scrapling.fetchers import StealthySession - - _ensure_browser() - - cookie_cache = Path.home() / ".cache" / "cp-nvim" / "codeforces-cookies.json" - saved_cookies = [] - if cookie_cache.exists(): - try: - saved_cookies = json.loads(cookie_cache.read_text()) - except Exception: - pass - - logged_in = False - - def check_login(page): - nonlocal logged_in - logged_in = page.evaluate( - "() => Array.from(document.querySelectorAll('a'))" - ".some(a => a.textContent.includes('Logout'))" - ) - print(f"logged_in: {logged_in}", flush=True) - - def submit_action(page): - print(f"ENTERED submit_action: url={page.url}", flush=True) - - with StealthySession( - headless=True, - timeout=BROWSER_SESSION_TIMEOUT, - google_search=False, - cookies=saved_cookies, - ) as session: - print("fetch homepage...", flush=True) - session.fetch(f"{BASE_URL}/", page_action=check_login, network_idle=True) - - print("fetch submit page...", flush=True) - session.fetch( - f"{BASE_URL}/contest/1933/submit", - page_action=submit_action, - ) - print("DONE", flush=True) - - return "ok" - - -async def main(): - print("Running via asyncio.to_thread...", flush=True) - result = await asyncio.to_thread(_test_submit) - print(f"Result: {result}", flush=True) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/t/cf_cli_real.py b/t/cf_cli_real.py deleted file mode 100644 index 448dab1..0000000 --- a/t/cf_cli_real.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 -"""Simulate exactly what the CLI does.""" -import asyncio -import json -import os -import sys - -sys.path.insert(0, ".") - -SOURCE = '#include \nusing namespace std;\nint main() { cout << 42; }\n' - - -async def main(): - from scrapers.codeforces import CodeforcesScraper - from scrapers.language_ids import get_language_id - - scraper = CodeforcesScraper() - credentials = json.loads(os.environ.get("CP_CREDENTIALS", "{}")) - language_id = get_language_id("codeforces", "cpp") or "89" - - print(f"source length: {len(SOURCE)}", flush=True) - print(f"credentials keys: {list(credentials.keys())}", flush=True) - print(f"language_id: {language_id}", flush=True) - - result = await scraper.submit("1933", "a", SOURCE, language_id, credentials) - print(result.model_dump_json(indent=2), flush=True) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/t/cf_exact.py b/t/cf_exact.py deleted file mode 100644 index e65bbdb..0000000 --- a/t/cf_exact.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python3 -"""Call _submit_headless directly, no asyncio.""" -import json -import os -import sys - -sys.path.insert(0, ".") - -from scrapers.codeforces import _submit_headless - -creds = json.loads(os.environ.get("CP_CREDENTIALS", "{}")) -result = _submit_headless("1933", "a", "int main(){}", "89", creds) -print(result.model_dump_json(indent=2)) diff --git a/t/cf_hang_debug.py b/t/cf_hang_debug.py deleted file mode 100644 index 0c3ba0e..0000000 --- a/t/cf_hang_debug.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python3 -"""Pinpoint where session.fetch hangs on the submit page.""" -import json -import sys -import threading -from pathlib import Path - -sys.path.insert(0, ".") - -from scrapers.atcoder import _ensure_browser -from scrapers.codeforces import BASE_URL -from scrapers.timeouts import BROWSER_SESSION_TIMEOUT - - -def watchdog(label, timeout=20): - import time - time.sleep(timeout) - print(f"WATCHDOG: {label} timed out after {timeout}s", flush=True) - import os - os._exit(1) - - -def main(): - from scrapling.fetchers import StealthySession - - _ensure_browser() - - cookie_cache = Path.home() / ".cache" / "cp-nvim" / "codeforces-cookies.json" - saved_cookies = [] - if cookie_cache.exists(): - try: - saved_cookies = json.loads(cookie_cache.read_text()) - except Exception: - pass - - def check_login(page): - logged_in = page.evaluate( - "() => Array.from(document.querySelectorAll('a'))" - ".some(a => a.textContent.includes('Logout'))" - ) - print(f"logged_in: {logged_in}", flush=True) - - def submit_action(page): - print(f"submit_action ENTERED: url={page.url} title={page.title()}", flush=True) - - try: - with StealthySession( - headless=True, - timeout=BROWSER_SESSION_TIMEOUT, - google_search=False, - cookies=saved_cookies, - ) as session: - print("1. Homepage...", flush=True) - session.fetch(f"{BASE_URL}/", page_action=check_login, network_idle=True) - - print("2. Submit page (no network_idle, no solve_cloudflare)...", flush=True) - t = threading.Thread(target=watchdog, args=("session.fetch submit", 30), daemon=True) - t.start() - - session.fetch( - f"{BASE_URL}/contest/1933/submit", - page_action=submit_action, - ) - print("3. Done!", flush=True) - except Exception as e: - print(f"FATAL: {type(e).__name__}: {e}", flush=True) - - -if __name__ == "__main__": - main()