From d82e9fb3c0e8c448314f6eac4ddaf9db70a2c58c Mon Sep 17 00:00:00 2001 From: secretpray Date: Sun, 8 Nov 2026 13:43:12 +0200 Subject: [PATCH 1/5] chore: remove maintainer tools from the repository and the package Remove the browser playground, the verification runbooks, the smoke command (octaspace-smoke and python -m octaspace) and the console scripts. The source distribution now contains only the package source, README, CHANGELOG, LICENSE and pyproject.toml. --- .flake8 | 2 - .gitignore | 2 - docs/runbooks/browser-playground.md | 212 ----------- docs/runbooks/console-verification.md | 267 -------------- docs/runbooks/publish.md | 338 ----------------- examples/console_mock.py | 60 --- examples/console_real.py | 48 --- examples/smoke.py | 4 - playground/__init__.py | 10 - playground/app.py | 130 ------- playground/catalog.py | 303 ---------------- playground/errors.py | 34 -- playground/fixtures/accounts/balance.json | 1 - playground/fixtures/accounts/show.json | 1 - playground/fixtures/apps/index.json | 1 - playground/fixtures/idle_jobs/logs.json | 1 - playground/fixtures/idle_jobs/show.json | 1 - playground/fixtures/network/info.json | 1 - playground/fixtures/nodes/index.json | 1 - playground/fixtures/nodes/show.json | 1 - playground/fixtures/services/mr/index.json | 1 - .../fixtures/services/session/info.json | 1 - .../fixtures/services/session/logs.json | 1 - playground/fixtures/services/vpn/index.json | 1 - playground/fixtures/sessions/index.json | 1 - playground/helpers.py | 248 ------------- playground/json_renderer.py | 68 ---- playground/main.py | 15 - playground/mock_data.py | 320 ---------------- playground/routes/__init__.py | 37 -- playground/routes/account.py | 51 --- playground/routes/apps.py | 40 -- playground/routes/dashboard.py | 83 ----- playground/routes/diagnostics.py | 188 ---------- playground/routes/idle_jobs.py | 75 ---- playground/routes/nodes.py | 78 ---- playground/routes/services.py | 60 --- playground/routes/sessions.py | 122 ------- playground/routes/settings.py | 44 --- playground/state.py | 203 ----------- playground/static/playground.css | 38 -- playground/static/playground.js | 102 ------ playground/templates/account.html | 75 ---- playground/templates/apps.html | 81 ----- playground/templates/base.html | 184 ---------- playground/templates/dashboard.html | 120 ------ playground/templates/diagnostics.html | 136 ------- playground/templates/idle_jobs.html | 86 ----- playground/templates/network.html | 73 ---- playground/templates/node_show.html | 27 -- playground/templates/nodes_index.html | 79 ---- playground/templates/services.html | 68 ---- playground/templates/sessions.html | 119 ------ playground/templates/shared/error_card.html | 29 -- playground/templates/shared/json_viewer.html | 7 - .../templates/shared/node_detail_panel.html | 32 -- .../templates/shared/resource_panel.html | 39 -- .../templates/shared/sessions_table.html | 53 --- playground/views.py | 278 -------------- pyproject.toml | 20 +- src/octaspace/__main__.py | 26 -- tests/playground/test_app.py | 167 --------- tests/test_main.py | 91 ----- uv.lock | 342 ------------------ 64 files changed, 9 insertions(+), 5318 deletions(-) delete mode 100644 .flake8 delete mode 100644 docs/runbooks/browser-playground.md delete mode 100644 docs/runbooks/console-verification.md delete mode 100644 docs/runbooks/publish.md delete mode 100644 examples/console_mock.py delete mode 100644 examples/console_real.py delete mode 100644 examples/smoke.py delete mode 100644 playground/__init__.py delete mode 100644 playground/app.py delete mode 100644 playground/catalog.py delete mode 100644 playground/errors.py delete mode 100644 playground/fixtures/accounts/balance.json delete mode 100644 playground/fixtures/accounts/show.json delete mode 100644 playground/fixtures/apps/index.json delete mode 100644 playground/fixtures/idle_jobs/logs.json delete mode 100644 playground/fixtures/idle_jobs/show.json delete mode 100644 playground/fixtures/network/info.json delete mode 100644 playground/fixtures/nodes/index.json delete mode 100644 playground/fixtures/nodes/show.json delete mode 100644 playground/fixtures/services/mr/index.json delete mode 100644 playground/fixtures/services/session/info.json delete mode 100644 playground/fixtures/services/session/logs.json delete mode 100644 playground/fixtures/services/vpn/index.json delete mode 100644 playground/fixtures/sessions/index.json delete mode 100644 playground/helpers.py delete mode 100644 playground/json_renderer.py delete mode 100644 playground/main.py delete mode 100644 playground/mock_data.py delete mode 100644 playground/routes/__init__.py delete mode 100644 playground/routes/account.py delete mode 100644 playground/routes/apps.py delete mode 100644 playground/routes/dashboard.py delete mode 100644 playground/routes/diagnostics.py delete mode 100644 playground/routes/idle_jobs.py delete mode 100644 playground/routes/nodes.py delete mode 100644 playground/routes/services.py delete mode 100644 playground/routes/sessions.py delete mode 100644 playground/routes/settings.py delete mode 100644 playground/state.py delete mode 100644 playground/static/playground.css delete mode 100644 playground/static/playground.js delete mode 100644 playground/templates/account.html delete mode 100644 playground/templates/apps.html delete mode 100644 playground/templates/base.html delete mode 100644 playground/templates/dashboard.html delete mode 100644 playground/templates/diagnostics.html delete mode 100644 playground/templates/idle_jobs.html delete mode 100644 playground/templates/network.html delete mode 100644 playground/templates/node_show.html delete mode 100644 playground/templates/nodes_index.html delete mode 100644 playground/templates/services.html delete mode 100644 playground/templates/sessions.html delete mode 100644 playground/templates/shared/error_card.html delete mode 100644 playground/templates/shared/json_viewer.html delete mode 100644 playground/templates/shared/node_detail_panel.html delete mode 100644 playground/templates/shared/resource_panel.html delete mode 100644 playground/templates/shared/sessions_table.html delete mode 100644 playground/views.py delete mode 100644 src/octaspace/__main__.py delete mode 100644 tests/playground/test_app.py delete mode 100644 tests/test_main.py diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 7da1f96..0000000 --- a/.flake8 +++ /dev/null @@ -1,2 +0,0 @@ -[flake8] -max-line-length = 100 diff --git a/.gitignore b/.gitignore index 7b04746..a3e11c2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,3 @@ __pycache__/ *.pyc .venv/ .python-version.local -.tmp/ -.claude/ diff --git a/docs/runbooks/browser-playground.md b/docs/runbooks/browser-playground.md deleted file mode 100644 index bdc8ff4..0000000 --- a/docs/runbooks/browser-playground.md +++ /dev/null @@ -1,212 +0,0 @@ -# Browser Playground Runbook - -This runbook is the browser-side QA companion to the console runbook. Use it only after lint, typecheck, tests, and console verification are already green. - -## Goal - -Verify that: - -- the browser playground starts locally -- the page set and navigation match the Ruby dummy app closely -- fixture-backed mock QA works without live API access -- failure scenarios are visible and readable in the browser -- live mode works for public and authenticated endpoints -- diagnostics and the footer request log remain useful during manual testing - -## Prerequisites - -- repository root opened at `python-sdk` -- development dependencies installed -- automated checks already green - -Recommended setup: - -```bash -# With uv: -uv sync --group dev - -# Or with pip + existing venv: -.venv/bin/python -m pip install -e ".[dev]" 2>/dev/null || \ - .venv/bin/python -m pip install -e . fastapi jinja2 python-multipart uvicorn pytest pytest-cov respx mypy ruff -``` - -Mandatory automated checks before browser QA: - -```bash -.venv/bin/ruff check src tests examples playground -.venv/bin/mypy src/octaspace playground -.venv/bin/pytest -``` - -## Start The Playground - -The playground lives in the repository root under `playground/`. It is a development tool and is **not** shipped in the PyPI wheel. - -Start it from the repository root: - -```bash -.venv/bin/uvicorn playground.main:app --reload -``` - -Open: - -```text -http://127.0.0.1:8000/playground/dashboard -``` - -## Token Safety - -- paste tokens only into the local sidebar field when needed -- do not commit tokens, screenshots, or copied payloads with live secrets -- prefer disposable resources for any live mutation check -- clear the token field or close the browser when verification is finished - -If you want the token ready in the shell first: - -```bash -read -s OCTA_API_KEY -export OCTA_API_KEY -``` - -## Expected Page Set - -The playground should expose the same core page set as the Ruby dummy app: - -- `Dashboard` -- `Network` -- `Apps` -- `Nodes` -- `Sessions` -- `Services` -- `Idle Jobs` -- `Diagnostics` - -Additional note: - -- `/playground/account` exists and must work, but it is intentionally not present in the sidebar because the Ruby dummy app also omits it from navigation - -## Scenario A: Fixtures Mode - -The default state is `Real API`. For deterministic offline QA, switch `Transport` to `Fixtures` in the sidebar first. - -Checklist: - -1. Open `Dashboard`. -2. Confirm the mock badge shows `Fixtures`. -3. Confirm `Network`, `Account`, and `Balance` panels render payloads. -4. Open `Network` and confirm the transport/config panels render. -5. Open `Apps`, `Nodes`, `Sessions`, `Services`, and `Idle Jobs`. -6. Confirm all of them render fixture-backed payloads without live API access. - -What this verifies: - -- template rendering -- sidebar settings wiring -- fixture-backed SDK calls -- footer request logging -- route coverage for the full dummy-style page set - -## Scenario B: Failure Scenarios - -Cycle the sidebar `Transport` through: - -- `401 Unauthorized` -- `403 Forbidden` -- `404 Not Found` -- `429 Rate Limited` -- `500 Server Error` -- `Slow (3s)` -- `Timeout` -- `Network Error` - -Recommended checks: - -1. In each scenario, open `Diagnostics`. -2. Run `octa_client.network.info` or another simple call. -3. Confirm the UI renders a readable error card instead of crashing. -4. Confirm the footer request log shows status or `ERR` with a duration. - -What this verifies: - -- typed error display -- timeout / connection handling -- readable failure UX for manual QA - -## Scenario C: Live Public Verification - -Switch `Transport` back to `Real API` and leave the API key empty. - -Checklist: - -1. Open `Dashboard`. -2. Confirm `Network` still renders. -3. Confirm authenticated panels show the locked state instead of crashing. -4. Open `Network` directly and confirm the response is live. - -What this verifies: - -- real transport startup -- public endpoint availability -- auth-gated UI behaviour - -## Scenario D: Live Authenticated Verification - -Enter a valid token in the sidebar. - -Recommended checklist: - -1. `Dashboard`: confirm `Account` and `Balance` now load. -2. `Account`: confirm profile and balance both render. -3. `Apps`: confirm app list renders. -4. `Nodes`: confirm list renders and detail panel opens. -5. `Sessions`: confirm current and recent tabs work. -6. `Services`: confirm MR, Render, and VPN tabs load. -7. `Idle Jobs`: test a known sample `node_id` / `job_id` if you have them. - -What this verifies: - -- authenticated transport flow -- multi-page parity with the Ruby dummy app -- repeated request behaviour with the real SDK transport - -## Scenario E: Diagnostics And Smoke - -Open `Diagnostics`. - -Checklist: - -1. Run one public call such as `octa_client.network.info`. -2. Run one authenticated read call such as `octa_client.accounts.profile`. -3. Run one payload-based call such as `octa_client.idle_jobs.find(node_id:, job_id:)`. -4. Run `Run SDK Smoke`. -5. Confirm the result panel and footer request log update after each run. - -Important: - -- diagnostics includes mutation-capable methods such as `services.mr.create`, `services.render.create`, `services.vpn.create`, and `services.session(uuid).stop` -- there is no separate mutation-guard toggle in the current UI -- in live mode, only run those calls against disposable targets and only when the team explicitly wants that check - -## Scenario F: Footer Request Log - -The request log is part of the shared page footer, matching the Ruby dummy layout style. It is not a standalone page. - -Checklist: - -1. Trigger several calls across different pages. -2. Confirm the footer log records method, path, status, duration, and time. -3. Use `Clear`. -4. Confirm the footer resets to `No requests yet`. - -## Completion Criteria - -Browser playground verification is complete only when: - -1. the expected page set is present -2. fixtures mode works -3. failure scenarios render cleanly -4. live public verification works -5. live authenticated verification works when a valid token is available -6. diagnostics and smoke run correctly -7. footer request logging remains readable and resettable -8. automated checks are still green afterward diff --git a/docs/runbooks/console-verification.md b/docs/runbooks/console-verification.md deleted file mode 100644 index cdab441..0000000 --- a/docs/runbooks/console-verification.md +++ /dev/null @@ -1,267 +0,0 @@ -# Console Verification Runbook - -This runbook is for a new developer joining the repository and needing a deterministic way to verify the SDK from the console before thinking about browser QA, release work, or any push. - -## Goal - -Verify that: - -- the package installs locally -- public and authenticated resource calls behave as expected -- transport diagnostics are visible -- mock mode and real API mode are both reproducible -- token handling is safe and explicit - -## Prerequisites - -- Python `3.10+` -- repository root opened at `python-sdk` -- a local virtualenv - -Setup: - -```bash -uv sync --group dev -``` - -If you prefer a plain `venv` + `pip` flow instead: - -```bash -python3 -m venv .venv -.venv/bin/python -m pip install -e . pytest pytest-cov respx mypy ruff hatchling fastapi jinja2 python-multipart uvicorn -``` - -Mandatory automated checks before manual console verification: - -```bash -.venv/bin/python -m ruff check src tests examples playground -.venv/bin/python -m mypy src playground -.venv/bin/python -m pytest -``` - -## Token Safety - -Do not paste tokens into committed files, screenshots, shell history, or example scripts. - -Recommended for `zsh` / `bash`: - -```bash -read -s OCTA_API_KEY -export OCTA_API_KEY -``` - -`read -s OCTA_API_KEY` waits for you to type the token without echoing it on screen. `export OCTA_API_KEY` makes that value visible to processes launched from the current shell, including `.venv/bin/python`. - -After finishing: - -```bash -unset OCTA_API_KEY OCTA_SESSION_UUID OCTA_ALLOW_STOP -``` - -Rules: - -- prefer environment variables over inline command arguments -- do not save tokens in `.env` unless team policy explicitly allows it -- do not capture console screenshots with live tokens visible -- do not run destructive session commands unless you intentionally target a disposable session - -## Scenario A: Mocked Console Verification - -This is the fastest deterministic check and must work without any network access to the OctaSpace API. - -Command: - -```bash -.venv/bin/python examples/console_mock.py -``` - -What it verifies: - -- SDK import path -- `MockTransport` -- resource calls for public and authenticated-style flows -- typed response access -- session proxy flow -- transport diagnostics in mock mode - -Expected output should include: - -- `transport_stats` -- `network` -- `profile` -- `balance` -- `nodes` -- `session_info` -- `session_stop` - -If this script fails: - -1. rerun `pytest` -2. inspect changed SDK modules -3. inspect any recent payload-shape changes - -## Scenario B: Public Real API Verification - -This verifies that the client can talk to the live API without requiring authentication for public endpoints. - -Command: - -```bash -.venv/bin/python -m octaspace -.venv/bin/octaspace-smoke -``` - -What it verifies: - -- `ClientConfig.from_env()` -- default client startup -- live `network.info()` -- JSON output formatting for smoke mode - -Expected result: - -- printed JSON payload from `/network` - -## Scenario C: Authenticated Real API Verification - -Set the token first: - -```bash -read -s OCTA_API_KEY -export OCTA_API_KEY -``` - -These two lines must be run in the shell before starting Python. They are not valid inside Python REPL. - -Run: - -```bash -.venv/bin/python examples/console_real.py -``` - -What it verifies: - -- authenticated client startup -- `accounts.profile()` -- `accounts.balance()` -- `nodes.list()` -- `sessions.list()` -- transport diagnostics in real mode - -Expected output should include: - -- `transport_stats` -- `network` -- `profile` -- `balance` -- `nodes_count` -- `sessions_count` - -If the token is missing, the script should still show public endpoint output and explicitly state that authenticated checks were skipped. - -## Scenario D: Targeted Session Inspection With Real Data - -If you have a known safe session UUID: - -```bash -export OCTA_SESSION_UUID="session-uuid-here" -.venv/bin/python examples/console_real.py -``` - -Expected additional output: - -- `session_info` -- `session_logs_preview` - -This should remain read-only. - -## Scenario E: Explicit Stop Command Against a Disposable Session - -Only use this for an intentionally disposable session. - -```bash -export OCTA_SESSION_UUID="session-uuid-here" -export OCTA_ALLOW_STOP=1 -.venv/bin/python examples/console_real.py -``` - -Expected additional output: - -- `session_stop` - -Safety rule: - -- never set `OCTA_ALLOW_STOP=1` against a production or user-critical session - -## Suggested REPL Checks - -For exploratory debugging: - -```bash -.venv/bin/python -``` - -```python -import os -from octaspace import OctaSpaceClient - -client = OctaSpaceClient(api_key=os.getenv("OCTA_API_KEY"), keep_alive=True, pool_size=10) -print(client.transport_stats()) -print(client.network.info().data) -print(client.accounts.profile().data) -client.close() -``` - -If you are already inside Python REPL and forgot to export the token in the shell, set it in-process instead: - -```python -import os, getpass -os.environ["OCTA_API_KEY"] = getpass.getpass("OCTA_API_KEY: ") -``` - -## Troubleshooting - -### `ModuleNotFoundError` - -The editable install was not done in the active virtualenv. - -Fix: - -```bash -.venv/bin/python -m pip install -e . -``` - -### `AuthenticationError` - -- token is missing -- token is invalid -- wrong shell session exported the variable -- `read -s` / `export` was entered inside Python REPL instead of the shell - -Check: - -```bash -python3 - <<'PY' -import os -print("present" if os.getenv("OCTA_API_KEY") else "missing") -PY -``` - -### `APIConnectionError` / `APITimeoutError` - -- temporary live API/network issue -- bad `base_url` / `base_urls` -- TLS verification problem in a custom environment - -Check the current config and transport stats before changing code. - -## Completion Criteria - -Manual console verification is complete only when: - -1. mock scenario passes -2. public real API scenario passes -3. authenticated real API scenario passes when a valid token is available -4. optional session inspection behaves as documented -5. browser playground runbook is ready to execute next -6. all automated checks remain green afterward diff --git a/docs/runbooks/publish.md b/docs/runbooks/publish.md deleted file mode 100644 index 2402b14..0000000 --- a/docs/runbooks/publish.md +++ /dev/null @@ -1,338 +0,0 @@ -# Publication Runbook - -This runbook covers every step from a clean repository to a published PyPI release. - -Repository: - -- GitHub: `https://github.com/octaspace/python-sdk` -- SSH remote: `git@github.com:octaspace/python-sdk.git` - -## Prerequisites - -- A working virtualenv at `.venv/` with dev dependencies installed (see `README.md#local-development-setup`) -- `twine` and `build` available in the venv: - ```bash - .venv/bin/pip install twine build - ``` -- PyPI account at https://pypi.org with 2FA enabled -- TestPyPI account at https://test.pypi.org with 2FA enabled -- API tokens created at both registries (instructions in Step 0 below) -- GitHub repository created at `octaspace/python-sdk` - -Initial repository push: - -```bash -git remote add origin git@github.com:octaspace/python-sdk.git -git branch -M main -git push -u origin main -``` - ---- - -## Step 0 — Create API Tokens - -You need tokens for both TestPyPI and PyPI. Do this once before the first upload. - -**TestPyPI token:** - -1. Log in at https://test.pypi.org -2. Go to Account Settings → API tokens → Add API token -3. Name: `python-sdk` (or any name you like) -4. Scope: `Entire account` (for the first upload; switch to project-scoped after the project exists) -5. Copy the token — it starts with `pypi-` and is shown only once - -**PyPI token:** - -1. Log in at https://pypi.org -2. Same flow: Account Settings → API tokens → Add API token -3. Name: `python-sdk` -4. Scope: `Entire account` (switch to project-scoped after first upload) -5. Copy the token - -**Store tokens in `~/.pypirc`** (create the file if it does not exist): - -```ini -[distutils] -index-servers = - pypi - testpypi - -[pypi] -username = __token__ -password = pypi- - -[testpypi] -username = __token__ -password = pypi- -``` - -Set restrictive permissions on the file: - -```bash -chmod 600 ~/.pypirc -``` - -After the first successful upload you can go back to PyPI/TestPyPI, delete the account-scoped tokens, and create new project-scoped tokens for `octaspace`. Update `~/.pypirc` with the new tokens. - ---- - -## Step 1 — Quality Gate - -All checks must be green before building anything. - -```bash -.venv/bin/ruff check src tests examples playground -.venv/bin/mypy src/octaspace playground -.venv/bin/pytest -``` - -Expected outcome: - -- ruff: `All checks passed!` -- mypy: `Success: no issues found in N source files` -- pytest: all tests passing, 0 failed - -If any check fails, fix it before proceeding. - ---- - -## Step 2 — Verify Version - -Open `src/octaspace/__about__.py` and confirm the version matches the intended release: - -```python -__version__ = "0.1.0" -``` - -Version scheme: `MAJOR.MINOR.PATCH` (SemVer). - -Bump rules: - -| Change type | Example | Bump | -| --- | --- | --- | -| Backwards-compatible fixes | bug fix, doc update | PATCH | -| New backwards-compatible features | new resource method | MINOR | -| Breaking API changes | rename method, remove param | MAJOR | - -Also update `CHANGELOG.md`: add a section `## [X.Y.Z] - YYYY-MM-DD` with a short summary of changes. - ---- - -## Step 3 — Build - -Delete any stale artifacts and build fresh: - -```bash -rm -rf dist/ -.venv/bin/python -m build -``` - -Expected output: - -``` -Successfully built octaspace-X.Y.Z.tar.gz and octaspace-X.Y.Z-py3-none-any.whl -``` - -Verify the wheel contains only the SDK package (no `playground/`, no `tests/`): - -```bash -python3 -m zipfile -l dist/octaspace-*.whl -``` - -Every file entry must start with `octaspace/` or `octaspace-*.dist-info/`. If `playground/` or `tests/` appear, check `[tool.hatch.build.targets.wheel]` in `pyproject.toml`. - -Check the `py.typed` marker is present (required for PEP 561 typed packages): - -```bash -python3 -m zipfile -l dist/octaspace-*.whl | grep py.typed -``` - ---- - -## Step 4 — Wheel Smoke Test - -Install the wheel in a clean throwaway environment and verify the public API works without any dev dependencies: - -```bash -python3 -m venv /tmp/octaspace-wheel-test -/tmp/octaspace-wheel-test/bin/python -m pip install dist/octaspace-*.whl - -/tmp/octaspace-wheel-test/bin/python - <<'EOF' -from octaspace import OctaSpaceClient, __version__ -print("version:", __version__) -with OctaSpaceClient() as client: - data = client.network.info().data - print("network.info:", type(data)) -print("smoke: ok") -EOF -``` - -Expected: - -``` -version: X.Y.Z -network.info: -smoke: ok -``` - -Also verify that `playground` is not importable from the wheel: - -```bash -/tmp/octaspace-wheel-test/bin/python -c "import playground" 2>&1 | grep -q "No module named" && echo "playground not in wheel: ok" -``` - -Cleanup: - -```bash -rm -rf /tmp/octaspace-wheel-test -``` - ---- - -## Step 5 — TestPyPI - -Upload to TestPyPI first. This catches metadata problems without affecting real users. - -```bash -.venv/bin/twine upload --repository testpypi dist/* -``` - -If `~/.pypirc` is configured (see Step 0), no credentials prompt appears. - -After upload, open the release page on TestPyPI and confirm: - -- version matches -- description (README) renders correctly -- classifiers and links are present - -Install from TestPyPI and smoke test: - -```bash -python3 -m venv /tmp/octaspace-testpypi -/tmp/octaspace-testpypi/bin/python -m pip install \ - --index-url https://test.pypi.org/simple/ \ - --extra-index-url https://pypi.org/simple/ \ - octaspace - -/tmp/octaspace-testpypi/bin/python -c " -from octaspace import OctaSpaceClient, __version__ -print('installed version:', __version__) -print('network.info:', OctaSpaceClient().network.info().data) -" - -rm -rf /tmp/octaspace-testpypi -``` - -`--extra-index-url https://pypi.org/simple/` is required because `httpx` (the only runtime dependency) is on real PyPI, not TestPyPI. - ---- - -## Step 6 — Push To GitHub And Tag - -Commit the release and push to GitHub: - -```bash -git add -p -git commit -m "Release 0.1.0" -git push origin main -``` - -Wait for CI to pass at `https://github.com/octaspace/python-sdk/actions`. - -Create and push the release tag: - -```bash -git tag -a v0.1.0 -m "Release 0.1.0" -git push origin v0.1.0 -``` - -Optionally create a GitHub release for the tag (UI or CLI): - -```bash -gh release create v0.1.0 --title "v0.1.0" --notes "Initial public release." -``` - ---- - -## Step 7 — Publish To PyPI - -Upload the same artifacts (already built in Step 3) directly to real PyPI: - -```bash -.venv/bin/twine upload dist/* -``` - -If `~/.pypirc` is configured, no credentials prompt appears. - -After upload, verify the release page at https://pypi.org/project/octaspace/. - -Final install check from real PyPI: - -```bash -python3 -m venv /tmp/octaspace-final -/tmp/octaspace-final/bin/python -m pip install octaspace -/tmp/octaspace-final/bin/python -c "from octaspace import OctaSpaceClient; print(OctaSpaceClient().network.info().data)" -rm -rf /tmp/octaspace-final -``` - ---- - -## Step 8 — Post-Release - -After a successful PyPI upload: - -1. **Bump the dev version** in `src/octaspace/__about__.py` to the next development marker, e.g.: - - ```python - __version__ = "0.2.0.dev0" - ``` - - This ensures any builds from `main` after the release are clearly distinguishable from the published version. - -2. **Push the post-release bump**: - - ```bash - git commit -am "Start 0.2.0 development" - git push origin main - ``` - -3. **Rotate tokens** (optional but recommended): go back to PyPI and TestPyPI, delete the account-scoped tokens created in Step 0, and create new project-scoped tokens for `octaspace`. Update `~/.pypirc`. - ---- - -## Troubleshooting - -| Symptom | Likely cause | Fix | -| --- | --- | --- | -| `403 Forbidden` on upload | Wrong token or scope | Re-check `~/.pypirc`; create a new token at pypi.org/manage/account/token/ | -| `File already exists` on upload | Re-uploading the same version | Bump the version — PyPI does not allow replacing existing files | -| `playground` appears in wheel | `packages` in `pyproject.toml` too broad | Confirm `packages = ["src/octaspace"]` under `[tool.hatch.build.targets.wheel]` | -| README renders as raw text on PyPI | Wrong or missing `readme = "README.md"` | Confirm the `[project]` table in `pyproject.toml` has `readme = "README.md"` | -| Dependency resolution fails | Pinned dep conflicts with user environment | Widen `httpx` version range in `dependencies` | - ---- - -## Quick Reference - -```bash -# Full quality gate -.venv/bin/ruff check src tests examples playground && \ -.venv/bin/mypy src/octaspace playground && \ -.venv/bin/pytest && \ -echo "gate: ok" - -# Build -rm -rf dist/ -.venv/bin/python -m build - -# TestPyPI -.venv/bin/twine upload --repository testpypi dist/* - -# Push + tag -git push origin main -git tag -a vX.Y.Z -m "Release X.Y.Z" -git push origin vX.Y.Z - -# Publish to real PyPI -.venv/bin/twine upload dist/* -``` diff --git a/examples/console_mock.py b/examples/console_mock.py deleted file mode 100644 index d19567b..0000000 --- a/examples/console_mock.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import annotations - -import json - -import httpx - -from octaspace import OctaSpaceClient -from octaspace._transport.mock_transport import MockTransport -from octaspace.response import APIResponse - - -def make_response(status_code: int, payload: object) -> APIResponse: - return APIResponse.from_httpx(httpx.Response(status_code, json=payload)) - - -def dump(label: str, payload: object) -> None: - print(f"{label}: {json.dumps(payload, ensure_ascii=False)}") - - -def main() -> None: - transport = MockTransport() - transport.add_response("GET", "/network", make_response(200, {"power": 1234, "nodes": 2})) - transport.add_response( - "GET", - "/accounts", - make_response(200, {"account_uuid": "acc-1", "email": "user@example.com"}), - ) - transport.add_response( - "GET", - "/accounts/balance", - make_response(200, {"balance": 123.45, "currency": "OCTA"}), - ) - transport.add_response( - "GET", - "/nodes", - make_response(200, [{"id": 1, "state": "online"}, {"id": 2, "state": "offline"}]), - ) - transport.add_response( - "GET", - "/services/sess-abc-123/info", - make_response(200, {"uuid": "sess-abc-123", "status": "running"}), - ) - transport.add_response( - "POST", - "/services/sess-abc-123/stop", - make_response(200, {"uuid": "sess-abc-123", "status": "stopped"}), - ) - - with OctaSpaceClient(transport=transport) as client: - dump("transport_stats", client.transport_stats()) - dump("network", client.network.info().data) - dump("profile", client.accounts.profile().data) - dump("balance", client.accounts.balance().data) - dump("nodes", client.nodes.list().data) - dump("session_info", client.services.session("sess-abc-123").info().data) - dump("session_stop", client.services.session("sess-abc-123").stop(score=5).data) - - -if __name__ == "__main__": - main() diff --git a/examples/console_real.py b/examples/console_real.py deleted file mode 100644 index 2fd7f39..0000000 --- a/examples/console_real.py +++ /dev/null @@ -1,48 +0,0 @@ -from __future__ import annotations - -import json -import os - -from octaspace import OctaSpaceClient - - -def dump(label: str, payload: object) -> None: - print(f"{label}: {json.dumps(payload, ensure_ascii=False)}") - - -def main() -> None: - api_key = os.getenv("OCTA_API_KEY") - session_uuid = os.getenv("OCTA_SESSION_UUID") - allow_stop = os.getenv("OCTA_ALLOW_STOP") == "1" - - with OctaSpaceClient(api_key=api_key, keep_alive=True, pool_size=10) as client: - dump("transport_stats", client.transport_stats()) - dump("network", client.network.info().data) - - if not api_key: - print("OCTA_API_KEY is not set; authenticated checks skipped.") - return - - dump("profile", client.accounts.profile().data) - dump("balance", client.accounts.balance().data) - dump("nodes_count", len(client.nodes.list().data)) - dump("sessions_count", len(client.sessions.list().data)) - - if not session_uuid: - print("OCTA_SESSION_UUID is not set; session inspection skipped.") - return - - session = client.services.session(session_uuid) - dump("session_info", session.info().data) - logs = session.logs().data - preview = logs[:5] if isinstance(logs, list) else logs - dump("session_logs_preview", preview) - - if allow_stop: - dump("session_stop", session.stop(score=5).data) - else: - print("OCTA_ALLOW_STOP is not set; destructive stop call skipped.") - - -if __name__ == "__main__": - main() diff --git a/examples/smoke.py b/examples/smoke.py deleted file mode 100644 index ecf9b7a..0000000 --- a/examples/smoke.py +++ /dev/null @@ -1,4 +0,0 @@ -from octaspace.__main__ import smoke - -if __name__ == "__main__": - smoke() diff --git a/playground/__init__.py b/playground/__init__.py deleted file mode 100644 index 0195530..0000000 --- a/playground/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -"""OctaSpace SDK Browser Playground (development only — not shipped to PyPI). - -Quick start:: - - uv sync --group playground - uvicorn playground.main:app --reload -""" -from .app import create_app - -__all__ = ["create_app"] diff --git a/playground/app.py b/playground/app.py deleted file mode 100644 index 1ad749e..0000000 --- a/playground/app.py +++ /dev/null @@ -1,130 +0,0 @@ -"""OctaSpace SDK Browser Playground — application factory. - -The playground is a development-only FastAPI application shipped alongside -the SDK source but NOT included in the PyPI wheel. It lets developers -verify SDK behaviour against mock scenarios or a live API key directly in -the browser, mirroring the role of the Rails Dummy app in the Ruby gem. - -Running -------- -After installing the playground dependency group:: - - uv sync --group playground - uvicorn playground.main:app --reload - -Then open http://127.0.0.1:8000/playground/dashboard -""" -from __future__ import annotations - -from pathlib import Path -from secrets import token_urlsafe - -from fastapi import FastAPI -from fastapi.staticfiles import StaticFiles -from fastapi.templating import Jinja2Templates - -from .errors import error_payload -from .helpers import ( - base_context, # noqa: F401 — kept for external use / tests - build_client, # noqa: F401 - capture_api_call, # noqa: F401 - config_snapshot, - diagnostics_preset_path, - query_url, -) -from .json_renderer import json_html -from .state import PlaygroundState -from .views import ( - app_ports_count, - format_bytes, - format_datetime, - format_duration, - format_short_time, - node_gpu_count, - node_gpu_model, - node_location, - node_ram_label, - node_summary_rows, - playground_present, - response_data, - response_error, - response_request_id, - response_status, - session_service_badge_class, - session_service_label, - session_status_badge_class, - session_status_label, - show_service_column, - transport_description, - transport_label, - truncate_value, -) - -_ROOT = Path(__file__).resolve().parent - - -def create_app(state: PlaygroundState | None = None) -> FastAPI: - """Create and return a configured FastAPI playground application. - - Pass a *PlaygroundState* instance to inject pre-configured state - (useful in tests). If omitted, a fresh default state is used. - """ - app = FastAPI(title="OctaSpace SDK Playground", docs_url=None, redoc_url=None) - app.state.playground_state = state or PlaygroundState() - app.state.csrf_token = token_urlsafe(32) - - # --- templates ---------------------------------------------------------- - templates = Jinja2Templates(directory=str(_ROOT / "templates")) - templates.env.globals.update( - # layout helpers - query_url=query_url, - diagnostics_preset_path=diagnostics_preset_path, - config_snapshot=config_snapshot, - present=playground_present, - # formatting - format_duration=format_duration, - format_bytes=format_bytes, - format_datetime=format_datetime, - format_short_time=format_short_time, - truncate_value=truncate_value, - # response inspection - response_data=response_data, - response_error=response_error, - response_status=response_status, - response_request_id=response_request_id, - # node - node_location=node_location, - node_gpu_model=node_gpu_model, - node_gpu_count=node_gpu_count, - node_ram_label=node_ram_label, - node_summary_rows=node_summary_rows, - # app - app_ports_count=app_ports_count, - # session - session_service_label=session_service_label, - session_service_badge_class=session_service_badge_class, - session_status_label=session_status_label, - session_status_badge_class=session_status_badge_class, - show_service_column=show_service_column, - # transport - transport_label=transport_label, - transport_description=transport_description, - # error display - error_payload=error_payload, - ) - templates.env.filters["json_html"] = json_html - # Store templates in app state so route handlers can access them - app.state.templates = templates - - # --- static files ------------------------------------------------------- - app.mount( - "/playground/static", - StaticFiles(directory=str(_ROOT / "static")), - name="playground_static", - ) - - # --- routes ------------------------------------------------------------- - from .routes import router - app.include_router(router) - - return app diff --git a/playground/catalog.py b/playground/catalog.py deleted file mode 100644 index 9b02773..0000000 --- a/playground/catalog.py +++ /dev/null @@ -1,303 +0,0 @@ -from __future__ import annotations - -import json -from dataclasses import dataclass -from typing import Any - -from octaspace import OctaSpaceClient -from octaspace.response import APIResponse - -from .state import PlaygroundSettings - - -@dataclass(frozen=True, slots=True) -class PlaygroundCall: - id: str - label: str - description: str - method: str - path: str - requires_auth: bool = False - mutation: bool = False - takes_payload: bool = False - - -CALLS: tuple[PlaygroundCall, ...] = ( - PlaygroundCall( - id="network.info", - label="octa_client.network.info", - description="GET /network - public, no auth required", - method="GET", - path="/network", - ), - PlaygroundCall( - id="accounts.profile", - label="octa_client.accounts.profile", - description="GET /accounts - requires API key", - method="GET", - path="/accounts", - requires_auth=True, - ), - PlaygroundCall( - id="accounts.balance", - label="octa_client.accounts.balance", - description="GET /accounts/balance - requires API key", - method="GET", - path="/accounts/balance", - requires_auth=True, - ), - PlaygroundCall( - id="apps.list", - label="octa_client.apps.list", - description="GET /apps - requires API key", - method="GET", - path="/apps", - requires_auth=True, - ), - PlaygroundCall( - id="idle_jobs.find", - label="octa_client.idle_jobs.find(node_id:, job_id:)", - description="GET /idle_jobs/:node_id/:job_id - requires API key", - method="GET", - path="/idle_jobs/:node_id/:job_id", - requires_auth=True, - takes_payload=True, - ), - PlaygroundCall( - id="idle_jobs.logs", - label="octa_client.idle_jobs.logs(node_id:, job_id:)", - description="GET /idle_jobs/:node_id/:job_id/logs - requires API key", - method="GET", - path="/idle_jobs/:node_id/:job_id/logs", - requires_auth=True, - takes_payload=True, - ), - PlaygroundCall( - id="nodes.list", - label="octa_client.nodes.list", - description="GET /nodes - requires API key", - method="GET", - path="/nodes", - requires_auth=True, - ), - PlaygroundCall( - id="sessions.list", - label="octa_client.sessions.list", - description="GET /sessions - requires API key", - method="GET", - path="/sessions", - requires_auth=True, - ), - PlaygroundCall( - id="sessions.list.recent", - label="octa_client.sessions.list(recent: true)", - description="GET /sessions?recent=true - requires API key", - method="GET", - path="/sessions?recent=true", - requires_auth=True, - ), - PlaygroundCall( - id="services.mr.list", - label="octa_client.services.mr.list", - description="GET /services/mr - requires API key", - method="GET", - path="/services/mr", - requires_auth=True, - ), - PlaygroundCall( - id="services.render.list", - label="octa_client.services.render.list", - description="GET /services/render - requires API key", - method="GET", - path="/services/render", - requires_auth=True, - ), - PlaygroundCall( - id="services.vpn.list", - label="octa_client.services.vpn.list", - description="GET /services/vpn - requires API key", - method="GET", - path="/services/vpn", - requires_auth=True, - ), - PlaygroundCall( - id="services.mr.create", - label="octa_client.services.mr.create", - description="POST /services/mr - requires API key", - method="POST", - path="/services/mr", - requires_auth=True, - mutation=True, - takes_payload=True, - ), - PlaygroundCall( - id="services.render.create", - label="octa_client.services.render.create", - description="POST /services/render - requires API key", - method="POST", - path="/services/render", - requires_auth=True, - mutation=True, - takes_payload=True, - ), - PlaygroundCall( - id="services.vpn.create", - label="octa_client.services.vpn.create", - description="POST /services/vpn - requires API key", - method="POST", - path="/services/vpn", - requires_auth=True, - mutation=True, - takes_payload=True, - ), - PlaygroundCall( - id="services.session.stop", - label="octa_client.services.session(uuid).stop", - description="POST /services/:uuid/stop - requires API key", - method="POST", - path="/services/:uuid/stop", - requires_auth=True, - mutation=True, - takes_payload=True, - ), -) - -SMOKE_CALL_IDS: tuple[str, ...] = ( - "network.info", - "accounts.profile", - "accounts.balance", - "apps.list", - "nodes.list", - "services.mr.list", - "services.render.list", - "services.vpn.list", - "sessions.list", - "sessions.list.recent", -) - -CALLS_BY_ID = {call.id: call for call in CALLS} - - -def call_for(call_id: str) -> PlaygroundCall: - return CALLS_BY_ID.get(call_id, CALLS[0]) - - -def smoke_calls() -> list[PlaygroundCall]: - return [call_for(call_id) for call_id in SMOKE_CALL_IDS] - - -def default_payload(call: PlaygroundCall, settings: PlaygroundSettings) -> str: - payload = payload_for(call.id, settings) - if payload is None: - return "" - return json.dumps(payload, indent=2, ensure_ascii=False) - - -def payload_for(call_id: str, settings: PlaygroundSettings) -> dict[str, Any] | None: - node_id = _json_id(settings.sample_node_id) - job_id = _json_id(settings.sample_job_id) - uuid = settings.sample_session_uuid - - if call_id in {"idle_jobs.find", "idle_jobs.logs"}: - return {"node_id": node_id, "job_id": job_id} - if call_id == "services.mr.create": - return { - "node_id": node_id, - "disk_size": 10, - "image": "ubuntu:24.04", - "app": "app-1", - } - if call_id == "services.render.create": - return {"node_id": node_id, "disk_size": 100} - if call_id == "services.vpn.create": - return {"node_id": node_id, "subkind": "wg"} - if call_id == "services.session.stop": - return {"uuid": uuid, "score": 5} - return None - - -def resolved_path(call: PlaygroundCall, payload: dict[str, Any] | None = None) -> str: - data = payload or {} - if call.id.startswith("idle_jobs."): - node_id = data.get("node_id", ":node_id") - job_id = data.get("job_id", ":job_id") - suffix = "/logs" if call.id.endswith(".logs") else "" - return f"/idle_jobs/{node_id}/{job_id}{suffix}" - if call.id == "services.session.stop": - uuid = data.get("uuid", ":uuid") - return f"/services/{uuid}/stop" - return call.path - - -def execute_call( - client: OctaSpaceClient, - call: PlaygroundCall, - payload: dict[str, Any] | None = None, -) -> APIResponse: - data = payload or {} - - if call.id == "network.info": - return client.network.info() - if call.id == "accounts.profile": - return client.accounts.profile() - if call.id == "accounts.balance": - return client.accounts.balance() - if call.id == "apps.list": - return client.apps.list() - if call.id == "idle_jobs.find": - return client.idle_jobs.find(node_id=data["node_id"], job_id=data["job_id"]) - if call.id == "idle_jobs.logs": - return client.idle_jobs.logs(node_id=data["node_id"], job_id=data["job_id"]) - if call.id == "nodes.list": - return client.nodes.list() - if call.id == "sessions.list": - return client.sessions.list() - if call.id == "sessions.list.recent": - return client.sessions.list(recent=True) - if call.id == "services.mr.list": - return client.services.mr.list() - if call.id == "services.render.list": - return client.services.render.list() - if call.id == "services.vpn.list": - return client.services.vpn.list() - if call.id == "services.mr.create": - return client.services.mr.create( - node_id=int(data["node_id"]), - disk_size=int(data["disk_size"]), - image=str(data["image"]), - app=str(data.get("app", "")), - envs=_string_dict(data.get("envs")), - ports=_int_list(data.get("ports")), - http_ports=_int_list(data.get("http_ports")), - start_command=str(data.get("start_command", "")), - entrypoint=str(data.get("entrypoint", "")), - ) - if call.id == "services.render.create": - return client.services.render.create(**data) - if call.id == "services.vpn.create": - return client.services.vpn.create(**data) - if call.id == "services.session.stop": - uuid = str(data["uuid"]) - return client.services.session(uuid).stop( - **{key: value for key, value in data.items() if key != "uuid"} - ) - raise ValueError(f"Unsupported playground call: {call.id}") - - -def _json_id(value: str) -> int | str: - try: - return int(value) - except ValueError: - return value - - -def _int_list(value: Any) -> list[int]: - if not isinstance(value, list): - return [] - return [int(item) for item in value] - - -def _string_dict(value: Any) -> dict[str, str]: - if not isinstance(value, dict): - return {} - return {str(key): str(item) for key, item in value.items()} diff --git a/playground/errors.py b/playground/errors.py deleted file mode 100644 index 8b751d3..0000000 --- a/playground/errors.py +++ /dev/null @@ -1,34 +0,0 @@ -from __future__ import annotations - -from typing import Any - -from octaspace.errors import APIError, OctaSpaceError - - -class PayloadError(Exception): - """Raised when request payload JSON is invalid or missing required keys.""" - - -def status_for_error(error: OctaSpaceError) -> str: - if isinstance(error, APIError): - return str(error.status_code) - return "ERR" - - -def error_payload(error: Exception) -> dict[str, Any]: - if isinstance(error, PayloadError): - return {"class": type(error).__name__, "message": str(error)} - if isinstance(error, APIError): - payload: dict[str, Any] = { - "class": type(error).__name__, - "message": str(error), - "status": error.status_code, - "request_id": error.request_id, - } - if error.response.data is not None: - payload["response"] = error.response.data - retry_after = getattr(error, "retry_after", None) - if retry_after is not None: - payload["retry_after"] = retry_after - return payload - return {"class": type(error).__name__, "message": str(error)} diff --git a/playground/fixtures/accounts/balance.json b/playground/fixtures/accounts/balance.json deleted file mode 100644 index 479e970..0000000 --- a/playground/fixtures/accounts/balance.json +++ /dev/null @@ -1 +0,0 @@ -{"balance":123.45,"currency":"OCTA"} diff --git a/playground/fixtures/accounts/show.json b/playground/fixtures/accounts/show.json deleted file mode 100644 index 2b3ef09..0000000 --- a/playground/fixtures/accounts/show.json +++ /dev/null @@ -1 +0,0 @@ -{"account_uuid":"acc-123-abc","email":"user@example.com","avatar":"https://example.com/avatar.png","balance":12.5} diff --git a/playground/fixtures/apps/index.json b/playground/fixtures/apps/index.json deleted file mode 100644 index 44c982a..0000000 --- a/playground/fixtures/apps/index.json +++ /dev/null @@ -1 +0,0 @@ -[{"uuid":"app-1","name":"Example App"}] diff --git a/playground/fixtures/idle_jobs/logs.json b/playground/fixtures/idle_jobs/logs.json deleted file mode 100644 index 6402098..0000000 --- a/playground/fixtures/idle_jobs/logs.json +++ /dev/null @@ -1 +0,0 @@ -["job line 1","job line 2"] diff --git a/playground/fixtures/idle_jobs/show.json b/playground/fixtures/idle_jobs/show.json deleted file mode 100644 index 4fc64ea..0000000 --- a/playground/fixtures/idle_jobs/show.json +++ /dev/null @@ -1 +0,0 @@ -{"node_id":69,"job_id":42,"status":"running"} diff --git a/playground/fixtures/network/info.json b/playground/fixtures/network/info.json deleted file mode 100644 index ac9b5d8..0000000 --- a/playground/fixtures/network/info.json +++ /dev/null @@ -1 +0,0 @@ -{"power":1234,"nodes":2,"market":{"price":1.23}} diff --git a/playground/fixtures/nodes/index.json b/playground/fixtures/nodes/index.json deleted file mode 100644 index d185880..0000000 --- a/playground/fixtures/nodes/index.json +++ /dev/null @@ -1 +0,0 @@ -[{"id":1,"ip":"1.2.3.4","state":"online","location":{"country":"UA"},"prices":{"gpu_hour":0.5},"system":{"gpu":"A100"}},{"id":2,"ip":"5.6.7.8","state":"offline","location":{"country":"PL"},"prices":{"gpu_hour":0.3},"system":{"gpu":"RTX 4090"}}] diff --git a/playground/fixtures/nodes/show.json b/playground/fixtures/nodes/show.json deleted file mode 100644 index 2e76953..0000000 --- a/playground/fixtures/nodes/show.json +++ /dev/null @@ -1 +0,0 @@ -{"id":1,"ip":"1.2.3.4","state":"online","location":{"country":"UA"},"prices":{"gpu_hour":0.5},"system":{"gpu":"A100"}} diff --git a/playground/fixtures/services/mr/index.json b/playground/fixtures/services/mr/index.json deleted file mode 100644 index b46a68e..0000000 --- a/playground/fixtures/services/mr/index.json +++ /dev/null @@ -1 +0,0 @@ -[{"uuid":"mr-sess-111","service":"mr"}] diff --git a/playground/fixtures/services/session/info.json b/playground/fixtures/services/session/info.json deleted file mode 100644 index 1a644d4..0000000 --- a/playground/fixtures/services/session/info.json +++ /dev/null @@ -1 +0,0 @@ -{"uuid":"sess-abc-123","status":"running","service":"mr"} diff --git a/playground/fixtures/services/session/logs.json b/playground/fixtures/services/session/logs.json deleted file mode 100644 index 2dc2c59..0000000 --- a/playground/fixtures/services/session/logs.json +++ /dev/null @@ -1 +0,0 @@ -["line1","line2"] diff --git a/playground/fixtures/services/vpn/index.json b/playground/fixtures/services/vpn/index.json deleted file mode 100644 index a383f90..0000000 --- a/playground/fixtures/services/vpn/index.json +++ /dev/null @@ -1 +0,0 @@ -[{"uuid":"vpn-sess-222","service":"vpn"}] diff --git a/playground/fixtures/sessions/index.json b/playground/fixtures/sessions/index.json deleted file mode 100644 index 677eef3..0000000 --- a/playground/fixtures/sessions/index.json +++ /dev/null @@ -1 +0,0 @@ -[{"uuid":"sess-1","service":"mr"}] diff --git a/playground/helpers.py b/playground/helpers.py deleted file mode 100644 index 1fdaf38..0000000 --- a/playground/helpers.py +++ /dev/null @@ -1,248 +0,0 @@ -from __future__ import annotations - -import json -from collections.abc import Callable -from dataclasses import dataclass -from secrets import compare_digest -from time import monotonic -from typing import Any, cast -from urllib.parse import urlencode - -from fastapi import HTTPException, Request -from fastapi.templating import Jinja2Templates - -from octaspace import OctaSpaceClient -from octaspace.config import ClientConfig -from octaspace.errors import OctaSpaceError -from octaspace.response import APIResponse - -from .catalog import payload_for -from .mock_data import build_mock_transport -from .state import MOCK_SCENARIOS, PlaygroundSettings, PlaygroundState - -# --------------------------------------------------------------------------- -# Nav items -# --------------------------------------------------------------------------- - -@dataclass(frozen=True, slots=True) -class NavItem: - id: str - name: str - path: str - badge: str | None = None - - -NAV_ITEMS: tuple[NavItem, ...] = ( - NavItem(id="dashboard", name="Dashboard", path="/playground/dashboard"), - NavItem(id="network", name="Network", path="/playground/network", badge="public"), - NavItem(id="apps", name="Apps", path="/playground/apps"), - NavItem(id="nodes", name="Nodes", path="/playground/nodes"), - NavItem(id="sessions", name="Sessions", path="/playground/sessions"), - NavItem(id="services", name="Services", path="/playground/services"), - NavItem(id="idle_jobs", name="Idle Jobs", path="/playground/idle-jobs"), - NavItem(id="diagnostics", name="Diagnostics", path="/playground/diagnostics", badge="mock"), -) - - -# --------------------------------------------------------------------------- -# FastAPI app-state accessors -# --------------------------------------------------------------------------- - -def get_state(request: Request) -> PlaygroundState: - return cast(PlaygroundState, request.app.state.playground_state) - - -def get_templates(request: Request) -> Jinja2Templates: - return cast(Jinja2Templates, request.app.state.templates) - - -def get_csrf_token(request: Request) -> str: - return cast(str, request.app.state.csrf_token) - - -def validate_csrf(request: Request, submitted_token: str | None = None) -> None: - candidate = ( - (submitted_token or "").strip() - or request.headers.get("x-csrf-token", "").strip() - or request.query_params.get("csrf_token", "").strip() - ) - expected = get_csrf_token(request) - if not candidate or not compare_digest(candidate, expected): - raise HTTPException(status_code=403, detail="Invalid CSRF token") - - -# --------------------------------------------------------------------------- -# Client factory -# --------------------------------------------------------------------------- - -def build_client(settings: PlaygroundSettings) -> OctaSpaceClient: - config = ClientConfig( - api_key=settings.api_key or None, - base_url=settings.base_url, - keep_alive=False, - ) - if settings.is_mock_active: - return OctaSpaceClient(config=config, transport=build_mock_transport(settings)) - return OctaSpaceClient(config=config) - - -def playground_config(settings: PlaygroundSettings) -> ClientConfig: - return ClientConfig(api_key=settings.api_key or None, base_url=settings.base_url) - - -def config_snapshot(settings: PlaygroundSettings) -> dict[str, Any]: - config = playground_config(settings) - return { - "urls": config.urls, - "mock_scenario": settings.mock_scenario, - "open_timeout": config.open_timeout, - "read_timeout": config.read_timeout, - "write_timeout": config.write_timeout, - "keep_alive": config.keep_alive, - "pool_size": config.pool_size, - "pool_timeout": config.pool_timeout, - "idle_timeout": config.idle_timeout, - "max_retries": config.max_retries, - "retry_interval": config.retry_interval, - "backoff_factor": config.backoff_factor, - } - - -# --------------------------------------------------------------------------- -# Request capture -# --------------------------------------------------------------------------- - -def capture_api_call( - state: PlaygroundState, - *, - label: str, - method: str, - path: str, - fn: Callable[[], APIResponse], -) -> APIResponse | OctaSpaceError: - started_at = monotonic() - try: - response = fn() - except OctaSpaceError as exc: - state.append_request( - label=label, - method=method, - path=path, - status=_status_for_error(exc), - duration_ms=elapsed_ms(started_at), - error=str(exc), - ) - return exc - state.append_request( - label=label, - method=method, - path=path, - status=str(response.status_code), - duration_ms=elapsed_ms(started_at), - ) - return response - - -def elapsed_ms(started_at: float) -> int: - return round((monotonic() - started_at) * 1000) - - -def _status_for_error(error: OctaSpaceError) -> str: - from octaspace.errors import APIError - if isinstance(error, APIError): - return str(error.status_code) - return "ERR" - - -# --------------------------------------------------------------------------- -# Template context -# --------------------------------------------------------------------------- - -def base_context( - request: Request, - *, - page: str, - title: str, - settings: PlaygroundSettings, - transport_stats: dict[str, Any], - page_url: str | None = None, -) -> dict[str, Any]: - resolved_page_url = page_url or ( - request.url.path + (f"?{request.url.query}" if request.url.query else "") - ) - return { - "request": request, - "page": page, - "title": title, - "page_url": resolved_page_url, - "csrf_token": get_csrf_token(request), - "nav_items": NAV_ITEMS, - "settings": settings, - "config": playground_config(settings), - "mock_scenarios": MOCK_SCENARIOS, - "request_log": get_state(request).request_log(), - "transport_stats": transport_stats, - "playground_api_key_present": settings.api_key_present, - "playground_auth_available": settings.auth_available, - "playground_mock_active": settings.is_mock_active, - "playground_mock_scenario": settings.mock_scenario, - "playground_masked_api_key": settings.masked_api_key, - "playground_transport_label": settings.transport_label, - "playground_transport_description": settings.transport_description, - "playground_timeout_label": _timeout_label(settings), - } - - -def _timeout_label(settings: PlaygroundSettings) -> str: - config = playground_config(settings) - return f"{config.open_timeout}s / {config.read_timeout}s" - - -# --------------------------------------------------------------------------- -# URL / routing helpers -# --------------------------------------------------------------------------- - -def safe_return_path(path: str | None) -> str: - candidate = (path or "").strip() - if candidate.startswith("/") and not candidate.startswith("//"): - return candidate - return "/playground/dashboard" - - -def query_url(request: Request, param: str, value: str) -> str: - query = dict(request.query_params) - query[param] = value - encoded = urlencode(query) - return f"{request.url.path}?{encoded}" if encoded else request.url.path - - -def diagnostics_preset_path( - request: Request, - call_id: str, - settings: PlaygroundSettings, - overrides: dict[str, Any] | None = None, -) -> str: - payload = payload_for(call_id, settings) or {} - if overrides: - payload.update({k: v for k, v in overrides.items() if v not in (None, "")}) - query = urlencode({ - "call": call_id, - "payload_json": json.dumps(payload, indent=2, ensure_ascii=False), - }) - return f"/playground/diagnostics?{query}" - - -# --------------------------------------------------------------------------- -# Payload parsing -# --------------------------------------------------------------------------- - -def parse_payload_json(payload_json: str) -> dict[str, Any]: - if not payload_json.strip(): - return {} - try: - parsed = json.loads(payload_json) - except json.JSONDecodeError as exc: - raise ValueError(f"Invalid JSON: {exc.msg}") from exc - if not isinstance(parsed, dict): - raise ValueError("Payload JSON must be an object") - return parsed diff --git a/playground/json_renderer.py b/playground/json_renderer.py deleted file mode 100644 index 8854da6..0000000 --- a/playground/json_renderer.py +++ /dev/null @@ -1,68 +0,0 @@ -from __future__ import annotations - -import json -from typing import Any - -from markupsafe import Markup, escape - -from octaspace.response import APIResponse - -from .errors import error_payload - - -def serializable(value: Any) -> Any: - if isinstance(value, APIResponse): - return serializable(value.data) - if isinstance(value, Exception): - return error_payload(value) - if isinstance(value, dict): - return {str(k): serializable(v) for k, v in value.items()} - if isinstance(value, (list, tuple)): - return [serializable(item) for item in value] - if isinstance(value, (str, int, float, bool)) or value is None: - return value - if hasattr(value, "to_dict"): - return serializable(value.to_dict()) - return str(value) - - -def json_html(value: Any) -> Markup: - return _render_node(serializable(value), 0) - - -def _render_node(node: Any, level: int) -> Markup: - if isinstance(node, dict): - return _render_hash(node, level) - if isinstance(node, list): - return _render_array(node, level) - if isinstance(node, str): - return Markup(f'{escape(json.dumps(node))}') - if isinstance(node, bool): - return Markup(f'{escape(str(node).lower())}') - if isinstance(node, (int, float)): - return Markup(f'{escape(str(node))}') - if node is None: - return Markup('null') - return Markup(f'{escape(json.dumps(str(node)))}') - - -def _render_hash(node: dict[str, Any], level: int) -> Markup: - if not node: - return Markup("{}") - lines = [ - f'{_indent(level + 1)}{escape(json.dumps(str(k)))}: ' - f"{_render_node(v, level + 1)}" - for k, v in node.items() - ] - return Markup("{\n" + ",\n".join(lines) + f"\n{_indent(level)}}}") - - -def _render_array(node: list[Any], level: int) -> Markup: - if not node: - return Markup("[]") - lines = [f"{_indent(level + 1)}{_render_node(item, level + 1)}" for item in node] - return Markup("[\n" + ",\n".join(lines) + f"\n{_indent(level)}]") - - -def _indent(level: int) -> str: - return " " * level diff --git a/playground/main.py b/playground/main.py deleted file mode 100644 index 8da8709..0000000 --- a/playground/main.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Uvicorn entry point for the OctaSpace SDK Browser Playground. - -The playground is a dev-only tool (not shipped to PyPI). -Run from the project root after installing playground dependencies: - - uv sync --group playground - uvicorn playground.main:app --reload - -Then open: http://127.0.0.1:8000/playground/dashboard -""" -from __future__ import annotations - -from .app import create_app - -app = create_app() diff --git a/playground/mock_data.py b/playground/mock_data.py deleted file mode 100644 index 0838654..0000000 --- a/playground/mock_data.py +++ /dev/null @@ -1,320 +0,0 @@ -from __future__ import annotations - -import copy -import json -import time -from pathlib import Path -from typing import Any - -import httpx - -from octaspace.errors import APIConnectionError, APITimeoutError, raise_for_response -from octaspace.response import APIResponse - -from .state import PlaygroundSettings - -# Keep playground fixture data local to the playground so the dev app does not -# depend on the test suite's directory layout. -FIXTURES_DIR = Path(__file__).resolve().parent / "fixtures" -ERROR_SCENARIOS = {"401", "403", "404", "429", "500"} - - -def _load_fixture(name: str) -> Any: - return json.loads((FIXTURES_DIR / name).read_text(encoding="utf-8")) - - -def _coerce_identifier(value: str) -> int | str: - try: - return int(value) - except ValueError: - return value - - -def _normalize_param_value(value: Any) -> str: - if isinstance(value, bool): - return "true" if value else "false" - return str(value) - - -def _normalize_params(params: dict[str, Any] | None) -> tuple[tuple[str, str], ...]: - if not params: - return () - return tuple(sorted((str(key), _normalize_param_value(value)) for key, value in params.items())) - - -def _json_response( - status_code: int, - payload: Any, - *, - headers: dict[str, str] | None = None, -) -> APIResponse: - response_headers = { - "Content-Type": "application/json", - "X-Request-Id": f"mock-{status_code}", - } - if headers: - response_headers.update(headers) - return APIResponse.from_httpx( - httpx.Response( - status_code, - json=payload, - headers=response_headers, - ) - ) - - -def _text_response( - status_code: int, - payload: str, - *, - content_type: str = "text/plain", -) -> APIResponse: - return APIResponse.from_httpx( - httpx.Response( - status_code, - content=payload.encode("utf-8"), - headers={ - "Content-Type": content_type, - "X-Request-Id": f"mock-{status_code}", - }, - ) - ) - - -class PlaygroundMockTransport: - def __init__(self, settings: PlaygroundSettings) -> None: - self._settings = settings - self._responses: dict[tuple[str, str, tuple[tuple[str, str], ...]], APIResponse] = {} - - def add_response( - self, - method: str, - path: str, - response: APIResponse, - *, - params: dict[str, Any] | None = None, - ) -> None: - key = (method.upper(), self._normalize_path(path), _normalize_params(params)) - self._responses[key] = response - - def request( - self, - method: str, - path: str, - *, - params: dict[str, Any] | None = None, - body: Any = None, - headers: dict[str, str] | None = None, - ) -> APIResponse: - del body, headers - scenario = self._settings.mock_scenario - if scenario == "timeout": - raise APITimeoutError("Mocked timeout after configured read timeout") - if scenario == "network-error": - raise APIConnectionError("Mocked network failure") - if scenario == "slow": - time.sleep(3.0) - if scenario in ERROR_SCENARIOS: - response_headers = {"Retry-After": "60"} if scenario == "429" else None - error_response = _json_response( - int(scenario), - {"error": f"Mocked {scenario}", "scenario": scenario}, - headers=response_headers, - ) - raise_for_response(error_response) - - normalized_path = self._normalize_path(path) - key = (method.upper(), normalized_path, _normalize_params(params)) - response: APIResponse | None = self._responses.get(key) - if response is None: - response = self._responses.get((method.upper(), normalized_path, ())) - if response is None: - response = _json_response( - 404, - { - "error": f"No mock response for {method.upper()} {normalized_path}", - "scenario": "fixtures", - }, - ) - raise_for_response(response) - return response - - def get( - self, - path: str, - *, - params: dict[str, Any] | None = None, - headers: dict[str, str] | None = None, - ) -> APIResponse: - return self.request("GET", path, params=params, headers=headers) - - def post( - self, - path: str, - *, - body: Any = None, - headers: dict[str, str] | None = None, - ) -> APIResponse: - return self.request("POST", path, body=body, headers=headers) - - def put( - self, - path: str, - *, - body: Any = None, - headers: dict[str, str] | None = None, - ) -> APIResponse: - return self.request("PUT", path, body=body, headers=headers) - - def patch( - self, - path: str, - *, - body: Any = None, - headers: dict[str, str] | None = None, - ) -> APIResponse: - return self.request("PATCH", path, body=body, headers=headers) - - def delete( - self, - path: str, - *, - params: dict[str, Any] | None = None, - headers: dict[str, str] | None = None, - ) -> APIResponse: - return self.request("DELETE", path, params=params, headers=headers) - - def close(self) -> None: - return None - - def transport_stats(self) -> dict[str, Any]: - return { - "mode": "mock", - "scenario": self._settings.mock_scenario, - "label": self._settings.selected_scenario.label, - } - - @staticmethod - def _normalize_path(path: str) -> str: - return path if path.startswith("/") else f"/{path}" - - -def build_mock_transport(settings: PlaygroundSettings) -> PlaygroundMockTransport: - transport = PlaygroundMockTransport(settings) - - node_id = settings.sample_node_id - job_id = settings.sample_job_id - session_uuid = settings.sample_session_uuid - coerced_node_id = _coerce_identifier(node_id) - coerced_job_id = _coerce_identifier(job_id) - - profile = _load_fixture("accounts/show.json") - balance = _load_fixture("accounts/balance.json") - apps = _load_fixture("apps/index.json") - network = _load_fixture("network/info.json") - nodes = _load_fixture("nodes/index.json") - node_show = _load_fixture("nodes/show.json") - sessions = _load_fixture("sessions/index.json") - recent_sessions = copy.deepcopy(sessions) - idle_job = _load_fixture("idle_jobs/show.json") - idle_logs = _load_fixture("idle_jobs/logs.json") - mr_sessions = _load_fixture("services/mr/index.json") - vpn_sessions = _load_fixture("services/vpn/index.json") - service_info = _load_fixture("services/session/info.json") - service_logs = _load_fixture("services/session/logs.json") - - nodes[0]["id"] = coerced_node_id - node_show["id"] = coerced_node_id - sessions[0]["uuid"] = session_uuid - recent_sessions[0]["uuid"] = f"recent-{session_uuid}" - recent_sessions[0]["termination_reason"] = 0 - recent_sessions[0]["duration"] = 3600 - idle_job["node_id"] = coerced_node_id - idle_job["job_id"] = coerced_job_id - service_info["uuid"] = session_uuid - mr_sessions[0]["uuid"] = f"mr-{session_uuid}" - vpn_sessions[0]["uuid"] = f"vpn-{session_uuid}" - - transport.add_response("GET", "/network", _json_response(200, network)) - transport.add_response("GET", "/accounts", _json_response(200, profile)) - transport.add_response("GET", "/accounts/balance", _json_response(200, balance)) - transport.add_response( - "POST", - "/accounts", - _json_response(201, {"wallet": "wallet-mock-001", "status": "generated"}), - ) - transport.add_response("GET", "/apps", _json_response(200, apps)) - transport.add_response("GET", "/nodes", _json_response(200, nodes)) - transport.add_response("GET", f"/nodes/{node_id}", _json_response(200, node_show)) - transport.add_response( - "GET", - f"/nodes/{node_id}/ident", - _text_response(200, f"ssh-ed25519 AAAA-playground-{node_id} octaspace@mock"), - ) - transport.add_response( - "GET", - f"/nodes/{node_id}/logs", - _text_response(200, "node log line 1\nnode log line 2\nnode log line 3"), - ) - transport.add_response( - "PATCH", - f"/nodes/{node_id}/prices", - _json_response(200, {"status": "updated", "node_id": coerced_node_id}), - ) - transport.add_response( - "GET", - f"/nodes/{node_id}/reboot", - _json_response(200, {"status": "rebooting", "node_id": coerced_node_id}), - ) - transport.add_response("GET", "/sessions", _json_response(200, sessions)) - transport.add_response( - "GET", - "/sessions", - _json_response(200, recent_sessions), - params={"recent": True}, - ) - transport.add_response( - "GET", - f"/idle_jobs/{node_id}/{job_id}", - _json_response(200, idle_job), - ) - transport.add_response( - "GET", - f"/idle_jobs/{node_id}/{job_id}/logs", - _json_response(200, idle_logs), - ) - transport.add_response("GET", "/services/mr", _json_response(200, mr_sessions)) - transport.add_response( - "POST", - "/services/mr", - _json_response(201, {"uuid": f"new-mr-{session_uuid}", "status": "starting"}), - ) - transport.add_response("GET", "/services/render", _json_response(200, [])) - transport.add_response( - "POST", - "/services/render", - _json_response(201, {"uuid": f"new-render-{session_uuid}", "status": "queued"}), - ) - transport.add_response("GET", "/services/vpn", _json_response(200, vpn_sessions)) - transport.add_response( - "POST", - "/services/vpn", - _json_response(201, {"uuid": f"new-vpn-{session_uuid}", "status": "starting"}), - ) - transport.add_response( - "GET", - f"/services/{session_uuid}/info", - _json_response(200, service_info), - ) - transport.add_response( - "GET", - f"/services/{session_uuid}/logs", - _json_response(200, service_logs), - ) - transport.add_response( - "POST", - f"/services/{session_uuid}/stop", - _json_response(200, {"uuid": session_uuid, "status": "stopped"}), - ) - - return transport diff --git a/playground/routes/__init__.py b/playground/routes/__init__.py deleted file mode 100644 index fbe6ebb..0000000 --- a/playground/routes/__init__.py +++ /dev/null @@ -1,37 +0,0 @@ -from __future__ import annotations - -from fastapi import APIRouter -from fastapi.responses import RedirectResponse - -from .account import router as account_router -from .apps import router as apps_router -from .dashboard import router as dashboard_router -from .diagnostics import router as diagnostics_router -from .idle_jobs import router as idle_jobs_router -from .nodes import router as nodes_router -from .services import router as services_router -from .sessions import router as sessions_router -from .settings import router as settings_router - -router = APIRouter() - -# Redirect roots to dashboard -@router.get("/", include_in_schema=False) -def root() -> RedirectResponse: - return RedirectResponse("/playground/dashboard", status_code=303) - -@router.get("/playground", include_in_schema=False) -def playground_root() -> RedirectResponse: - return RedirectResponse("/playground/dashboard", status_code=303) - -router.include_router(dashboard_router) -router.include_router(account_router) -router.include_router(apps_router) -router.include_router(nodes_router) -router.include_router(sessions_router) -router.include_router(services_router) -router.include_router(idle_jobs_router) -router.include_router(diagnostics_router) -router.include_router(settings_router) - -__all__ = ["router"] diff --git a/playground/routes/account.py b/playground/routes/account.py deleted file mode 100644 index 432cdb0..0000000 --- a/playground/routes/account.py +++ /dev/null @@ -1,51 +0,0 @@ -from __future__ import annotations - -from typing import Any - -from fastapi import APIRouter, Request -from fastapi.responses import HTMLResponse - -from ..helpers import ( - base_context, - build_client, - capture_api_call, - get_state, - get_templates, -) - -router = APIRouter() - - -@router.get("/playground/account", response_class=HTMLResponse) -def account(request: Request) -> HTMLResponse: - state = get_state(request) - settings = state.snapshot_settings() - with build_client(settings) as client: - if settings.api_key_present: - profile = capture_api_call( - state, - label="octa_client.accounts.profile", - method="GET", - path="/accounts", - fn=lambda: client.accounts.profile(), - ) - balance = capture_api_call( - state, - label="octa_client.accounts.balance", - method="GET", - path="/accounts/balance", - fn=lambda: client.accounts.balance(), - ) - else: - profile = None - balance = None - transport_stats = client.transport_stats() - - context: dict[str, Any] = base_context( - request, page="account", title="Account", - settings=settings, transport_stats=transport_stats, - ) - context.update({"profile": profile, "balance": balance}) - return get_templates(request).TemplateResponse( - request=request, name="account.html", context=context, - ) diff --git a/playground/routes/apps.py b/playground/routes/apps.py deleted file mode 100644 index 3e30bfa..0000000 --- a/playground/routes/apps.py +++ /dev/null @@ -1,40 +0,0 @@ -from __future__ import annotations - -from typing import Any - -from fastapi import APIRouter, Request -from fastapi.responses import HTMLResponse - -from ..helpers import ( - base_context, - build_client, - capture_api_call, - get_state, - get_templates, -) - -router = APIRouter() - - -@router.get("/playground/apps", response_class=HTMLResponse) -def apps(request: Request) -> HTMLResponse: - state = get_state(request) - settings = state.snapshot_settings() - with build_client(settings) as client: - apps_result = capture_api_call( - state, - label="octa_client.apps.list", - method="GET", - path="/apps", - fn=lambda: client.apps.list(), - ) - transport_stats = client.transport_stats() - - context: dict[str, Any] = base_context( - request, page="apps", title="Apps", - settings=settings, transport_stats=transport_stats, - ) - context.update({"apps": apps_result}) - return get_templates(request).TemplateResponse( - request=request, name="apps.html", context=context, - ) diff --git a/playground/routes/dashboard.py b/playground/routes/dashboard.py deleted file mode 100644 index b96417d..0000000 --- a/playground/routes/dashboard.py +++ /dev/null @@ -1,83 +0,0 @@ -from __future__ import annotations - -from typing import Any - -from fastapi import APIRouter, Request -from fastapi.responses import HTMLResponse - -from ..helpers import ( - base_context, - build_client, - capture_api_call, - config_snapshot, - get_state, - get_templates, -) - -router = APIRouter() - - -@router.get("/playground/dashboard", response_class=HTMLResponse) -def dashboard(request: Request) -> HTMLResponse: - state = get_state(request) - settings = state.snapshot_settings() - with build_client(settings) as client: - network = capture_api_call( - state, - label="octa_client.network.info", - method="GET", - path="/network", - fn=lambda: client.network.info(), - ) - if settings.api_key_present: - profile = capture_api_call( - state, - label="octa_client.accounts.profile", - method="GET", - path="/accounts", - fn=lambda: client.accounts.profile(), - ) - balance = capture_api_call( - state, - label="octa_client.accounts.balance", - method="GET", - path="/accounts/balance", - fn=lambda: client.accounts.balance(), - ) - else: - profile = None - balance = None - transport_stats = client.transport_stats() - - context: dict[str, Any] = base_context( - request, page="dashboard", title="Dashboard", - settings=settings, transport_stats=transport_stats, - ) - context.update({"network": network, "profile": profile, "balance": balance}) - return get_templates(request).TemplateResponse( - request=request, name="dashboard.html", context=context, - ) - - -@router.get("/playground/network", response_class=HTMLResponse) -def network(request: Request) -> HTMLResponse: - state = get_state(request) - settings = state.snapshot_settings() - with build_client(settings) as client: - network_result = capture_api_call( - state, - label="octa_client.network.info", - method="GET", - path="/network", - fn=lambda: client.network.info(), - ) - transport_stats = client.transport_stats() - - context: dict[str, Any] = base_context( - request, page="network", title="Network", - settings=settings, transport_stats=transport_stats, - ) - context.update({"network": network_result, "config_snapshot": config_snapshot(settings)}) - return get_templates(request).TemplateResponse( - request=request, name="network.html", context=context, - ) diff --git a/playground/routes/diagnostics.py b/playground/routes/diagnostics.py deleted file mode 100644 index e87b4c6..0000000 --- a/playground/routes/diagnostics.py +++ /dev/null @@ -1,188 +0,0 @@ -from __future__ import annotations - -from datetime import datetime, timezone -from functools import partial -from time import monotonic -from typing import Any - -from fastapi import APIRouter, Request -from fastapi.responses import HTMLResponse - -from ..catalog import CALLS, call_for, default_payload, execute_call, payload_for, resolved_path -from ..errors import PayloadError, error_payload -from ..helpers import ( # noqa: F401 — diagnostics_preset_path registered in Jinja globals - base_context, - build_client, - capture_api_call, - diagnostics_preset_path, - elapsed_ms, - get_state, - get_templates, - parse_payload_json, - validate_csrf, -) -from ..state import PlaygroundSettings -from ..views import summarize_payload - -router = APIRouter() - - -def _transport_stats(settings: PlaygroundSettings) -> dict[str, Any]: - with build_client(settings) as client: - return client.transport_stats() - - -@router.get("/playground/diagnostics", response_class=HTMLResponse) -def diagnostics_show( - request: Request, - call: str | None = None, - payload_json: str | None = None, -) -> HTMLResponse: - settings = get_state(request).snapshot_settings() - selected_call = call_for(call or CALLS[0].id) - context: dict[str, Any] = base_context( - request, page="diagnostics", title="Diagnostics", - settings=settings, transport_stats=_transport_stats(settings), - ) - context.update({ - "calls": CALLS, - "selected_call": selected_call, - "payload_json": ( - payload_json if payload_json is not None - else default_payload(selected_call, settings) - ), - "result": {"status": "idle"}, - }) - return get_templates(request).TemplateResponse( - request=request, name="diagnostics.html", context=context, - ) - - -@router.post("/playground/diagnostics/run", response_class=HTMLResponse) -async def diagnostics_run(request: Request) -> HTMLResponse: - state = get_state(request) - settings = state.snapshot_settings() - form_data = await request.form() - validate_csrf(request, str(form_data.get("csrf_token") or "")) - selected_call = call_for(str(form_data.get("call") or CALLS[0].id)) - payload_json = str(form_data.get("payload_json") or "") - - try: - payload = parse_payload_json(payload_json) if selected_call.takes_payload else None - except ValueError as exc: - result: dict[str, Any] = { - "status": "error", "error": PayloadError(str(exc)), "duration_ms": 0, - } - else: - started_at = monotonic() - with build_client(settings) as client: - response = capture_api_call( - state, - label=selected_call.label, - method=selected_call.method, - path=resolved_path(selected_call, payload), - fn=lambda: execute_call(client, selected_call, payload), - ) - duration_ms = elapsed_ms(started_at) - from octaspace.errors import OctaSpaceError - if isinstance(response, OctaSpaceError): - result = {"status": "error", "error": response, "duration_ms": duration_ms} - else: - result = { - "status": "success", - "data": response.data, - "response": response, - "duration_ms": duration_ms, - } - - context: dict[str, Any] = base_context( - request, page="diagnostics", title="Diagnostics", - settings=settings, transport_stats=_transport_stats(settings), - page_url="/playground/diagnostics", - ) - context.update({ - "calls": CALLS, - "selected_call": selected_call, - "payload_json": payload_json or default_payload(selected_call, settings), - "result": result, - }) - return get_templates(request).TemplateResponse( - request=request, name="diagnostics.html", context=context, - ) - - -@router.post("/playground/diagnostics/smoke", response_class=HTMLResponse) -async def diagnostics_smoke(request: Request) -> HTMLResponse: - from ..catalog import smoke_calls - state = get_state(request) - settings = state.snapshot_settings() - form_data = await request.form() - validate_csrf(request, str(form_data.get("csrf_token") or "")) - suites = smoke_calls() - started_at = monotonic() - suite_results: list[dict[str, Any]] = [] - - from octaspace.errors import OctaSpaceError - with build_client(settings) as client: - transport_stats = client.transport_stats() - for suite in suites: - suite_started = monotonic() - response = capture_api_call( - state, - label=suite.label, - method=suite.method, - path=suite.path, - fn=partial(execute_call, client, suite, payload_for(suite.id, settings)), - ) - duration_ms = elapsed_ms(suite_started) - if isinstance(response, OctaSpaceError): - suite_results.append({ - "id": suite.id, "label": suite.label, - "method": suite.method, "path": suite.path, - "requires_auth": suite.requires_auth, - "status": "failed", "duration_ms": duration_ms, - "error": error_payload(response), - }) - else: - suite_results.append({ - "id": suite.id, "label": suite.label, - "method": suite.method, "path": suite.path, - "requires_auth": suite.requires_auth, - "status": "passed", "duration_ms": duration_ms, - "http_status": response.status_code, - "request_id": response.request_id, - "summary": summarize_payload(response.data), - }) - - failed = sum(1 for r in suite_results if r["status"] == "failed") - smoke_result: dict[str, Any] = { - "kind": "sdk_smoke", - "status": "passed" if failed == 0 else "failed", - "started_at": datetime.now(timezone.utc).isoformat(timespec="seconds"), - "duration_ms": elapsed_ms(started_at), - "summary": { - "total": len(suite_results), - "passed": len(suite_results) - failed, - "failed": failed, - }, - "suites": suite_results, - } - - settings = state.snapshot_settings() - selected_call = call_for("network.info") - context: dict[str, Any] = base_context( - request, page="diagnostics", title="Diagnostics", - settings=settings, transport_stats=transport_stats, - page_url="/playground/diagnostics", - ) - context.update({ - "calls": CALLS, - "selected_call": selected_call, - "payload_json": default_payload(selected_call, settings), - "result": { - "status": "smoke", "duration_ms": smoke_result["duration_ms"], "data": smoke_result, - }, - }) - return get_templates(request).TemplateResponse( - request=request, name="diagnostics.html", context=context, - ) diff --git a/playground/routes/idle_jobs.py b/playground/routes/idle_jobs.py deleted file mode 100644 index 4b78a68..0000000 --- a/playground/routes/idle_jobs.py +++ /dev/null @@ -1,75 +0,0 @@ -from __future__ import annotations - -from typing import Any - -from fastapi import APIRouter, Request -from fastapi.responses import HTMLResponse - -from ..catalog import payload_for -from ..helpers import ( - base_context, - build_client, - capture_api_call, - get_state, - get_templates, -) - -router = APIRouter() - - -@router.get("/playground/idle-jobs", response_class=HTMLResponse) -def idle_jobs_show( - request: Request, - node_id: str | None = None, - job_id: str | None = None, -) -> HTMLResponse: - state = get_state(request) - settings = state.snapshot_settings() - defaults = payload_for("idle_jobs.find", settings) or {} - resolved_node_id = node_id or str(defaults.get("node_id", settings.sample_node_id)) - resolved_job_id = job_id or str(defaults.get("job_id", settings.sample_job_id)) - - job = None - logs = None - with build_client(settings) as client: - if node_id is not None and job_id is not None: - job = capture_api_call( - state, - label=( - f"octa_client.idle_jobs.find(" - f"node_id: {resolved_node_id}, job_id: {resolved_job_id})" - ), - method="GET", - path=f"/idle_jobs/{resolved_node_id}/{resolved_job_id}", - fn=lambda: client.idle_jobs.find( - node_id=resolved_node_id, job_id=resolved_job_id, - ), - ) - logs = capture_api_call( - state, - label=( - f"octa_client.idle_jobs.logs(" - f"node_id: {resolved_node_id}, job_id: {resolved_job_id})" - ), - method="GET", - path=f"/idle_jobs/{resolved_node_id}/{resolved_job_id}/logs", - fn=lambda: client.idle_jobs.logs( - node_id=resolved_node_id, job_id=resolved_job_id, - ), - ) - transport_stats = client.transport_stats() - - context: dict[str, Any] = base_context( - request, page="idle_jobs", title="Idle Jobs", - settings=settings, transport_stats=transport_stats, - ) - context.update({ - "node_id": resolved_node_id, - "job_id": resolved_job_id, - "job": job, - "logs": logs, - "requested": node_id is not None and job_id is not None, - }) - return get_templates(request).TemplateResponse( - request=request, name="idle_jobs.html", context=context, - ) diff --git a/playground/routes/nodes.py b/playground/routes/nodes.py deleted file mode 100644 index 7192463..0000000 --- a/playground/routes/nodes.py +++ /dev/null @@ -1,78 +0,0 @@ -from __future__ import annotations - -from typing import Any - -from fastapi import APIRouter, Request -from fastapi.responses import HTMLResponse - -from ..helpers import ( - base_context, - build_client, - capture_api_call, - get_state, - get_templates, -) - -router = APIRouter() - - -@router.get("/playground/nodes", response_class=HTMLResponse) -def nodes_index(request: Request, selected_id: str | None = None) -> HTMLResponse: - state = get_state(request) - settings = state.snapshot_settings() - selected_node_id = selected_id or request.query_params.get("id") - with build_client(settings) as client: - nodes_result = capture_api_call( - state, - label="octa_client.nodes.list", - method="GET", - path="/nodes", - fn=lambda: client.nodes.list(), - ) - selected_node = None - if selected_node_id: - selected_node = capture_api_call( - state, - label=f"octa_client.nodes.find({selected_node_id})", - method="GET", - path=f"/nodes/{selected_node_id}", - fn=lambda: client.nodes.find(selected_node_id), - ) - transport_stats = client.transport_stats() - - context: dict[str, Any] = base_context( - request, page="nodes", title="Nodes", - settings=settings, transport_stats=transport_stats, - ) - context.update({ - "nodes": nodes_result, - "selected_node": selected_node, - "selected_node_id": selected_node_id, - }) - return get_templates(request).TemplateResponse( - request=request, name="nodes_index.html", context=context, - ) - - -@router.get("/playground/nodes/{node_id}", response_class=HTMLResponse) -def nodes_show(request: Request, node_id: str) -> HTMLResponse: - state = get_state(request) - settings = state.snapshot_settings() - with build_client(settings) as client: - node = capture_api_call( - state, - label=f"octa_client.nodes.find({node_id})", - method="GET", - path=f"/nodes/{node_id}", - fn=lambda: client.nodes.find(node_id), - ) - transport_stats = client.transport_stats() - - context: dict[str, Any] = base_context( - request, page="nodes", title="Nodes", - settings=settings, transport_stats=transport_stats, - ) - context.update({"node": node, "node_id": node_id}) - return get_templates(request).TemplateResponse( - request=request, name="node_show.html", context=context, - ) diff --git a/playground/routes/services.py b/playground/routes/services.py deleted file mode 100644 index 5e4ee8f..0000000 --- a/playground/routes/services.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import annotations - -from typing import Any - -from fastapi import APIRouter, Request -from fastapi.responses import HTMLResponse - -from ..helpers import ( - base_context, - build_client, - capture_api_call, - get_state, - get_templates, -) - -router = APIRouter() - - -@router.get("/playground/services", response_class=HTMLResponse) -def services_show(request: Request, tab: str | None = None) -> HTMLResponse: - state = get_state(request) - settings = state.snapshot_settings() - active_tab = tab if tab in {"mr", "render", "vpn"} else "mr" - with build_client(settings) as client: - mr_sessions = capture_api_call( - state, - label="octa_client.services.mr.list", - method="GET", - path="/services/mr", - fn=lambda: client.services.mr.list(), - ) - render_sessions = capture_api_call( - state, - label="octa_client.services.render.list", - method="GET", - path="/services/render", - fn=lambda: client.services.render.list(), - ) - vpn_sessions = capture_api_call( - state, - label="octa_client.services.vpn.list", - method="GET", - path="/services/vpn", - fn=lambda: client.services.vpn.list(), - ) - transport_stats = client.transport_stats() - - context: dict[str, Any] = base_context( - request, page="services", title="Services", - settings=settings, transport_stats=transport_stats, - ) - context.update({ - "active_tab": active_tab, - "mr_sessions": mr_sessions, - "render_sessions": render_sessions, - "vpn_sessions": vpn_sessions, - }) - return get_templates(request).TemplateResponse( - request=request, name="services.html", context=context, - ) diff --git a/playground/routes/sessions.py b/playground/routes/sessions.py deleted file mode 100644 index 86c493a..0000000 --- a/playground/routes/sessions.py +++ /dev/null @@ -1,122 +0,0 @@ -from __future__ import annotations - -from typing import Any -from urllib.parse import urlencode - -from fastapi import APIRouter, Request -from fastapi.responses import HTMLResponse, RedirectResponse - -from octaspace.errors import OctaSpaceError - -from ..helpers import ( - base_context, - build_client, - capture_api_call, - get_state, - get_templates, - validate_csrf, -) -from ..views import response_data - -router = APIRouter() - - -@router.get("/playground/sessions", response_class=HTMLResponse) -def sessions_index( - request: Request, - tab: str | None = None, - stop_uuid: str | None = None, - stop_notice: str | None = None, - stop_error: str | None = None, - request_id: str | None = None, -) -> HTMLResponse: - state = get_state(request) - settings = state.snapshot_settings() - active_tab = tab if tab in {"current", "recent"} else "current" - with build_client(settings) as client: - current_sessions = capture_api_call( - state, - label="octa_client.sessions.list", - method="GET", - path="/sessions", - fn=lambda: client.sessions.list(), - ) - recent_sessions = capture_api_call( - state, - label="octa_client.sessions.list(recent: true)", - method="GET", - path="/sessions?recent=true", - fn=lambda: client.sessions.list(recent=True), - ) - transport_stats = client.transport_stats() - - stop_target = None - current_data = response_data(current_sessions) - if isinstance(current_data, list) and stop_uuid: - stop_target = next( - (s for s in current_data if isinstance(s, dict) and str(s.get("uuid")) == stop_uuid), - None, - ) - - context: dict[str, Any] = base_context( - request, page="sessions", title="Sessions", - settings=settings, transport_stats=transport_stats, - ) - context.update({ - "active_tab": active_tab, - "current_sessions": current_sessions, - "recent_sessions": recent_sessions, - "stop_uuid": stop_uuid, - "stop_notice": stop_notice, - "stop_error": stop_error, - "stop_request_id": request_id, - "stop_target": stop_target, - }) - return get_templates(request).TemplateResponse( - request=request, name="sessions.html", context=context, - ) - - -@router.api_route("/playground/sessions/stop", methods=["POST"], include_in_schema=False) -async def sessions_stop(request: Request) -> RedirectResponse: - state = get_state(request) - settings = state.snapshot_settings() - form_data = await request.form() - validate_csrf(request, str(form_data.get("csrf_token") or "")) - active_tab = str(form_data.get("tab") or "current") - uuid = str(form_data.get("uuid") or "").strip() - - if not uuid: - params = urlencode({"tab": active_tab, "stop_error": "Session UUID is required"}) - return RedirectResponse(f"/playground/sessions?{params}", status_code=303) - - stop_params: dict[str, Any] = {} - score_str = str(form_data.get("score") or "").strip() - if score_str: - try: - stop_params["score"] = int(score_str) - except ValueError: - params = urlencode({ - "tab": active_tab, - "stop_error": f"Score must be an integer, got: {score_str!r}", - }) - return RedirectResponse(f"/playground/sessions?{params}", status_code=303) - - with build_client(settings) as client: - response = capture_api_call( - state, - label=f"octa_client.services.session({uuid}).stop", - method="POST", - path=f"/services/{uuid}/stop", - fn=lambda: client.services.session(uuid).stop(**stop_params), - ) - - if isinstance(response, OctaSpaceError): - redirect_params: dict[str, str] = {"tab": active_tab, "stop_error": str(response)} - else: - redirect_params = { - "tab": active_tab, "stop_notice": f"Session {uuid} stop request submitted.", - } - if response.request_id: - redirect_params["request_id"] = response.request_id - return RedirectResponse(f"/playground/sessions?{urlencode(redirect_params)}", status_code=303) diff --git a/playground/routes/settings.py b/playground/routes/settings.py deleted file mode 100644 index ef322f8..0000000 --- a/playground/routes/settings.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import annotations - -from fastapi import APIRouter, Request -from fastapi.responses import RedirectResponse - -from ..helpers import get_state, safe_return_path, validate_csrf - -router = APIRouter() - - -@router.api_route("/playground/settings", methods=["POST", "PATCH"], include_in_schema=False) -async def settings_update(request: Request) -> RedirectResponse: - state = get_state(request) - form_data = await request.form() - validate_csrf(request, str(form_data.get("csrf_token") or "")) - form = {str(k): str(v) for k, v in form_data.items()} - state.update_settings(form) - return RedirectResponse(safe_return_path(form.get("return_to")), status_code=303) - - -@router.api_route("/playground/settings", methods=["DELETE"], include_in_schema=False) -async def settings_reset(request: Request) -> RedirectResponse: - validate_csrf(request) - state = get_state(request) - state.reset_settings() - return RedirectResponse( - safe_return_path(request.query_params.get("return_to")), - status_code=303, - ) - - -@router.api_route("/playground/request-log", methods=["POST", "DELETE"], include_in_schema=False) -async def request_log_clear(request: Request) -> RedirectResponse: - submitted_token: str | None = None - if request.method == "POST": - form_data = await request.form() - submitted_token = str(form_data.get("csrf_token") or "") - validate_csrf(request, submitted_token) - state = get_state(request) - state.clear_request_log() - return RedirectResponse( - request.headers.get("referer") or "/playground/dashboard", - status_code=303, - ) diff --git a/playground/state.py b/playground/state.py deleted file mode 100644 index a702dbf..0000000 --- a/playground/state.py +++ /dev/null @@ -1,203 +0,0 @@ -from __future__ import annotations - -from collections import deque -from collections.abc import Mapping -from dataclasses import dataclass -from datetime import datetime, timezone -from threading import Lock - - -@dataclass(frozen=True, slots=True) -class MockScenario: - value: str - label: str - description: str - - -MOCK_SCENARIOS: tuple[MockScenario, ...] = ( - MockScenario( - value="real", - label="Real API", - description="Use the actual OctaSpace API.", - ), - MockScenario( - value="fixtures", - label="Fixtures", - description="Deterministic fixture-backed responses for local browser QA.", - ), - MockScenario( - value="401", - label="401 Unauthorized", - description="Simulates invalid or missing API key.", - ), - MockScenario( - value="403", - label="403 Forbidden", - description="Simulates insufficient permissions.", - ), - MockScenario( - value="404", - label="404 Not Found", - description="Simulates a missing resource.", - ), - MockScenario( - value="429", - label="429 Rate Limited", - description="Simulates rate limiting with Retry-After: 60.", - ), - MockScenario( - value="500", - label="500 Server Error", - description="Simulates an internal server error.", - ), - MockScenario( - value="slow", - label="Slow (3s)", - description="Adds a 3-second delay before returning fixture responses.", - ), - MockScenario( - value="timeout", - label="Timeout", - description="Raises a timeout error without contacting the API.", - ), - MockScenario( - value="network-error", - label="Network Error", - description="Simulates a connection failure.", - ), -) - - -def _safe_scenario(value: str | None) -> str: - candidate = (value or "").strip() - known = {entry.value for entry in MOCK_SCENARIOS} - return candidate if candidate in known else "real" - - -@dataclass(frozen=True, slots=True) -class PlaygroundSettings: - api_key: str = "" - base_url: str = "https://api.octa.space" - mock_scenario: str = "real" - sample_node_id: str = "1" - sample_job_id: str = "42" - sample_session_uuid: str = "sess-abc-123" - - @property - def api_key_present(self) -> bool: - return bool(self.api_key.strip()) - - @property - def is_mock_active(self) -> bool: - return self.mock_scenario != "real" - - @property - def auth_available(self) -> bool: - return self.is_mock_active or self.api_key_present - - @property - def selected_scenario(self) -> MockScenario: - return next( - (entry for entry in MOCK_SCENARIOS if entry.value == self.mock_scenario), - MOCK_SCENARIOS[0], - ) - - @property - def transport_label(self) -> str: - if self.is_mock_active: - return self.selected_scenario.label - return "Real API" - - @property - def transport_description(self) -> str: - if self.is_mock_active: - return self.selected_scenario.description - return "Uses the real API through the Python httpx transport." - - @property - def masked_api_key(self) -> str: - key = self.api_key.strip() - if not key: - return "" - if len(key) <= 8: - return "*" * len(key) - return f"{key[:4]}{'*' * max(len(key) - 8, 4)}{key[-4:]}" - - @classmethod - def from_form( - cls, - form: Mapping[str, str], - current: PlaygroundSettings | None = None, - ) -> PlaygroundSettings: - existing = current or cls() - return cls( - api_key=form.get("api_key", existing.api_key), - base_url=form.get("base_url", existing.base_url).strip() or existing.base_url, - mock_scenario=_safe_scenario(form.get("mock_scenario", existing.mock_scenario)), - sample_node_id=existing.sample_node_id, - sample_job_id=existing.sample_job_id, - sample_session_uuid=existing.sample_session_uuid, - ) - - -@dataclass(frozen=True, slots=True) -class RequestLogEntry: - timestamp: str - label: str - method: str - path: str - status: str - duration_ms: int - error: str | None = None - - -class PlaygroundState: - _MAX_LOG_ENTRIES = 100 - - def __init__(self) -> None: - self._lock = Lock() - self._settings = PlaygroundSettings() - self._request_log: deque[RequestLogEntry] = deque(maxlen=self._MAX_LOG_ENTRIES) - - def snapshot_settings(self) -> PlaygroundSettings: - with self._lock: - return self._settings - - def update_settings(self, form: Mapping[str, str]) -> PlaygroundSettings: - with self._lock: - self._settings = PlaygroundSettings.from_form(form, self._settings) - return self._settings - - def reset_settings(self) -> None: - with self._lock: - self._settings = PlaygroundSettings() - - def request_log(self) -> list[RequestLogEntry]: - with self._lock: - return list(self._request_log) # snapshot in insertion order (newest first) - - def append_request( - self, - *, - label: str, - method: str, - path: str, - status: str, - duration_ms: int, - error: str | None = None, - ) -> None: - entry = RequestLogEntry( - timestamp=datetime.now(timezone.utc).isoformat(timespec="seconds"), - label=label, - method=method, - path=path, - status=status, - duration_ms=duration_ms, - error=error, - ) - with self._lock: - self._request_log.appendleft(entry) # O(1), deque handles maxlen cap - - def clear_request_log(self) -> None: - with self._lock: - self._request_log.clear() diff --git a/playground/static/playground.css b/playground/static/playground.css deleted file mode 100644 index 5b28ece..0000000 --- a/playground/static/playground.css +++ /dev/null @@ -1,38 +0,0 @@ -.json-key { color: rgb(148 163 184); } -.json-string { color: rgb(34 211 238); } -.json-number { color: rgb(251 191 36); } -.json-bool { color: rgb(129 140 248); } -.json-null { color: rgb(248 113 113); } - -::-webkit-scrollbar { width: 8px; height: 8px; } -::-webkit-scrollbar-track { background: rgb(15 23 42); } -::-webkit-scrollbar-thumb { background: rgb(51 65 85); border-radius: 9999px; } -::-webkit-scrollbar-thumb:hover { background: rgb(71 85 105); } - -/* Page loader ---------------------------------------------------------------- - Visibility is controlled from JS after 0.4 s, so fast responses never - produce a visible flash. */ -#page-loader { - display: none; - position: absolute; - inset: 0; - z-index: 20; - flex-direction: column; - align-items: center; - justify-content: center; - background: rgba(2, 6, 23, 0.78); -} -#page-loader.loader-active { - display: flex; - opacity: 1; -} - -.loader-spinner { - width: 28px; - height: 28px; - border: 2.5px solid rgba(148, 163, 184, 0.18); - border-top-color: rgb(34, 211, 238); - border-radius: 50%; - animation: loader-spin 0.7s linear infinite; -} -@keyframes loader-spin { to { transform: rotate(360deg); } } diff --git a/playground/static/playground.js b/playground/static/playground.js deleted file mode 100644 index 66dd43f..0000000 --- a/playground/static/playground.js +++ /dev/null @@ -1,102 +0,0 @@ -document.addEventListener("click", async (event) => { - const button = event.target.closest("[data-copy-json]") - if (!button) return - - const viewer = button.closest("[data-json-viewer]") - const pre = viewer && viewer.querySelector("pre") - if (!pre) return - - const originalLabel = button.dataset.label || button.textContent || "Copy" - - try { - await navigator.clipboard.writeText(pre.textContent || "") - button.textContent = button.dataset.copiedLabel || "Copied!" - } catch (_error) { - button.textContent = "Copy failed" - } - - button.disabled = true - window.setTimeout(() => { - button.textContent = originalLabel - button.disabled = false - }, 1500) -}) - -document.addEventListener("change", (event) => { - const field = event.target.closest("[data-autosubmit-setting]") - if (!field || !field.form) return - - const initialValue = field.dataset.initialValue ?? "" - const currentValue = field.value ?? "" - if (currentValue === initialValue) return - - field.form.requestSubmit() -}) - -// --------------------------------------------------------------------------- -// Page loader -// Appears only after 400 ms of waiting — fast responses produce no flash. -// --------------------------------------------------------------------------- -const _LOADER_SKIP = ["/playground/settings", "/playground/request-log"] -const _LOADER_DELAY_MS = 400 -let _loaderTimer = null - -function _activateLoader () { - const el = document.getElementById("page-loader") - if (!el) return - el.classList.add("loader-active") - el.setAttribute("aria-hidden", "false") -} - -function _scheduleLoader () { - if (_loaderTimer !== null) return - _loaderTimer = window.setTimeout(() => { - _loaderTimer = null - _activateLoader() - }, _LOADER_DELAY_MS) -} - -function _hideLoader () { - if (_loaderTimer !== null) { - window.clearTimeout(_loaderTimer) - _loaderTimer = null - } - const el = document.getElementById("page-loader") - if (!el) return - el.classList.remove("loader-active") - el.setAttribute("aria-hidden", "true") -} - -// Show on form submissions that cause a full-page reload. -document.addEventListener("submit", (event) => { - const form = event.target - if (!form || !form.action) return - try { - const pathname = new URL(form.action).pathname - if (_LOADER_SKIP.some((a) => pathname === a)) return - } catch (_) { return } - _scheduleLoader() -}) - -// Show on internal navigation link clicks (nav, tabs, Refetch, etc.). -document.addEventListener("click", (event) => { - if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return - const link = event.target.closest("a[href]") - if (!link) return - const href = link.getAttribute("href") - if (!href || href.startsWith("#") || href.startsWith("mailto:")) return - if (link.target === "_blank") return - try { - const url = new URL(href, location.href) - if (url.origin !== location.origin) return - } catch (_) { return } - _scheduleLoader() -}) - -// Remove loader if the browser restores a page from the back/forward cache. -window.addEventListener("pageshow", (event) => { - if (event.persisted) _hideLoader() -}) - -// Clear pending state if the browser keeps the page alive in the bfcache. -window.addEventListener("pagehide", _hideLoader) diff --git a/playground/templates/account.html b/playground/templates/account.html deleted file mode 100644 index 2d6ba2f..0000000 --- a/playground/templates/account.html +++ /dev/null @@ -1,75 +0,0 @@ -{% extends "base.html" %} -{% block content %} -{% set profile_data = response_data(profile) %} -{% set balance_data = response_data(balance) %} -{% set profile_error = response_error(profile) %} -{% set balance_error = response_error(balance) %} -{% set profile_hash = profile_data or {} %} -{% set balance_hash = balance_data or {} %} -{% set balance_value = balance_hash.get("balance") or balance_hash.get("amount") or ((balance_hash.values()|list)[0] if balance_hash else None) %} -{% set profile_formatted %} -
-
- Account UUID - {{ profile_hash.get("account_uuid") or profile_hash.get("account", {}).get("uuid") or "—" }} -
-
- Email - {{ profile_hash.get("email") or profile_hash.get("account", {}).get("email") or "—" }} -
-
- Avatar - {{ profile_hash.get("avatar") or "—" }} -
-
-{% endset %} -{% set balance_formatted %} -
-
-
Balance
-
{{ balance_value or "—" }}
-
-
-
Currency
-
{{ balance_hash.get("currency") or balance_hash.get("unit") or "OCTA" }}
-
-
-{% endset %} - -
-
-

Account

-

- Direct account view for the Python playground. The Dashboard uses the same panels in a condensed layout. -

-
- -
- {% set title = "Profile" %} - {% set hook = "octa_client.accounts.profile" %} - {% set data = profile_data %} - {% set error = profile_error %} - {% set locked = not playground_api_key_present %} - {% set tab_param = "profile_tab" %} - {% set current_tab = request.query_params.get("profile_tab", "formatted") %} - {% set empty_copy = "No data available." %} - {% set locked_copy = "Enter an API key in the sidebar to load this resource" %} - {% set badge = none %} - {% set formatted = profile_formatted %} - {% include "shared/resource_panel.html" %} - - {% set title = "Balance" %} - {% set hook = "octa_client.accounts.balance" %} - {% set data = balance_data %} - {% set error = balance_error %} - {% set locked = not playground_api_key_present %} - {% set tab_param = "balance_tab" %} - {% set current_tab = request.query_params.get("balance_tab", "formatted") %} - {% set empty_copy = "No data available." %} - {% set locked_copy = "Enter an API key in the sidebar to load this resource" %} - {% set badge = none %} - {% set formatted = balance_formatted %} - {% include "shared/resource_panel.html" %} -
-
-{% endblock %} diff --git a/playground/templates/apps.html b/playground/templates/apps.html deleted file mode 100644 index 0965071..0000000 --- a/playground/templates/apps.html +++ /dev/null @@ -1,81 +0,0 @@ -{% extends "base.html" %} -{% block content %} -{% set apps_data = response_data(apps) or [] %} -{% set apps_error = response_error(apps) %} -
-
-
-

Apps

-

- octa_client.apps.list — available application catalog for service sessions. -

-
-
- {% if playground_mock_active %} - Mock: {{ playground_mock_scenario }} - {% endif %} - Refetch -
-
- - {% if apps_error %} - {% set error = apps_error %} - {% include "shared/error_card.html" %} - {% elif apps_data %} -
-
- {{ apps_data|length }} apps - octa_client.apps.list -
-
- - - - - - - - - - - - - {% for app_item in apps_data %} - {% set app_uuid = app_item.get("uuid") %} - {% set app_image = app_item.get("image") or app_item.get("docker_image") %} - - - - - - - - - {% endfor %} - -
NameCategoryUUIDImagePortsActions
-
{{ app_item.get("name") or "—" }}
- {% if app_item.get("description") %} -
{{ app_item.get("description") }}
- {% endif %} -
{{ app_item.get("category") or "—" }}{{ app_uuid or "—" }}{{ app_image or "—" }}{{ app_ports_count(app_item) }} - {% if app_uuid %} - Use in MR Create - {% else %} - No UUID - {% endif %} -
-
-
- -
-
Raw JSON
- {% set data = apps_data %} - {% set max_height = "max-h-80" %} - {% include "shared/json_viewer.html" %} -
- {% else %} -
No apps found.
- {% endif %} -
-{% endblock %} diff --git a/playground/templates/base.html b/playground/templates/base.html deleted file mode 100644 index 2f13645..0000000 --- a/playground/templates/base.html +++ /dev/null @@ -1,184 +0,0 @@ - - - - - OctaSpace SDK Playground - - - - - - - -
- - -
-
- -
- {% block content %}{% endblock %} -
-
- -
-
-
-
-

Request Log

-
{{ - request_log|length }}
-
- {% if request_log %} -
- - -
- {% endif %} -
- - {% if request_log %} -
- - - - - - - - - - - - {% for entry in request_log %} - - - - - - - - {% endfor %} - -
MethodPathStatusDurationTime
{{ entry.method - }}{{ entry.path or entry.label or "—" }} - {% if entry.error %} - ERR - {% else %} - {{ - entry.status }} - {% endif %} - {{ entry.duration_ms }}ms{{ format_short_time(entry.timestamp) }}
-
- {% else %} -
No requests yet
- {% endif %} -
-
-
-
- - - \ No newline at end of file diff --git a/playground/templates/dashboard.html b/playground/templates/dashboard.html deleted file mode 100644 index 1a25518..0000000 --- a/playground/templates/dashboard.html +++ /dev/null @@ -1,120 +0,0 @@ -{% extends "base.html" %} -{% block content %} -{% set network_data = response_data(network) %} -{% set profile_data = response_data(profile) %} -{% set balance_data = response_data(balance) %} -{% set network_error = response_error(network) %} -{% set profile_error = response_error(profile) %} -{% set balance_error = response_error(balance) %} -{% set profile_hash = profile_data or {} %} -{% set balance_hash = balance_data or {} %} -{% set balance_value = balance_hash.get("balance") or balance_hash.get("amount") or ((balance_hash.values()|list)[0] if balance_hash else None) %} -{% set account_formatted %} -
-
- Account UUID - {{ profile_hash.get("account_uuid") or profile_hash.get("account", {}).get("uuid") or "—" }} -
-
- Email - {{ profile_hash.get("email") or profile_hash.get("account", {}).get("email") or "—" }} -
-
- Avatar - {{ profile_hash.get("avatar") or "—" }} -
-
-{% endset %} -{% set balance_formatted %} -
-
-
Balance
-
{{ balance_value or "—" }}
-
-
-
Currency
-
{{ balance_hash.get("currency") or balance_hash.get("unit") or "OCTA" }}
-
-
-{% endset %} - -
-
-

Dashboard

-

- Account health check. Demonstrates octa_client resource calls in the Python dummy app with runtime real/mock transport switching. -

-
- -
- - - {% if playground_mock_active %} - Mock transport active — - {{ playground_transport_label }} - {% elif playground_api_key_present %} - API key set — - {{ settings.base_url }} - {% else %} - No API key — enter one in the sidebar to test authenticated endpoints - {% endif %} - - {% if playground_mock_active %} - Mock: {{ playground_mock_scenario }} - {% endif %} -
- -
-
-
-
-

Network Stats

- public -
- octa_client.network.info -
- Open -
- - {% if network_error %} - {% set error = network_error %} - {% set max_height = "max-h-40" %} - {% include "shared/error_card.html" %} - {% elif network_data %} - {% set data = network_data %} - {% set max_height = "max-h-40" %} - {% include "shared/json_viewer.html" %} - {% else %} -
No network payload available.
- {% endif %} -
- -
- {% set title = "Account" %} - {% set hook = "octa_client.accounts.profile" %} - {% set data = profile_data %} - {% set error = profile_error %} - {% set locked = not playground_api_key_present %} - {% set tab_param = "account_tab" %} - {% set current_tab = request.query_params.get("account_tab", "formatted") %} - {% set empty_copy = "Account data is unavailable." %} - {% set locked_copy = "Enter an API key in the sidebar to load this resource" %} - {% set badge = none %} - {% set formatted = account_formatted %} - {% include "shared/resource_panel.html" %} - - {% set title = "Balance" %} - {% set hook = "octa_client.accounts.balance" %} - {% set data = balance_data %} - {% set error = balance_error %} - {% set locked = not playground_api_key_present %} - {% set tab_param = "balance_tab" %} - {% set current_tab = request.query_params.get("balance_tab", "formatted") %} - {% set empty_copy = "Balance data is unavailable." %} - {% set locked_copy = "Enter an API key in the sidebar to load this resource" %} - {% set badge = none %} - {% set formatted = balance_formatted %} - {% include "shared/resource_panel.html" %} -
-
-{% endblock %} diff --git a/playground/templates/diagnostics.html b/playground/templates/diagnostics.html deleted file mode 100644 index 73818ab..0000000 --- a/playground/templates/diagnostics.html +++ /dev/null @@ -1,136 +0,0 @@ -{% extends "base.html" %} -{% block content %} -
-
-
-

Diagnostics

-

- Trigger any SDK method directly and inspect the result or error. Switch the transport in the sidebar to test mock scenarios and typed failures. -

-
-
- - - -
-
- -
-
-
-
{{ playground_transport_label }}
-
- {{ playground_transport_description }}. Open timeout {{ config.open_timeout }}s, read timeout {{ config.read_timeout }}s, retries {{ config.max_retries }}. -
-
- {% if not playground_mock_active and not playground_api_key_present %} -
No API key — authenticated calls will fail until one is set
- {% endif %} -
- -
-
-

SDK Method

-
- {% for call in calls %} - {% set selected = call.id == selected_call.id %} -
- -
-
-
{{ call.label }}
-
{{ call.description }}
-
- {% if call.mutation %} - mutation - {% endif %} - {% if call.requires_auth %} - auth - {% endif %} -
-
- - {% if selected %} -
-
- - - {% if call.mutation or call.takes_payload %} -
- - -

Top-level JSON keys are forwarded as Python keyword arguments. For services.session.stop, include uuid. For idle_jobs.*, include node_id and job_id.

-
- {% endif %} - -
-
- {% endif %} -
- {% endfor %} -
-
- -
-
-

Result

- {% if result.duration_ms %} - {{ result.duration_ms }}ms - {% endif %} -
- - {% if result.status == "idle" %} -

Press Run to execute the selected method.

- {% elif result.status == "smoke" %} - {% set smoke = result.data %} -
-
- - SDK Smoke {{ 'passed' if smoke.status == 'passed' else 'finished with failures' }} -
-
-
-
Total
-
{{ smoke.summary.total }}
-
-
-
Passed
-
{{ smoke.summary.passed }}
-
-
-
Failed
-
{{ smoke.summary.failed }}
-
-
- {% set data = smoke %} - {% include "shared/json_viewer.html" %} -
- {% elif result.status == "success" %} -
-
- - Success -
-
-
-
Status
-
{{ result.response.status_code }}
-
- {% if result.response.request_id %} -
-
Request ID
-
{{ result.response.request_id }}
-
- {% endif %} -
- {% set data = result.data %} - {% include "shared/json_viewer.html" %} -
- {% elif result.status == "error" %} - {% set error = result.error %} - {% include "shared/error_card.html" %} - {% endif %} -
-
-
-{% endblock %} diff --git a/playground/templates/idle_jobs.html b/playground/templates/idle_jobs.html deleted file mode 100644 index be53b0b..0000000 --- a/playground/templates/idle_jobs.html +++ /dev/null @@ -1,86 +0,0 @@ -{% extends "base.html" %} -{% block content %} -{% set job_error = response_error(job) %} -{% set logs_error = response_error(logs) %} -{% set job_data = response_data(job) %} -{% set logs_data = response_data(logs) %} -
-
-
-

Idle Jobs

-

- octa_client.idle_jobs.find + octa_client.idle_jobs.logs — inspect a specific job by node_id and job_id. -

-
- {% if playground_mock_active %} -
- Mock: {{ playground_mock_scenario }} -
- {% endif %} -
- -
-
-
- - -
-
- - -
-
- -
-
-
- -
-
-
-
-

Status

- octa_client.idle_jobs.find(node_id: {{ node_id }}, job_id: {{ job_id }}) -
-
- - {% if not requested %} -
Enter a node ID and job ID to load the job.
- {% elif job_error %} - {% set error = job_error %} - {% set max_height = "max-h-56" %} - {% include "shared/error_card.html" %} - {% elif job_data %} - {% set data = job_data %} - {% set max_height = "max-h-[28rem]" %} - {% include "shared/json_viewer.html" %} - {% else %} -
No job response available.
- {% endif %} -
- -
-
-
-

Logs

- octa_client.idle_jobs.logs(node_id: {{ node_id }}, job_id: {{ job_id }}) -
-
- - {% if not requested %} -
Logs are loaded together with job details.
- {% elif logs_error %} - {% set error = logs_error %} - {% set max_height = "max-h-56" %} - {% include "shared/error_card.html" %} - {% elif logs_data %} - {% set data = logs_data %} - {% set max_height = "max-h-[28rem]" %} - {% include "shared/json_viewer.html" %} - {% else %} -
No log response available.
- {% endif %} -
-
-
-{% endblock %} diff --git a/playground/templates/network.html b/playground/templates/network.html deleted file mode 100644 index af86fd9..0000000 --- a/playground/templates/network.html +++ /dev/null @@ -1,73 +0,0 @@ -{% extends "base.html" %} -{% block content %} -{% set network_data = response_data(network) %} -{% set network_error = response_error(network) %} -
-
-
-

Network Stats

- public - {% if playground_mock_active %} - mock - {% endif %} -
-

- Public endpoint — no API key required. Compare the current transport payload with the Python client configuration side by side. -

-
- -
-
-
-
-

Python SDK

- octa_client.network.info -
- Refetch -
- -
-
status: {{ response_status(network) }}
-
request_id: {{ response_request_id(network) }}
-
- - {% if network_error %} - {% set error = network_error %} - {% include "shared/error_card.html" %} - {% elif network_data %} - {% set data = network_data %} - {% include "shared/json_viewer.html" %} - {% else %} -
No network payload available.
- {% endif %} -
- -
-
-
-

Transport

- {{ playground_transport_label }} -
- {{ transport_stats.mode }} -
- -
-
description: {{ playground_transport_description }}
-
timeouts: {{ playground_timeout_label }}
-
keep_alive: {{ config.keep_alive }}
-
max_retries: {{ config.max_retries }}
-
- - {% set data = config_snapshot %} - {% include "shared/json_viewer.html" %} -
-
- -
-
Client Configuration
- {% set data = config_snapshot %} - {% set max_height = "max-h-24" %} - {% include "shared/json_viewer.html" %} -
-
-{% endblock %} diff --git a/playground/templates/node_show.html b/playground/templates/node_show.html deleted file mode 100644 index a654668..0000000 --- a/playground/templates/node_show.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends "base.html" %} -{% block content %} -{% set node_data = response_data(node) %} -{% set node_error = response_error(node) %} -
-
-
-

Nodes

-

- Direct node view for octa_client.nodes.find({{ node_id }}). -

-
- Back to list -
- - {% if node_error %} - {% set error = node_error %} - {% include "shared/error_card.html" %} - {% elif node_data %} - {% set node = node_data %} - {% set close_path = "/playground/nodes" %} - {% include "shared/node_detail_panel.html" %} - {% else %} -
Node payload unavailable.
- {% endif %} -
-{% endblock %} diff --git a/playground/templates/nodes_index.html b/playground/templates/nodes_index.html deleted file mode 100644 index 78b7f88..0000000 --- a/playground/templates/nodes_index.html +++ /dev/null @@ -1,79 +0,0 @@ -{% extends "base.html" %} -{% block content %} -{% set nodes_data = response_data(nodes) or [] %} -{% set nodes_error = response_error(nodes) %} -{% set selected_error = response_error(selected_node) %} -{% set selected_node_data = response_data(selected_node) %} -
-
-
-

Nodes

-

- octa_client.nodes.list + octa_client.nodes.find(id) — click a row to load detail. -

-
-
- {% if playground_mock_active %} - Mock: {{ playground_mock_scenario }} - {% endif %} - Refetch -
-
- - {% if nodes_error %} - {% set error = nodes_error %} - {% include "shared/error_card.html" %} - {% elif nodes_data %} -
-
-
- {{ nodes_data|length }} nodes - {% if selected_node_data or selected_error %} - Close detail - {% endif %} -
- -
- - - - - - - - - - - - - {% for node in nodes_data %} - {% set selected = selected_node_id|string == node.get("id")|string %} - - - - - - - - - {% endfor %} - -
IDLocationCPUGPURAMState
{{ node.get("id") }}{{ node_location(node) }}{{ node.get("system", {}).get("cpu_cores") or node.get("data", {}).get("cpu_cores") or "—" }} cores{% if node_gpu_count(node) %}{{ node_gpu_count(node) }}× {% endif %}{{ node_gpu_model(node) }}{{ node_ram_label(node) }}{{ node.get("state") or "unknown" }}
-
-
- - {% if selected_error %} - {% set error = selected_error %} - {% include "shared/error_card.html" %} - {% elif selected_node_data %} - {% set node = selected_node_data %} - {% set node_id = selected_node_id %} - {% set close_path = "/playground/nodes" %} - {% include "shared/node_detail_panel.html" %} - {% endif %} -
- {% else %} -
No nodes found.
- {% endif %} -
-{% endblock %} diff --git a/playground/templates/services.html b/playground/templates/services.html deleted file mode 100644 index b684e09..0000000 --- a/playground/templates/services.html +++ /dev/null @@ -1,68 +0,0 @@ -{% extends "base.html" %} -{% block content %} -{% set mr_data = response_data(mr_sessions) or [] %} -{% set render_data = response_data(render_sessions) or [] %} -{% set vpn_data = response_data(vpn_sessions) or [] %} -{% set tabs = [ - {"id": "mr", "label": "Machine Rental", "hook": "octa_client.services.mr.list", "path": "/services/mr", "data": mr_data, "error": response_error(mr_sessions)}, - {"id": "render", "label": "Render", "hook": "octa_client.services.render.list", "path": "/services/render", "data": render_data, "error": response_error(render_sessions)}, - {"id": "vpn", "label": "VPN", "hook": "octa_client.services.vpn.list", "path": "/services/vpn", "data": vpn_data, "error": response_error(vpn_sessions)} -] %} -{% set active = (tabs | selectattr('id', 'equalto', active_tab) | list | first) or tabs[0] %} -
-
-
-

Services

-

- Available resources for each service type. Uses the Python SDK client against the currently selected transport. -

-
-
- {% if playground_mock_active %} - Mock: {{ playground_mock_scenario }} - {% endif %} - Refetch -
-
- -
- Start mutations (mr.create, render.create, vpn.create) exist on octa_client.services.* but are intentionally not wired up here — use Diagnostics for manual request inspection. -
- -
-
- {% for tab_item in tabs %} - {{ tab_item.label }} - {% endfor %} -
- -
-
- {{ active.hook }} - status: {{ 'error' if active.error else 'success' }} -
- - {% if active.error %} - {% set error = active.error %} - {% include "shared/error_card.html" %} - {% else %} -
-
{{ active.data|length }} items
-
- {% set data = active.data %} - {% set max_height = "max-h-[32rem]" %} - {% include "shared/json_viewer.html" %} -
-
- {% endif %} -
-
- -
-
Request Reference
- {% set data = {"method": "GET", "path": active.path, "resource": active.hook, "transport": playground_transport_label} %} - {% set max_height = "max-h-20" %} - {% include "shared/json_viewer.html" %} -
-
-{% endblock %} diff --git a/playground/templates/sessions.html b/playground/templates/sessions.html deleted file mode 100644 index 39eed96..0000000 --- a/playground/templates/sessions.html +++ /dev/null @@ -1,119 +0,0 @@ -{% extends "base.html" %} -{% block content %} -{% set current_data = response_data(current_sessions) %} -{% set recent_data = response_data(recent_sessions) %} -{% set current_error = response_error(current_sessions) %} -{% set recent_error = response_error(recent_sessions) %} -{% set active_data = recent_data if active_tab == 'recent' else current_data %} -{% set active_error = recent_error if active_tab == 'recent' else current_error %} -{% set active_recent = active_tab == 'recent' %} -{% set active_hook = "octa_client.sessions.list(recent: true)" if active_recent else "octa_client.sessions.list" %} -
-
-
-

Sessions

-

- octa_client.sessions.list for discovery, with stop actions wired through octa_client.services.session(uuid).stop. -

-
-
- {% if playground_mock_active %} - Mock: {{ playground_mock_scenario }} - {% endif %} - Refetch -
-
- - {% if stop_notice %} -
-
- -
-
Stop request submitted
-

{{ stop_notice }}

- {% if stop_request_id %} -

request_id: {{ stop_request_id }}

- {% endif %} -
-
-
- {% endif %} - - {% if stop_error %} -
-
- -
-
Stop request failed
-

{{ stop_error }}

-
-
-
- {% endif %} - -
- - -
-
- {{ active_hook }} - status: {{ 'error' if active_error else 'success' }} -
- - {% if active_error %} - {% set error = active_error %} - {% include "shared/error_card.html" %} - {% elif active_data is not none %} - {% if not active_data %} -
No {{ 'recent' if active_recent else 'active' }} sessions found.
- {% else %} -
-
{{ active_data|length }} session{% if active_data|length != 1 %}s{% endif %}
- {% set sessions = active_data %} - {% set recent = active_recent %} - {% include "shared/sessions_table.html" %} -
- {% endif %} - {% else %} -
No response available.
- {% endif %} -
-
- - {% if active_data and not active_error %} -
-
Raw JSON
- {% set data = active_data %} - {% set max_height = "max-h-80" %} - {% include "shared/json_viewer.html" %} -
- {% endif %} -
- -{% if stop_target %} -
-
-

Stop session?

-

This will stop session {{ stop_target.get("uuid") }}. This action cannot be undone.

-
- Cancel -
- - - - -
-
-
-
-{% endif %} -{% endblock %} diff --git a/playground/templates/shared/error_card.html b/playground/templates/shared/error_card.html deleted file mode 100644 index 17b00e3..0000000 --- a/playground/templates/shared/error_card.html +++ /dev/null @@ -1,29 +0,0 @@ -{% set max_height = max_height or "max-h-72" %} -{% set payload = error_payload(error) %} -
-
- -
-
{{ payload["class"] }}
-

{{ payload["message"] or "Request failed" }}

-
-
- -
-
-
Status
-
{{ payload.get("status", "—") }}
-
-
-
Request ID
-
{{ payload.get("request_id", "—") }}
-
-
-
Type
-
{{ payload["class"] }}
-
-
- - {% set data = payload %} - {% include "shared/json_viewer.html" %} -
diff --git a/playground/templates/shared/json_viewer.html b/playground/templates/shared/json_viewer.html deleted file mode 100644 index 1a79b6a..0000000 --- a/playground/templates/shared/json_viewer.html +++ /dev/null @@ -1,7 +0,0 @@ -{% set max_height = max_height or "h-[42rem]" %} -
- -
{{ data|json_html }}
-
diff --git a/playground/templates/shared/node_detail_panel.html b/playground/templates/shared/node_detail_panel.html deleted file mode 100644 index 5fd7ee2..0000000 --- a/playground/templates/shared/node_detail_panel.html +++ /dev/null @@ -1,32 +0,0 @@ -{% set node_tab = request.query_params.get("node_tab", "summary") %} -{% set detail_node_id = node.get("id") or node_id %} -
-
-
-

Node #{{ detail_node_id }}

- octa_client.nodes.find({{ detail_node_id }}) -
- {% if close_path %} - Close detail - {% endif %} -
- - - - {% if node_tab == "raw" %} - {% set data = node %} - {% include "shared/json_viewer.html" %} - {% else %} -
- {% for label, value in node_summary_rows(node) %} -
- {{ label }} - {{ value }} -
- {% endfor %} -
- {% endif %} -
diff --git a/playground/templates/shared/resource_panel.html b/playground/templates/shared/resource_panel.html deleted file mode 100644 index d5e2b9c..0000000 --- a/playground/templates/shared/resource_panel.html +++ /dev/null @@ -1,39 +0,0 @@ -
-
-
-

{{ title }}

- {% if hook %} - {{ hook }} - {% endif %} -
- {% if badge %} - {{ badge }} - {% endif %} -
- - {% if locked %} -
- - - - {{ locked_copy }} -
- {% elif error %} - {% include "shared/error_card.html" %} - {% elif present(data) %} - - - {% if current_tab == "raw" %} - {% include "shared/json_viewer.html" %} - {% else %} - {{ formatted|safe }} - {% endif %} - {% else %} -
- {{ empty_copy }} -
- {% endif %} -
diff --git a/playground/templates/shared/sessions_table.html b/playground/templates/shared/sessions_table.html deleted file mode 100644 index 59dc1c8..0000000 --- a/playground/templates/shared/sessions_table.html +++ /dev/null @@ -1,53 +0,0 @@ -{% set show_service = show_service_column(sessions) %} -
- - - - - - {% if show_service %} - - {% endif %} - - - - - {% if not recent %} - - {% endif %} - - {% if not recent %} - - {% endif %} - - - - {% for session in sessions %} - - - - {% if show_service %} - - {% endif %} - - - - - {% if not recent %} - - {% endif %} - - {% if not recent %} - - {% endif %} - - {% endfor %} - -
SessionNode IDServiceStatusStartedDurationTraffic ↓/↑IPAmountAction
{{ truncate_value(session.get("uuid")) }}#{{ session.get("node_id") or "—" }} - {{ session_service_label(session.get("service")) }} - - {{ session_status_label(session, recent) }} - {{ format_datetime(session.get("started_at")) }}{{ format_duration(session.get("duration")) }}{{ format_bytes(session.get("rx")) }} / {{ format_bytes(session.get("tx")) }}{{ session.get("public_ip") or session.get("ip") or "—" }}{{ session.get("charge_amount") or "—" }} - Stop -
-
diff --git a/playground/views.py b/playground/views.py deleted file mode 100644 index e580eb1..0000000 --- a/playground/views.py +++ /dev/null @@ -1,278 +0,0 @@ -from __future__ import annotations - -from datetime import datetime, timezone -from typing import Any - -from octaspace.config import ClientConfig -from octaspace.errors import APIError, OctaSpaceError -from octaspace.response import APIResponse - -from .state import PlaygroundSettings - -# --------------------------------------------------------------------------- -# Response helpers -# --------------------------------------------------------------------------- - -def response_data(value: APIResponse | OctaSpaceError | None) -> Any: - return value.data if isinstance(value, APIResponse) else None - - -def response_error(value: APIResponse | OctaSpaceError | None) -> OctaSpaceError | None: - return value if isinstance(value, OctaSpaceError) else None - - -def response_status(value: APIResponse | OctaSpaceError | None) -> str: - if isinstance(value, APIResponse): - return str(value.status_code) - if isinstance(value, APIError): - return str(value.status_code) - return "—" - - -def response_request_id(value: APIResponse | OctaSpaceError | None) -> str: - if isinstance(value, APIResponse): - return value.request_id or "—" - if isinstance(value, APIError): - return value.request_id or "—" - return "—" - - -# --------------------------------------------------------------------------- -# Node helpers -# --------------------------------------------------------------------------- - -def _node_gpu_entries(node: dict[str, Any]) -> list[dict[str, Any]]: - gpus = node.get("system", {}).get("gpus") or node.get("data", {}).get("gpus") or [] - return [item for item in gpus if isinstance(item, dict)] - - -def node_location(node: dict[str, Any]) -> str: - parts = [node.get("location", {}).get("city"), node.get("location", {}).get("country")] - rendered = ", ".join(p for p in parts if p) - return rendered or "—" - - -def node_gpu_model(node: dict[str, Any]) -> str: - entries = _node_gpu_entries(node) - return ( - (entries[0].get("model") if entries else None) - or node.get("system", {}).get("gpu") - or node.get("system", {}).get("gpu_model") - or "—" - ) - - -def node_gpu_count(node: dict[str, Any]) -> int: - return len(_node_gpu_entries(node)) - - -def node_ram_label(node: dict[str, Any]) -> str: - ram = ( - node.get("system", {}).get("memory", {}).get("size") - or node.get("system", {}).get("memory") - or node.get("data", {}).get("memory_size") - or node.get("system", {}).get("ram") - ) - if isinstance(ram, (int, float)) and not isinstance(ram, bool) and ram > 1024: - return f"{round(float(ram) / 1024 / 1024 / 1024)} GB" - if ram not in (None, ""): - return f"{ram} GB" - return "—" - - -def node_summary_rows(node: dict[str, Any]) -> list[tuple[str, str]]: - entries = _node_gpu_entries(node) - return [ - ("State", str(node.get("state") or node.get("status") or "—")), - ("IP", str(node.get("ip") or node.get("public_ip") or "—")), - ("Location", node_location(node)), - ("CPU", str( - node.get("data", {}).get("cpu_model_name") - or node.get("system", {}).get("cpu_model_name") - or node.get("system", {}).get("cpu") - or "—" - )), - ("Cores", str( - node.get("data", {}).get("cpu_cores") - or node.get("system", {}).get("cpu_cores") - or "—" - )), - ("RAM", node_ram_label(node)), - ("CUDA", str( - node.get("data", {}).get("cuda_version") - or node.get("system", {}).get("cuda_version") - or "—" - )), - ("OS", str( - node.get("data", {}).get("os_version") - or node.get("system", {}).get("os") - or "—" - )), - ("Driver", str( - node.get("data", {}).get("gpu_driver_version") - or node.get("system", {}).get("gpu_driver_version") - or "—" - )), - ("Uptime", format_duration(node.get("data", {}).get("uptime") or node.get("uptime"))), - ("GPU vendor", str( - node.get("data", {}).get("gpu_vendor") - or (entries[0].get("vendor") if entries else None) - or "—" - )), - ] - - -# --------------------------------------------------------------------------- -# App helpers -# --------------------------------------------------------------------------- - -def app_ports_count(app: dict[str, Any]) -> int: - return len(app.get("ports") or []) + len(app.get("http_ports") or []) - - -# --------------------------------------------------------------------------- -# Session helpers -# --------------------------------------------------------------------------- - -def session_service_label(service: Any) -> str: - return {"mr": "RENTAL", "render": "RENDER", "vpn": "VPN"}.get(str(service), "—") - - -def session_service_badge_class(service: Any) -> str: - return { - "mr": "border border-sky-500/30 bg-sky-900/30 text-sky-400", - "render": "border border-orange-500/30 bg-orange-900/30 text-orange-400", - "vpn": "border border-indigo-500/30 bg-indigo-900/30 text-indigo-400", - }.get(str(service), "border border-slate-600/40 bg-slate-700/50 text-slate-400") - - -def session_status_label(session: dict[str, Any], recent: bool) -> str: - if recent and session.get("termination_reason") is not None: - return "Normal" if int(session.get("termination_reason", 0)) in {0, 1} else "Error" - if session.get("is_ready"): - return "Ready" - if session.get("progress"): - return str(session["progress"]) - return str(session.get("status") or "Starting") - - -def session_status_badge_class(session: dict[str, Any], recent: bool) -> str: - if recent and session.get("termination_reason") is not None: - if int(session.get("termination_reason", 0)) in {0, 1}: - return "border border-emerald-500/30 bg-emerald-500/15 text-emerald-400" - return "border border-rose-500/30 bg-rose-500/15 text-rose-400" - if session.get("is_ready"): - return "border border-emerald-500/30 bg-emerald-500/15 text-emerald-400" - return "border border-amber-500/30 bg-amber-500/15 text-amber-400" - - -def show_service_column(sessions: list[dict[str, Any]]) -> bool: - return any(s.get("service") for s in sessions) - - -def summarize_payload(payload: Any) -> dict[str, Any]: - if isinstance(payload, list): - sample_keys = ( - list(payload[0].keys())[:8] - if payload and isinstance(payload[0], dict) - else None - ) - return {"type": "array", "count": len(payload), "sample_keys": sample_keys} - if isinstance(payload, dict): - return {"type": "object", "keys": list(payload.keys())[:12]} - return {"type": type(payload).__name__, "value": payload} - - -# --------------------------------------------------------------------------- -# Transport / config labels -# --------------------------------------------------------------------------- - -def transport_label(settings: PlaygroundSettings) -> str: - return settings.transport_label - - -def transport_description(settings: PlaygroundSettings) -> str: - return settings.transport_description - - -def timeout_label(settings: PlaygroundSettings) -> str: - config = ClientConfig(api_key=settings.api_key or None, base_url=settings.base_url) - return f"{config.open_timeout}s / {config.read_timeout}s" - - -# --------------------------------------------------------------------------- -# Formatting helpers -# --------------------------------------------------------------------------- - -def playground_present(value: Any) -> bool: - return value is not None - - -def truncate_value(value: Any, leading: int = 8, trailing: int = 5) -> str: - s = str(value) - if len(s) <= leading + trailing + 1: - return s - return f"{s[:leading]}…{s[-trailing:]}" - - -def format_duration(value: Any) -> str: - try: - seconds = int(float(value)) - except (TypeError, ValueError): - return "—" - if seconds <= 0: - return "—" - hours, remainder = divmod(seconds, 3600) - minutes, secs = divmod(remainder, 60) - if hours > 0: - return f"{hours}h {minutes}m" - if minutes > 0: - return f"{minutes}m {secs}s" - return f"{secs}s" - - -def format_bytes(value: Any) -> str: - try: - bv = float(value) - except (TypeError, ValueError): - return "0 B" - if bv <= 0: - return "0 B" - units = ["B", "KB", "MB", "GB", "TB"] - index = 0 - while bv >= 1024 and index < len(units) - 1: - bv /= 1024.0 - index += 1 - return f"{bv:.1f} {units[index]}" - - -def format_datetime(value: Any) -> str: - parsed = _coerce_datetime(value) - if parsed is None: - return value if isinstance(value, str) and value else "—" - return parsed.astimezone().strftime("%b %-d, %H:%M") - - -def format_short_time(value: Any) -> str: - parsed = _coerce_datetime(value) - if parsed is None: - return value if isinstance(value, str) and value else "—" - return parsed.astimezone().strftime("%H:%M:%S") - - -def _coerce_datetime(value: Any) -> datetime | None: - if value in (None, ""): - return None - if isinstance(value, datetime): - return value if value.tzinfo else value.replace(tzinfo=timezone.utc) - if isinstance(value, (int, float)) and not isinstance(value, bool): - numeric = float(value) - secs = numeric / 1000.0 if numeric > 10_000_000_000 else numeric - return datetime.fromtimestamp(secs, tz=timezone.utc) - if isinstance(value, str): - try: - parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) - except ValueError: - return None - return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc) - return None diff --git a/pyproject.toml b/pyproject.toml index f4ed0ab..47c9bbe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,21 +35,12 @@ Repository = "https://github.com/octaspace/python-sdk" Issues = "https://github.com/octaspace/python-sdk/issues" Changelog = "https://github.com/octaspace/python-sdk/blob/main/CHANGELOG.md" -[project.scripts] -octaspace-smoke = "octaspace.__main__:smoke" - [dependency-groups] test = [ "pytest>=8.0", "pytest-cov>=5.0", "respx>=0.21", ] -playground = [ - "fastapi>=0.115", - "jinja2>=3.1", - "python-multipart>=0.0.9", - "uvicorn>=0.30", -] lint = [ "ruff>=0.11", ] @@ -57,7 +48,6 @@ typecheck = [ "mypy>=1.11", ] dev = [ - { include-group = "playground" }, { include-group = "test" }, { include-group = "lint" }, { include-group = "typecheck" }, @@ -66,6 +56,15 @@ dev = [ [tool.hatch.version] path = "src/octaspace/__about__.py" +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/README.md", + "/CHANGELOG.md", + "/LICENSE", + "/pyproject.toml", +] + [tool.hatch.build.targets.wheel] packages = ["src/octaspace"] @@ -79,7 +78,6 @@ select = ["E", "F", "I", "B", "UP", "SIM", "RUF"] [tool.pytest.ini_options] testpaths = ["tests"] -pythonpath = ["."] addopts = "--strict-markers --cov=octaspace --cov-report=term-missing" [tool.mypy] diff --git a/src/octaspace/__main__.py b/src/octaspace/__main__.py deleted file mode 100644 index d1cd7d7..0000000 --- a/src/octaspace/__main__.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import annotations - -import json - -from . import OctaSpaceClient -from .config import ClientConfig -from .response import APIResponse - - -def smoke() -> None: - config = ClientConfig.from_env() - with OctaSpaceClient(config=config) as client: - payload: dict[str, object] = { - "network": _response_payload(client.network.info()), - } - if config.api_key: - payload["account_balance"] = _response_payload(client.accounts.balance()) - print(json.dumps(payload, indent=2, ensure_ascii=False)) - - -def _response_payload(response: APIResponse) -> object: - return response.data if response.data is not None else response.body.decode("utf-8") - - -if __name__ == "__main__": # pragma: no cover - smoke() diff --git a/tests/playground/test_app.py b/tests/playground/test_app.py deleted file mode 100644 index c662af2..0000000 --- a/tests/playground/test_app.py +++ /dev/null @@ -1,167 +0,0 @@ -from __future__ import annotations - -from fastapi.testclient import TestClient -from playground.app import create_app -from playground.state import PlaygroundState - - -def build_client(*, mock_scenario: str = "fixtures", api_key: str = "") -> TestClient: - state = PlaygroundState() - state.update_settings({"mock_scenario": mock_scenario, "api_key": api_key}) - app = create_app(state) - return TestClient(app) - - -def csrf_token(client: TestClient) -> str: - return str(client.app.state.csrf_token) - - -def test_sidebar_matches_ruby_dummy_navigation() -> None: - with build_client() as client: - response = client.get("/playground/dashboard") - - assert response.status_code == 200 - for label in ( - "Dashboard", - "Network", - "Apps", - "Nodes", - "Sessions", - "Services", - "Idle Jobs", - "Diagnostics", - ): - assert label in response.text - assert 'href="/playground/account"' not in response.text - assert "Request Log" in response.text - assert ">Requests<" not in response.text - - -def test_dashboard_locks_account_panels_without_api_key() -> None: - with build_client() as client: - response = client.get("/playground/dashboard") - - assert response.status_code == 200 - assert "octa_client.network.info" in response.text - assert "Enter an API key in the sidebar to load this resource" in response.text - assert "user@example.com" not in response.text - - -def test_account_page_renders_fixture_data_with_api_key() -> None: - with build_client(api_key="test-token-1234") as client: - response = client.get("/playground/account") - - assert response.status_code == 200 - assert "user@example.com" in response.text - assert "123.45" in response.text - - -def test_apps_page_contains_table_and_diagnostics_link() -> None: - with build_client() as client: - response = client.get("/playground/apps") - - assert response.status_code == 200 - assert "Use in MR Create" in response.text - assert "octa_client.apps.list" in response.text - - -def test_nodes_page_can_open_detail_panel() -> None: - with build_client() as client: - response = client.get("/playground/nodes?selected_id=1") - - assert response.status_code == 200 - assert "Node #1" in response.text - assert "Close detail" in response.text - - -def test_sessions_page_has_tabs_and_stop_modal() -> None: - with build_client() as client: - response = client.get("/playground/sessions?tab=current&stop_uuid=sess-abc-123") - - assert response.status_code == 200 - assert "Current" in response.text - assert "Recent" in response.text - assert "Stop session?" in response.text - - -def test_services_page_has_expected_tabs_and_notice() -> None: - with build_client() as client: - response = client.get("/playground/services") - - assert response.status_code == 200 - assert "Machine Rental" in response.text - assert "Render" in response.text - assert "VPN" in response.text - assert "use Diagnostics for manual request inspection" in response.text - - -def test_idle_jobs_page_renders_form_and_results() -> None: - with build_client() as client: - response = client.get("/playground/idle-jobs?node_id=1&job_id=42") - - assert response.status_code == 200 - assert "Load Job" in response.text - assert "octa_client.idle_jobs.find" in response.text - assert "octa_client.idle_jobs.logs" in response.text - - -def test_diagnostics_run_and_request_log_clear() -> None: - with build_client() as client: - token = csrf_token(client) - response = client.post( - "/playground/diagnostics/run", - data={ - "csrf_token": token, - "call": "idle_jobs.find", - "payload_json": '{"node_id": 1, "job_id": 42}', - }, - ) - cleared = client.post( - "/playground/request-log", - data={"csrf_token": token}, - headers={"referer": "http://testserver/playground/diagnostics"}, - follow_redirects=True, - ) - - assert response.status_code == 200 - assert "octa_client.idle_jobs.find(node_id:, job_id:)" in response.text - assert "Success" in response.text - assert "No requests yet" in cleared.text - - -def test_diagnostics_invalid_json_is_reported_inline() -> None: - with build_client() as client: - token = csrf_token(client) - response = client.post( - "/playground/diagnostics/run", - data={ - "csrf_token": token, - "call": "services.session.stop", - "payload_json": "{bad json", - }, - ) - - assert response.status_code == 200 - assert "PayloadError" in response.text - - -def test_smoke_suite_renders_report() -> None: - with build_client(api_key="test-token-1234") as client: - response = client.post( - "/playground/diagnostics/smoke", - data={"csrf_token": csrf_token(client)}, - ) - - assert response.status_code == 200 - assert "SDK Smoke" in response.text - assert "octa_client.network.info" in response.text - - -def test_playground_post_routes_require_csrf_token() -> None: - with build_client() as client: - response = client.post( - "/playground/diagnostics/run", - data={"call": "network.info", "payload_json": ""}, - ) - - assert response.status_code == 403 diff --git a/tests/test_main.py b/tests/test_main.py deleted file mode 100644 index e298951..0000000 --- a/tests/test_main.py +++ /dev/null @@ -1,91 +0,0 @@ -from __future__ import annotations - -import json - -import httpx - -from octaspace.__main__ import smoke -from octaspace.config import ClientConfig -from octaspace.response import APIResponse - - -class _FakeNetwork: - def __init__(self, response) -> None: - self._response = response - - def info(self): - return self._response - - -class _FakeAccounts: - def __init__(self, response) -> None: - self._response = response - - def balance(self): - return self._response - - -class _FakeClient: - def __init__(self, *, config, network_response, balance_response=None) -> None: - self.config = config - self.network = _FakeNetwork(network_response) - self.accounts = _FakeAccounts(balance_response or network_response) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc, tb) -> None: - del exc_type, exc, tb - - -def test_smoke_prints_json_payload(monkeypatch, capsys) -> None: - config = ClientConfig() - response = APIResponse.from_httpx(httpx.Response(200, json={"ok": True})) - - monkeypatch.setattr("octaspace.__main__.ClientConfig.from_env", lambda: config) - monkeypatch.setattr( - "octaspace.__main__.OctaSpaceClient", - lambda config: _FakeClient(config=config, network_response=response), - ) - - smoke() - - assert json.loads(capsys.readouterr().out) == {"network": {"ok": True}} - - -def test_smoke_prints_text_when_response_is_not_json(monkeypatch, capsys) -> None: - response = APIResponse.from_httpx( - httpx.Response(200, content=b"plain-text", headers={"Content-Type": "text/plain"}) - ) - monkeypatch.setattr("octaspace.__main__.ClientConfig.from_env", lambda: ClientConfig()) - monkeypatch.setattr( - "octaspace.__main__.OctaSpaceClient", - lambda config: _FakeClient(config=config, network_response=response), - ) - - smoke() - - assert json.loads(capsys.readouterr().out) == {"network": "plain-text"} - - -def test_smoke_includes_account_balance_when_api_key_is_present(monkeypatch, capsys) -> None: - config = ClientConfig(api_key="env-key") - network_response = APIResponse.from_httpx(httpx.Response(200, json={"ok": True})) - balance_response = APIResponse.from_httpx(httpx.Response(200, json={"balance": 42})) - - monkeypatch.setattr("octaspace.__main__.ClientConfig.from_env", lambda: config) - monkeypatch.setattr( - "octaspace.__main__.OctaSpaceClient", - lambda config: _FakeClient( - config=config, - network_response=network_response, - balance_response=balance_response, - ), - ) - - smoke() - - assert json.loads(capsys.readouterr().out) == { - "network": {"ok": True}, - "account_balance": {"balance": 42}, - } diff --git a/uv.lock b/uv.lock index 168cb6f..0cc0115 100644 --- a/uv.lock +++ b/uv.lock @@ -2,24 +2,6 @@ version = 1 revision = 3 requires-python = ">=3.10" -[[package]] -name = "annotated-doc" -version = "0.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, -] - [[package]] name = "anyio" version = "4.13.0" @@ -43,18 +25,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] -[[package]] -name = "click" -version = "8.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -194,22 +164,6 @@ 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" }, ] -[[package]] -name = "fastapi" -version = "0.135.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-doc" }, - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f7/e6/7adb4c5fa231e82c35b8f5741a9f2d055f520c29af5546fd70d3e8e1cd2e/fastapi-0.135.3.tar.gz", hash = "sha256:bd6d7caf1a2bdd8d676843cdcd2287729572a1ef524fc4d65c17ae002a1be654", size = 396524, upload-time = "2026-04-01T16:23:58.188Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/a4/5caa2de7f917a04ada20018eccf60d6cc6145b0199d55ca3711b0fc08312/fastapi-0.135.3-py3-none-any.whl", hash = "sha256:9b0f590c813acd13d0ab43dd8494138eb58e484bfac405db1f3187cfc5810d98", size = 117734, upload-time = "2026-04-01T16:23:59.328Z" }, -] - [[package]] name = "h11" version = "0.16.0" @@ -265,18 +219,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] -[[package]] -name = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, -] - [[package]] name = "librt" version = "0.9.0" @@ -362,91 +304,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/47/7d70414bcdbb3bc1f458a8d10558f00bbfdb24e5a11740fc8197e12c3255/librt-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61", size = 50009, upload-time = "2026-04-09T16:06:07.995Z" }, ] -[[package]] -name = "markupsafe" -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" }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, - { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { 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 = "mypy" version = "1.20.1" @@ -523,25 +380,15 @@ dependencies = [ [package.dev-dependencies] dev = [ - { name = "fastapi" }, - { name = "jinja2" }, { name = "mypy" }, { name = "pytest" }, { name = "pytest-cov" }, - { name = "python-multipart" }, { name = "respx" }, { name = "ruff" }, - { name = "uvicorn" }, ] lint = [ { name = "ruff" }, ] -playground = [ - { name = "fastapi" }, - { name = "jinja2" }, - { name = "python-multipart" }, - { name = "uvicorn" }, -] test = [ { name = "pytest" }, { name = "pytest-cov" }, @@ -556,23 +403,13 @@ requires-dist = [{ name = "httpx", specifier = ">=0.27,<1.0" }] [package.metadata.requires-dev] dev = [ - { name = "fastapi", specifier = ">=0.115" }, - { name = "jinja2", specifier = ">=3.1" }, { name = "mypy", specifier = ">=1.11" }, { name = "pytest", specifier = ">=8.0" }, { name = "pytest-cov", specifier = ">=5.0" }, - { name = "python-multipart", specifier = ">=0.0.9" }, { name = "respx", specifier = ">=0.21" }, { name = "ruff", specifier = ">=0.11" }, - { name = "uvicorn", specifier = ">=0.30" }, ] lint = [{ name = "ruff", specifier = ">=0.11" }] -playground = [ - { name = "fastapi", specifier = ">=0.115" }, - { name = "jinja2", specifier = ">=3.1" }, - { name = "python-multipart", specifier = ">=0.0.9" }, - { name = "uvicorn", specifier = ">=0.30" }, -] test = [ { name = "pytest", specifier = ">=8.0" }, { name = "pytest-cov", specifier = ">=5.0" }, @@ -607,137 +444,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] -[[package]] -name = "pydantic" -version = "2.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/84/6b/69fd5c7194b21ebde0f8637e2a4ddc766ada29d472bfa6a5ca533d79549a/pydantic-2.13.0.tar.gz", hash = "sha256:b89b575b6e670ebf6e7448c01b41b244f471edd276cd0b6fe02e7e7aca320070", size = 843468, upload-time = "2026-04-13T10:51:35.571Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/01/d7/c3a52c61f5b7be648e919005820fbac33028c6149994cd64453f49951c17/pydantic-2.13.0-py3-none-any.whl", hash = "sha256:ab0078b90da5f3e2fd2e71e3d9b457ddcb35d0350854fbda93b451e28d56baaf", size = 471872, upload-time = "2026-04-13T10:51:33.343Z" }, -] - -[[package]] -name = "pydantic-core" -version = "2.46.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6f/0a/9414cddf82eda3976b14048cc0fa8f5b5d1aecb0b22e1dcd2dbfe0e139b1/pydantic_core-2.46.0.tar.gz", hash = "sha256:82d2498c96be47b47e903e1378d1d0f770097ec56ea953322f39936a7cf34977", size = 471441, upload-time = "2026-04-13T09:06:33.813Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/17/fd3ba2f035ac7b3a1ae0c55e5c0f6eb5275e87ad80a9b277cb2e70317e2c/pydantic_core-2.46.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d449eae37d6b066d8a8be0e3a7d7041712d6e9152869e7d03c203795aae44ed", size = 2122942, upload-time = "2026-04-13T09:04:32.413Z" }, - { url = "https://files.pythonhosted.org/packages/01/b5/214cb10e4050f430f383a21496087c1e51d583eec3c884b0e5f55c34eb69/pydantic_core-2.46.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4f7bfc1ffee4ddc03c2db472c7607a238dbbf76f7f64104fc6a623d47fb8e310", size = 1949068, upload-time = "2026-04-13T09:05:28.803Z" }, - { url = "https://files.pythonhosted.org/packages/b4/ab/8ab4ec2a879eead4bb51c3e9af65583e16cc504867e808909cd4f991a5ae/pydantic_core-2.46.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a30f5d1d4e1c958b44b5c777a0d1adcd930429f35101e4780281ffbe11103925", size = 1974362, upload-time = "2026-04-13T09:05:26.894Z" }, - { url = "https://files.pythonhosted.org/packages/8f/dd/dc8ef47e18ddcab169af68b3c11648e1ef85c56aa18e2f96312cc5442404/pydantic_core-2.46.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f68e12d2de32ac6313a7d3854f346d71731288184fbbfc9004e368714244d2cd", size = 2043754, upload-time = "2026-04-13T09:04:54.637Z" }, - { url = "https://files.pythonhosted.org/packages/7f/52/69195c8f6549d2b1b9ce0efbb9bf169b47dcb9a60f81ff53a67cb22d8fc7/pydantic_core-2.46.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7d1a058fb5aff8a1a221e7d8a0cf5b0133d069b2f293cb05f174c61bc7cdac34", size = 2230099, upload-time = "2026-04-13T09:04:44.37Z" }, - { url = "https://files.pythonhosted.org/packages/2a/41/48c8e7709604a4230f86f77bc17e1eb575e0894831f2c3beaecb3e8f7583/pydantic_core-2.46.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbd01128431f355e309267283e37e23704f24558e9059d930e213a377b1be919", size = 2293730, upload-time = "2026-04-13T09:04:27.583Z" }, - { url = "https://files.pythonhosted.org/packages/08/ab/f3bc576d37eb3036f7b1b2721ab0f89e4684fab48e1de1d0eca0dfef7469/pydantic_core-2.46.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7747a50d9f75fe264b9e2091a2f462a7dd400add8723a87a75240106b6f4d949", size = 2095380, upload-time = "2026-04-13T09:04:45.929Z" }, - { url = "https://files.pythonhosted.org/packages/fe/69/0f6e5bd9c5594b41deb91029ad0b16ffe5a270dd412033dd1135a40bbfa3/pydantic_core-2.46.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:1d9b841e9c82a9cdf397a720bb8a4f2d6da6780204e1eb07c2d90c4b5b791b0d", size = 2140115, upload-time = "2026-04-13T09:07:00.944Z" }, - { url = "https://files.pythonhosted.org/packages/28/7c/79cfc18d352797b84a7c5b27171d6557121843729bc637a90550d08370fd/pydantic_core-2.46.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:61d0f5951b7b86ec24e24fe0c5a2cce7c360830026dfbe004954e8fac9918b95", size = 2183044, upload-time = "2026-04-13T09:03:58.106Z" }, - { url = "https://files.pythonhosted.org/packages/59/bc/701b17bf7fd375e59e03838cffe8f6893498503b7d412d577ffd92dab56c/pydantic_core-2.46.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:aec0be48d2555ceac04905ffb8f2bb7e55a56644858891196191827b6fc656b7", size = 2185277, upload-time = "2026-04-13T09:05:52.482Z" }, - { url = "https://files.pythonhosted.org/packages/c3/43/ad927b8861ab787b4189ddb2dd70ebcdc20c5a4baf52df94934d6f87d730/pydantic_core-2.46.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:2c1ec2ced44a8a479d71a14f5be35461360acd388987873a8e0a02f7f81c8ec2", size = 2329998, upload-time = "2026-04-13T09:05:54.803Z" }, - { url = "https://files.pythonhosted.org/packages/47/33/ad11d56b97ea986f991da998d551a7513d19c06ed05a529e86520430e10e/pydantic_core-2.46.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5e157a25eed281f5e40119078e3dbf698c28b3d88ff0176eea3dd37191447b8d", size = 2369004, upload-time = "2026-04-13T09:05:14.052Z" }, - { url = "https://files.pythonhosted.org/packages/16/d1/a9a28a122f1227dc13fdd361d77a3f2df4aee64e4ac5693d7ce74a8ecfa4/pydantic_core-2.46.0-cp310-cp310-win32.whl", hash = "sha256:311929d9bfdb9fdbaf28beb39d88a1e36ca6dc5424ceca6d3bf81c9e1da2313c", size = 1982879, upload-time = "2026-04-13T09:05:19.277Z" }, - { url = "https://files.pythonhosted.org/packages/94/9a/52988a743cf7a9d84861e380c6a5496589aebbc3592d9ecdecb13c6bd0a2/pydantic_core-2.46.0-cp310-cp310-win_amd64.whl", hash = "sha256:60edfb53b13fbe7be9bb51447016b7bcd8772beb8ca216873be33e9d11b2c8e8", size = 2068907, upload-time = "2026-04-13T09:03:59.541Z" }, - { url = "https://files.pythonhosted.org/packages/ce/43/9bc38d43a6a48794209e4eb6d61e9c68395f69b7949f66842854b0cd1344/pydantic_core-2.46.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0027da787ae711f7fbd5a76cb0bb8df526acba6c10c1e44581de1b838db10b7b", size = 2121004, upload-time = "2026-04-13T09:05:17.531Z" }, - { url = "https://files.pythonhosted.org/packages/8c/1d/f43342b7107939b305b5e4efeef7d54e267a5ef51515570a5c1d77726efb/pydantic_core-2.46.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:63e288fc18d7eaeef5f16c73e65c4fd0ad95b25e7e21d8a5da144977b35eb997", size = 1947505, upload-time = "2026-04-13T09:04:48.975Z" }, - { url = "https://files.pythonhosted.org/packages/4a/cd/ccf48cbbcaf0d99ba65969459ebfbf7037600b2cfdcca3062084dd83a008/pydantic_core-2.46.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:080a3bdc6807089a1fe1fbc076519cea287f1a964725731d80b49d8ecffaa217", size = 1973301, upload-time = "2026-04-13T09:05:42.149Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ff/a7bb1e7a762fb1f40ad5ef4e6a92c012864a017b7b1fdfb71cf91faa8b73/pydantic_core-2.46.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c065f1c3e54c3e79d909927a8cb48ccbc17b68733552161eba3e0628c38e5d19", size = 2042208, upload-time = "2026-04-13T09:05:32.591Z" }, - { url = "https://files.pythonhosted.org/packages/ea/64/d3f11c6f6ace71526f3b03646df95eaab3f21edd13e00daae3f20f4e5a09/pydantic_core-2.46.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7e2db58ab46cfe602d4255381cce515585998c3b6699d5b1f909f519bc44a5aa", size = 2229046, upload-time = "2026-04-13T09:04:18.59Z" }, - { url = "https://files.pythonhosted.org/packages/d0/64/93db9a63cce71630c58b376d63de498aa93cb341c72cd5f189b5c08f5c28/pydantic_core-2.46.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c660974890ec1e4c65cff93f5670a5f451039f65463e9f9c03ad49746b49fc78", size = 2292138, upload-time = "2026-04-13T09:04:13.816Z" }, - { url = "https://files.pythonhosted.org/packages/e9/96/936fccce22f1f2ae8b2b694de651c2c929847be5f701c927a0bb3b1eb679/pydantic_core-2.46.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3be91482a8db77377c902cca87697388a4fb68addeb3e943ac74f425201a099", size = 2093333, upload-time = "2026-04-13T09:05:15.729Z" }, - { url = "https://files.pythonhosted.org/packages/75/76/c325e7fda69d589e26e772272044fe704c7e525c47d0d32a74f8345ac657/pydantic_core-2.46.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:1c72de82115233112d70d07f26a48cf6996eb86f7e143423ec1a182148455a9d", size = 2138802, upload-time = "2026-04-13T09:03:51.142Z" }, - { url = "https://files.pythonhosted.org/packages/c0/6f/ccaa2ff7d53a017b66841e2d38edd1f38d19ae1a2d0c5efee17f2d432229/pydantic_core-2.46.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7904e58768cd79304b992868d7710bfc85dc6c7ed6163f0f68dbc1dcd72dc231", size = 2181358, upload-time = "2026-04-13T09:04:30.737Z" }, - { url = "https://files.pythonhosted.org/packages/6c/71/0c4b6303e92d63edcb81f5301695cdf70bb351775b4733eea65acdac8384/pydantic_core-2.46.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1af8d88718005f57bb4768f92f4ff16bf31a747d39dfc919b22211b84e72c053", size = 2183985, upload-time = "2026-04-13T09:04:06.792Z" }, - { url = "https://files.pythonhosted.org/packages/71/eb/f6bf255de38a4393aaa10bff224e882b630576bc26ebfb401e42bb965092/pydantic_core-2.46.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:a5b891301b02770a5852253f4b97f8bd192e5710067bc129e20d43db5403ede2", size = 2328559, upload-time = "2026-04-13T09:06:14.143Z" }, - { url = "https://files.pythonhosted.org/packages/f2/71/93895a1545f50823a24b21d7761c2bd1b1afea7a6ddc019787caec237361/pydantic_core-2.46.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:48b671fe59031fd9754c7384ac05b3ed47a0cccb7d4db0ec56121f0e6a541b90", size = 2367466, upload-time = "2026-04-13T09:05:59.613Z" }, - { url = "https://files.pythonhosted.org/packages/78/39/62331b3e71f41fb13d486621e2aec49900ba56567fb3a0ae5999fded0005/pydantic_core-2.46.0-cp311-cp311-win32.whl", hash = "sha256:0a52b7262b6cc67033823e9549a41bb77580ac299dc964baae4e9c182b2e335c", size = 1981367, upload-time = "2026-04-13T09:07:37.563Z" }, - { url = "https://files.pythonhosted.org/packages/9f/51/caac70958420e2d6115962f550676df59647c11f96a44c2fcb61662fcd16/pydantic_core-2.46.0-cp311-cp311-win_amd64.whl", hash = "sha256:4103fea1beeef6b3a9fed8515f27d4fa30c929a1973655adf8f454dc49ee0662", size = 2065942, upload-time = "2026-04-13T09:06:37.873Z" }, - { url = "https://files.pythonhosted.org/packages/b2/cf/576b2a4eb5500a1a5da485613b1ea8bc0d7279b27e0426801574b284ae65/pydantic_core-2.46.0-cp311-cp311-win_arm64.whl", hash = "sha256:3137cd88938adb8e567c5e938e486adc7e518ffc96b4ae1ec268e6a4275704d7", size = 2052532, upload-time = "2026-04-13T09:06:03.697Z" }, - { url = "https://files.pythonhosted.org/packages/a7/d2/206c72ad47071559142a35f71efc29eb16448a4a5ae9487230ab8e4e292b/pydantic_core-2.46.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:66ccedb02c934622612448489824955838a221b3a35875458970521ef17b2f9c", size = 2117060, upload-time = "2026-04-13T09:04:47.443Z" }, - { url = "https://files.pythonhosted.org/packages/17/2c/7a53b33f91c8b77e696b1a6aa3bed609bf9374bdc0f8dcda681bc7d922b8/pydantic_core-2.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a44f27f4d2788ef9876ec47a43739b118c5904d74f418f53398f6ced3bbcacf2", size = 1951802, upload-time = "2026-04-13T09:05:34.591Z" }, - { url = "https://files.pythonhosted.org/packages/fc/20/90e548c1f6d38800ef11c915881525770ce270d8e5e887563ff046a08674/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26a1032bcce6ca4b4670eb3f7d8195bd0a8b8f255f1307823e217ca3cfa7c27", size = 1976621, upload-time = "2026-04-13T09:04:03.909Z" }, - { url = "https://files.pythonhosted.org/packages/20/3c/9c5810ca70b60c623488cdd80f7e9ee1a0812df81e97098b64788719860f/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b8d1412f725060527e56675904b17a2d421dddcf861eecf7c75b9dda47921a4", size = 2056721, upload-time = "2026-04-13T09:04:40.992Z" }, - { url = "https://files.pythonhosted.org/packages/1a/a3/d6e5f4cdec84278431c75540f90838c9d0a4dfe9402a8f3902073660ff28/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc3d1569edd859cabaa476cabce9eecd05049a7966af7b4a33b541bfd4ca1104", size = 2239634, upload-time = "2026-04-13T09:03:52.478Z" }, - { url = "https://files.pythonhosted.org/packages/46/42/ef58aacf330d8de6e309d62469aa1f80e945eaf665929b4037ac1bfcebc1/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:38108976f2d8afaa8f5067fd1390a8c9f5cc580175407cda636e76bc76e88054", size = 2315739, upload-time = "2026-04-13T09:05:04.971Z" }, - { url = "https://files.pythonhosted.org/packages/8b/86/c63b12fafa2d86a515bfd1840b39c23a49302f02b653161bf9c3a0566c50/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5a06d8ed01dad5575056b5187e5959b336793c6047920a3441ee5b03533836", size = 2098169, upload-time = "2026-04-13T09:07:27.151Z" }, - { url = "https://files.pythonhosted.org/packages/76/19/b5b33a2f6be4755b21a20434293c4364be255f4c1a108f125d101d4cc4ee/pydantic_core-2.46.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:04017ace142da9ce27cafd423a480872571b5c7e80382aec22f7d715ca8eb870", size = 2170830, upload-time = "2026-04-13T09:04:39.448Z" }, - { url = "https://files.pythonhosted.org/packages/99/ae/7559f99a29b7d440012ddb4da897359304988a881efaca912fd2f655652e/pydantic_core-2.46.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2629ad992ed1b1c012e6067f5ffafd3336fcb9b54569449fabb85621f1444ed3", size = 2203901, upload-time = "2026-04-13T09:04:01.048Z" }, - { url = "https://files.pythonhosted.org/packages/dd/0e/b0ef945a39aeb4ac58da316813e1106b7fbdfbf20ac141c1c27904355ac5/pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3068b1e7bd986aebc88f6859f8353e72072538dcf92a7fb9cf511a0f61c5e729", size = 2191789, upload-time = "2026-04-13T09:06:39.915Z" }, - { url = "https://files.pythonhosted.org/packages/90/f4/830484e07188c1236b013995818888ab93bab8fd88aa9689b1d8fd22220d/pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:1e366916ff69ff700aa9326601634e688581bc24c5b6b4f8738d809ec7d72611", size = 2344423, upload-time = "2026-04-13T09:05:12.252Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ba/e455c18cbdc333177af754e740be4fe9d1de173d65bbe534daf88da02ac0/pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:485a23e8f4618a1b8e23ac744180acde283fffe617f96923d25507d5cade62ec", size = 2384037, upload-time = "2026-04-13T09:06:24.503Z" }, - { url = "https://files.pythonhosted.org/packages/78/1f/b35d20d73144a41e78de0ae398e60fdd8bed91667daa1a5a92ab958551ba/pydantic_core-2.46.0-cp312-cp312-win32.whl", hash = "sha256:520940e1b702fe3b33525d0351777f25e9924f1818ca7956447dabacf2d339fd", size = 1967068, upload-time = "2026-04-13T09:05:23.374Z" }, - { url = "https://files.pythonhosted.org/packages/d1/84/4b6252e9606e8295647b848233cc4137ee0a04ebba8f0f9fb2977655b38c/pydantic_core-2.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:90d2048e0339fa365e5a66aefe760ddd3b3d0a45501e088bc5bc7f4ed9ff9571", size = 2071008, upload-time = "2026-04-13T09:05:21.392Z" }, - { url = "https://files.pythonhosted.org/packages/39/95/d08eb508d4d5560ccbd226ee5971e5ef9b749aba9b413c0c4ed6e406d4f6/pydantic_core-2.46.0-cp312-cp312-win_arm64.whl", hash = "sha256:a70247649b7dffe36648e8f34be5ce8c5fa0a27ff07b071ea780c20a738c05ce", size = 2036634, upload-time = "2026-04-13T09:05:48.299Z" }, - { url = "https://files.pythonhosted.org/packages/df/05/ab3b0742bad1d51822f1af0c4232208408902bdcfc47601f3b812e09e6c2/pydantic_core-2.46.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a05900c37264c070c683c650cbca8f83d7cbb549719e645fcd81a24592eac788", size = 2116814, upload-time = "2026-04-13T09:04:12.41Z" }, - { url = "https://files.pythonhosted.org/packages/98/08/30b43d9569d69094a0899a199711c43aa58fce6ce80f6a8f7693673eb995/pydantic_core-2.46.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de8e482fd4f1e3f36c50c6aac46d044462615d8f12cfafc6bebeaa0909eea22", size = 1951867, upload-time = "2026-04-13T09:04:02.364Z" }, - { url = "https://files.pythonhosted.org/packages/db/a0/bf9a1ba34537c2ed3872a48195291138fdec8fe26c4009776f00d63cf0c8/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c525ecf8a4cdf198327b65030a7d081867ad8e60acb01a7214fff95cf9832d47", size = 1977040, upload-time = "2026-04-13T09:06:16.088Z" }, - { url = "https://files.pythonhosted.org/packages/71/70/0ba03c20e1e118219fc18c5417b008b7e880f0e3fb38560ec4465984d471/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f14581aeb12e61542ce73b9bfef2bca5439d65d9ab3efe1a4d8e346b61838f9b", size = 2055284, upload-time = "2026-04-13T09:05:25.125Z" }, - { url = "https://files.pythonhosted.org/packages/58/cf/1e320acefbde7fb7158a9e5def55e0adf9a4634636098ce28dc6b978e0d3/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c108067f2f7e190d0dbd81247d789ec41f9ea50ccd9265a3a46710796ac60530", size = 2238896, upload-time = "2026-04-13T09:05:01.345Z" }, - { url = "https://files.pythonhosted.org/packages/df/f5/ea8ba209756abe9eba891bb0ef3772b4c59a894eb9ad86cd5bd0dd4e3e52/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ac10967e9a7bb1b96697374513f9a1a90a59e2fb41566b5e00ee45392beac59", size = 2314353, upload-time = "2026-04-13T09:06:07.942Z" }, - { url = "https://files.pythonhosted.org/packages/e8/f8/5885350203b72e96438eee7f94de0d8f0442f4627237ca8ef75de34db1cd/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7897078fe8a13b73623c0955dfb2b3d2c9acb7177aac25144758c9e5a5265aaa", size = 2098522, upload-time = "2026-04-13T09:04:23.239Z" }, - { url = "https://files.pythonhosted.org/packages/bf/88/5930b0e828e371db5a556dd3189565417ddc3d8316bb001058168aadcf5f/pydantic_core-2.46.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:e69ce405510a419a082a78faed65bb4249cfb51232293cc675645c12f7379bf7", size = 2168757, upload-time = "2026-04-13T09:07:12.46Z" }, - { url = "https://files.pythonhosted.org/packages/da/75/63d563d3035a0548e721c38b5b69fd5626fdd51da0f09ff4467503915b82/pydantic_core-2.46.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fd28d13eea0d8cf351dc1fe274b5070cc8e1cca2644381dee5f99de629e77cf3", size = 2202518, upload-time = "2026-04-13T09:05:44.418Z" }, - { url = "https://files.pythonhosted.org/packages/a7/53/1958eacbfddc41aadf5ae86dd85041bf054b675f34a2fa76385935f96070/pydantic_core-2.46.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:ee1547a6b8243e73dd10f585555e5a263395e55ce6dea618a078570a1e889aef", size = 2190148, upload-time = "2026-04-13T09:06:56.151Z" }, - { url = "https://files.pythonhosted.org/packages/c7/17/098cc6d3595e4623186f2bc6604a6195eb182e126702a90517236391e9ce/pydantic_core-2.46.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c3dc68dcf62db22a18ddfc3ad4960038f72b75908edc48ae014d7ac8b391d57a", size = 2342925, upload-time = "2026-04-13T09:04:17.286Z" }, - { url = "https://files.pythonhosted.org/packages/71/a7/abdb924620b1ac535c690b36ad5b8871f376104090f8842c08625cecf1d3/pydantic_core-2.46.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:004a2081c881abfcc6854a4623da6a09090a0d7c1398a6ae7133ca1256cee70b", size = 2383167, upload-time = "2026-04-13T09:04:52.643Z" }, - { url = "https://files.pythonhosted.org/packages/d7/c9/2ddd10f50e4b7350d2574629a0f53d8d4eb6573f9c19a6b43e6b1487a31d/pydantic_core-2.46.0-cp313-cp313-win32.whl", hash = "sha256:59d24ec8d5eaabad93097525a69d0f00f2667cb353eb6cda578b1cfff203ceef", size = 1965660, upload-time = "2026-04-13T09:06:05.877Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e7/1efc38ed6f2680c032bcefa0e3ebd496a8c77e92dfdb86b07d0f2fc632b1/pydantic_core-2.46.0-cp313-cp313-win_amd64.whl", hash = "sha256:71186dad5ac325c64d68fe0e654e15fd79802e7cc42bc6f0ff822d5ad8b1ab25", size = 2069563, upload-time = "2026-04-13T09:07:14.738Z" }, - { url = "https://files.pythonhosted.org/packages/c3/1e/a325b4989e742bf7e72ed35fa124bc611fd76539c9f8cd2a9a7854473533/pydantic_core-2.46.0-cp313-cp313-win_arm64.whl", hash = "sha256:8e4503f3213f723842c9a3b53955c88a9cfbd0b288cbd1c1ae933aebeec4a1b4", size = 2034966, upload-time = "2026-04-13T09:04:21.629Z" }, - { url = "https://files.pythonhosted.org/packages/36/3b/914891d384cdbf9a6f464eb13713baa22ea1e453d4da80fb7da522079370/pydantic_core-2.46.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:4fc801c290342350ffc82d77872054a934b2e24163727263362170c1db5416ca", size = 2113349, upload-time = "2026-04-13T09:04:59.407Z" }, - { url = "https://files.pythonhosted.org/packages/35/95/3a0c6f65e231709fb3463e32943c69d10285cb50203a2130a4732053a06d/pydantic_core-2.46.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0a36f2cc88170cc177930afcc633a8c15907ea68b59ac16bd180c2999d714940", size = 1949170, upload-time = "2026-04-13T09:06:09.935Z" }, - { url = "https://files.pythonhosted.org/packages/d1/63/d845c36a608469fe7bee226edeff0984c33dbfe7aecd755b0e7ab5a275c4/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a3912e0c568a1f99d4d6d3e41def40179d61424c0ca1c8c87c4877d7f6fd7fb", size = 1977914, upload-time = "2026-04-13T09:04:56.16Z" }, - { url = "https://files.pythonhosted.org/packages/08/6f/f2e7a7f85931fb31671f5378d1c7fc70606e4b36d59b1b48e1bd1ef5d916/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3534c3415ed1a19ab23096b628916a827f7858ec8db49ad5d7d1e44dc13c0d7b", size = 2050538, upload-time = "2026-04-13T09:05:06.789Z" }, - { url = "https://files.pythonhosted.org/packages/8c/97/f4aa7181dd9a16dd9059a99fc48fdab0c2aab68307283a5c04cf56de68c4/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21067396fc285609323a4db2f63a87570044abe0acddfcca8b135fc7948e3db7", size = 2236294, upload-time = "2026-04-13T09:07:03.2Z" }, - { url = "https://files.pythonhosted.org/packages/24/c1/6a5042fc32765c87101b500f394702890af04239c318b6002cfd627b710d/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2afd85b7be186e2fe7cdbb09a3d964bcc2042f65bbcc64ad800b3c7915032655", size = 2312954, upload-time = "2026-04-13T09:06:11.919Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e4/566101a561492ce8454f0844ca29c3b675a6b3a7b3ff577db85ed05c8c50/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67e2c2e171b78db8154da602de72ffdc473c6ee51de8a9d80c0f1cd4051abfc7", size = 2102533, upload-time = "2026-04-13T09:06:58.664Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ac/adc11ee1646a5c4dd9abb09a00e7909e6dc25beddc0b1310ca734bb9b48e/pydantic_core-2.46.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c16ae1f3170267b1a37e16dba5c297bdf60c8b5657b147909ca8774ce7366644", size = 2169447, upload-time = "2026-04-13T09:04:11.143Z" }, - { url = "https://files.pythonhosted.org/packages/26/73/408e686b45b82d28ac19e8229e07282254dbee6a5d24c5c7cf3cf3716613/pydantic_core-2.46.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:133b69e1c1ba34d3702eed73f19f7f966928f9aa16663b55c2ebce0893cca42e", size = 2200672, upload-time = "2026-04-13T09:03:54.056Z" }, - { url = "https://files.pythonhosted.org/packages/0a/3b/807d5b035ec891b57b9079ce881f48263936c37bd0d154a056e7fd152afb/pydantic_core-2.46.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:15ed8e5bde505133d96b41702f31f06829c46b05488211a5b1c7877e11de5eb5", size = 2188293, upload-time = "2026-04-13T09:07:07.614Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ed/719b307516285099d1196c52769fdbe676fd677da007b9c349ae70b7226d/pydantic_core-2.46.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:8cfc29a1c66a7f0fcb36262e92f353dd0b9c4061d558fceb022e698a801cb8ae", size = 2335023, upload-time = "2026-04-13T09:04:05.176Z" }, - { url = "https://files.pythonhosted.org/packages/8d/90/8718e4ae98c4e8a7325afdc079be82be1e131d7a47cb6c098844a9531ffe/pydantic_core-2.46.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e1155708540f13845bf68d5ac511a55c76cfe2e057ed12b4bf3adac1581fc5c2", size = 2377155, upload-time = "2026-04-13T09:06:18.081Z" }, - { url = "https://files.pythonhosted.org/packages/dd/dc/7172789283b963f81da2fc92b186e22de55687019079f71c4d570822502b/pydantic_core-2.46.0-cp314-cp314-win32.whl", hash = "sha256:de5635a48df6b2eef161d10ea1bc2626153197333662ba4cd700ee7ec1aba7f5", size = 1963078, upload-time = "2026-04-13T09:05:30.615Z" }, - { url = "https://files.pythonhosted.org/packages/e0/69/03a7ea4b6264def3a44eabf577528bcec2f49468c5698b2044dea54dc07e/pydantic_core-2.46.0-cp314-cp314-win_amd64.whl", hash = "sha256:f07a5af60c5e7cf53dd1ff734228bd72d0dc9938e64a75b5bb308ca350d9681e", size = 2068439, upload-time = "2026-04-13T09:04:57.729Z" }, - { url = "https://files.pythonhosted.org/packages/f5/eb/1c3afcfdee2ab6634b802ab0a0f1966df4c8b630028ec56a1cb0a710dc58/pydantic_core-2.46.0-cp314-cp314-win_arm64.whl", hash = "sha256:e7a77eca3c7d5108ff509db20aae6f80d47c7ed7516d8b96c387aacc42f3ce0f", size = 2026470, upload-time = "2026-04-13T09:05:08.654Z" }, - { url = "https://files.pythonhosted.org/packages/5c/30/1177dde61b200785c4739665e3aa03a9d4b2c25d2d0408b07d585e633965/pydantic_core-2.46.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5e7cdd4398bee1aaeafe049ac366b0f887451d9ae418fd8785219c13fea2f928", size = 2107447, upload-time = "2026-04-13T09:05:46.314Z" }, - { url = "https://files.pythonhosted.org/packages/b1/60/4e0f61f99bdabbbc309d364a2791e1ba31e778a4935bc43391a7bdec0744/pydantic_core-2.46.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5c2c92d82808e27cef3f7ab3ed63d657d0c755e0dbe5b8a58342e37bdf09bd2e", size = 1926927, upload-time = "2026-04-13T09:06:20.371Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d0/67f89a8269152c1d6eaa81f04e75a507372ebd8ca7382855a065222caa80/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bab80af91cd7014b45d1089303b5f844a9d91d7da60eabf3d5f9694b32a6655", size = 1966613, upload-time = "2026-04-13T09:07:05.389Z" }, - { url = "https://files.pythonhosted.org/packages/cd/07/8dfdc3edc78f29a80fb31f366c50203ec904cff6a4c923599bf50ac0d0ff/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e49ffdb714bc990f00b39d1ad1d683033875b5af15582f60c1f34ad3eeccfaa", size = 2032902, upload-time = "2026-04-13T09:06:42.47Z" }, - { url = "https://files.pythonhosted.org/packages/b0/2a/111c5e8fe24f99c46bcad7d3a82a8f6dbc738066e2c72c04c71f827d8c78/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca877240e8dbdeef3a66f751dc41e5a74893767d510c22a22fc5c0199844f0ce", size = 2244456, upload-time = "2026-04-13T09:05:36.484Z" }, - { url = "https://files.pythonhosted.org/packages/6b/7c/cfc5d11c15a63ece26e148572c77cfbb2c7f08d315a7b63ef0fe0711d753/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87e6843f89ecd2f596d7294e33196c61343186255b9880c4f1b725fde8b0e20d", size = 2294535, upload-time = "2026-04-13T09:06:01.689Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2c/f0d744e3dab7bd026a3f4670a97a295157cff923a2666d30a15a70a7e3d0/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e20bc5add1dd9bc3b9a3600d40632e679376569098345500799a6ad7c5d46c72", size = 2104621, upload-time = "2026-04-13T09:04:34.388Z" }, - { url = "https://files.pythonhosted.org/packages/a7/64/e7cc4698dc024264d214b51d5a47a2404221b12060dd537d76f831b2120a/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:ee6ff79a5f0289d64a9d6696a3ce1f98f925b803dd538335a118231e26d6d827", size = 2130718, upload-time = "2026-04-13T09:04:26.23Z" }, - { url = "https://files.pythonhosted.org/packages/0b/a8/224e655fec21f7d4441438ad2ecaccb33b5a3876ce7bb2098c74a49efc14/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:52d35cfb58c26323101c7065508d7bb69bb56338cda9ea47a7b32be581af055d", size = 2180738, upload-time = "2026-04-13T09:05:50.253Z" }, - { url = "https://files.pythonhosted.org/packages/32/7b/b3025618ed4c4e4cbaa9882731c19625db6669896b621760ea95bc1125ef/pydantic_core-2.46.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d14cc5a6f260fa78e124061eebc5769af6534fc837e9a62a47f09a2c341fa4ea", size = 2171222, upload-time = "2026-04-13T09:07:29.929Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e3/68170aa1d891920af09c1f2f34df61dc5ff3a746400027155523e3400e89/pydantic_core-2.46.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:4f7ff859d663b6635f6307a10803d07f0d09487e16c3d36b1744af51dbf948b2", size = 2320040, upload-time = "2026-04-13T09:06:35.732Z" }, - { url = "https://files.pythonhosted.org/packages/67/1b/5e65807001b84972476300c1f49aea2b4971b7e9fffb5c2654877dadd274/pydantic_core-2.46.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:8ef749be6ed0d69dba31902aaa8255a9bb269ae50c93888c4df242d8bb7acd9e", size = 2377062, upload-time = "2026-04-13T09:07:39.945Z" }, - { url = "https://files.pythonhosted.org/packages/75/03/48caa9dd5f28f7662bd52bff454d9a451f6b7e5e4af95e289e5e170749c9/pydantic_core-2.46.0-cp314-cp314t-win32.whl", hash = "sha256:d93ca72870133f86360e4bb0c78cd4e6ba2a0f9f3738a6486909ffc031463b32", size = 1951028, upload-time = "2026-04-13T09:04:20.224Z" }, - { url = "https://files.pythonhosted.org/packages/87/ed/e97ff55fe28c0e6e3cba641d622b15e071370b70e5f07c496b07b65db7c9/pydantic_core-2.46.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6ebb2668afd657e2127cb40f2ceb627dd78e74e9dfde14d9bf6cdd532a29ff59", size = 2048519, upload-time = "2026-04-13T09:05:10.464Z" }, - { url = "https://files.pythonhosted.org/packages/b6/51/e0db8267a287994546925f252e329eeae4121b1e77e76353418da5a3adf0/pydantic_core-2.46.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4864f5bbb7993845baf9209bae1669a8a76769296a018cb569ebda9dcb4241f5", size = 2026791, upload-time = "2026-04-13T09:04:37.724Z" }, - { url = "https://files.pythonhosted.org/packages/2d/f1/6731c2d6caf03efe822101edb4783eb3f212f34b7b005a34f039f67e76e1/pydantic_core-2.46.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:ce2e38e27de73ff6a0312a9e3304c398577c418d90bbde97f0ba1ee3ab7ac39f", size = 2121259, upload-time = "2026-04-13T09:07:34.845Z" }, - { url = "https://files.pythonhosted.org/packages/72/fd/ac34d4c92e739e37a040be9e7ea84d116afec5f983a7db856c27135fba77/pydantic_core-2.46.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:f0d34ba062396de0be7421e6e69c9a6821bf6dc73a0ab9959a48a5a6a1e24754", size = 1945798, upload-time = "2026-04-13T09:04:24.729Z" }, - { url = "https://files.pythonhosted.org/packages/b6/a4/f413a522c4047c46b109be6805a3095d35e5a4882fd5b4fdc0909693dfc0/pydantic_core-2.46.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4c0a12147b4026dd68789fb9f22f1a8769e457f9562783c181880848bbd6412", size = 1986062, upload-time = "2026-04-13T09:05:57.177Z" }, - { url = "https://files.pythonhosted.org/packages/91/2e/9760025ea8b0f49903c0ceebdfc2d8ef839da872426f2b03cae9de036a7c/pydantic_core-2.46.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a99896d9db56df901ab4a63cd6a36348a569cff8e05f049db35f4016a817a3d9", size = 2145344, upload-time = "2026-04-13T09:03:56.924Z" }, - { url = "https://files.pythonhosted.org/packages/74/0c/106ed5cc50393d90523f09adcc50d05e42e748eb107dc06aea971137f02d/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:bc0e2fefe384152d7da85b5c2fe8ce2bf24752f68a58e3f3ea42e28a29dfdeb2", size = 2104968, upload-time = "2026-04-13T09:06:26.967Z" }, - { url = "https://files.pythonhosted.org/packages/f5/71/b494cef3165e3413ee9bbbb5a9eedc9af0ea7b88d8638beef6c2061b110e/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:a2ab0e785548be1b4362a62c4004f9217598b7ee465f1f420fc2123e2a5b5b02", size = 1940442, upload-time = "2026-04-13T09:06:29.332Z" }, - { url = "https://files.pythonhosted.org/packages/7e/3e/a4d578c8216c443e26a1124f8c1e07c0654264ce5651143d3883d85ff140/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16d45aecb18b8cba1c68eeb17c2bb2d38627ceed04c5b30b882fc9134e01f187", size = 1999672, upload-time = "2026-04-13T09:04:42.798Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c1/9114560468685525a21770138382fd0cb849aaf351ff2c7b97f760d121e0/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5078f6c377b002428e984259ac327ef8902aacae6c14b7de740dd4869a491501", size = 2154533, upload-time = "2026-04-13T09:04:50.868Z" }, - { url = "https://files.pythonhosted.org/packages/09/ed/fbd8127e4a19c4fdbb2f4983cf72c7b3534086df640c813c5c0ec4218177/pydantic_core-2.46.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:be3e04979ba4d68183f247202c7f4f483f35df57690b3f875c06340a1579b47c", size = 2119951, upload-time = "2026-04-13T09:04:35.923Z" }, - { url = "https://files.pythonhosted.org/packages/ec/77/df8711ebb45910412f90d75198430fa1120f5618336b71fa00303601c5a4/pydantic_core-2.46.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b1eae8d7d9b8c2a90b34d3d9014804dca534f7f40180197062634499412ea14e", size = 1953812, upload-time = "2026-04-13T09:05:40.293Z" }, - { url = "https://files.pythonhosted.org/packages/12/fe/14b35df69112bd812d6818a395eeab22eeaa2befc6f85bc54ed648430186/pydantic_core-2.46.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a95a2773680dd4b6b999d4eccdd1b577fd71c31739fb4849f6ada47eabb9c56", size = 2139585, upload-time = "2026-04-13T09:06:46.94Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f0/4fea4c14ebbdeb87e5f6edd2620735fcbd384865f06707fe229c021ce041/pydantic_core-2.46.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25988c3159bb097e06abfdf7b21b1fcaf90f187c74ca6c7bb842c1f72ce74fa8", size = 2179154, upload-time = "2026-04-13T09:04:15.639Z" }, - { url = "https://files.pythonhosted.org/packages/5c/36/6329aa79ba32b73560e6e453164fb29702b115fd3b2b650e796e1dc27862/pydantic_core-2.46.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:747d89bd691854c719a3381ba46b6124ef916ae85364c79e11db9c84995d8d03", size = 2182917, upload-time = "2026-04-13T09:07:24.483Z" }, - { url = "https://files.pythonhosted.org/packages/92/61/edbf7aea71052d410347846a2ea43394f74651bf6822b8fad8703ca00575/pydantic_core-2.46.0-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:909a7327b83ca93b372f7d48df0ebc7a975a5191eb0b6e024f503f4902c24124", size = 2327716, upload-time = "2026-04-13T09:06:31.681Z" }, - { url = "https://files.pythonhosted.org/packages/a4/11/aa5089b941e85294b1d5d526840b18f0d4464f842d43d8999ce50ef881c1/pydantic_core-2.46.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:2f7e6a3752378a69fadf3f5ee8bc5fa082f623703eec0f4e854b12c548322de0", size = 2365925, upload-time = "2026-04-13T09:05:38.338Z" }, - { url = "https://files.pythonhosted.org/packages/0c/75/e187b0ea247f71f2009d156df88b7d8449c52a38810c9a1bd55dd4871206/pydantic_core-2.46.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ef47ee0a3ac4c2bb25a083b3acafb171f65be4a0ac1e84edef79dd0016e25eaa", size = 2193856, upload-time = "2026-04-13T09:05:03.114Z" }, -] - [[package]] name = "pygments" version = "2.20.0" @@ -779,15 +485,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] -[[package]] -name = "python-multipart" -version = "0.0.26" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" }, -] - [[package]] name = "respx" version = "0.23.1" @@ -825,19 +522,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" }, ] -[[package]] -name = "starlette" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, -] - [[package]] name = "tomli" version = "2.4.1" @@ -900,29 +584,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac8 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" }, ] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -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" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, -] - -[[package]] -name = "uvicorn" -version = "0.44.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/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" }, -] From 9629d5cbfb211284476ab9e522b33728943ce534 Mon Sep 17 00:00:00 2001 From: secretpray Date: Sun, 8 Nov 2026 13:43:37 +0200 Subject: [PATCH 2/5] chore(deps): require Python 3.11 or newer and update development tools Python 3.10 has reached end of life. Declare support for Python 3.11-3.15, use an SPDX license expression with license-files, and raise the minimum versions of the test, lint and type-check tools. --- .python-version | 2 +- pyproject.toml | 25 +- uv.lock | 782 +++++++++++++++++++++++++----------------------- 3 files changed, 426 insertions(+), 383 deletions(-) diff --git a/.python-version b/.python-version index c8cfe39..2c07333 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.10 +3.11 diff --git a/pyproject.toml b/pyproject.toml index 47c9bbe..c88a8e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["hatchling>=1.27"] +requires = ["hatchling>=1.33"] build-backend = "hatchling.build" [project] @@ -8,7 +8,8 @@ dynamic = ["version"] description = "Official Python SDK for the OctaSpace API." readme = "README.md" license = "MIT" -requires-python = ">=3.10" +license-files = ["LICENSE"] +requires-python = ">=3.11" authors = [ { name = "OctaSpace" }, ] @@ -16,12 +17,12 @@ keywords = ["octaspace", "sdk", "api", "gpu", "cloud", "compute"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Typing :: Typed", ] dependencies = [ @@ -37,15 +38,15 @@ Changelog = "https://github.com/octaspace/python-sdk/blob/main/CHANGELOG.md" [dependency-groups] test = [ - "pytest>=8.0", - "pytest-cov>=5.0", - "respx>=0.21", + "pytest>=9.2", + "pytest-cov>=7.1", + "respx>=0.23", ] lint = [ - "ruff>=0.11", + "ruff>=0.17", ] typecheck = [ - "mypy>=1.11", + "mypy>=2.4", ] dev = [ { include-group = "test" }, @@ -64,12 +65,14 @@ include = [ "/LICENSE", "/pyproject.toml", ] +core-metadata-version = "2.4" [tool.hatch.build.targets.wheel] packages = ["src/octaspace"] +core-metadata-version = "2.4" [tool.ruff] -target-version = "py310" +target-version = "py311" line-length = 100 src = ["src", "tests"] @@ -81,6 +84,6 @@ testpaths = ["tests"] addopts = "--strict-markers --cov=octaspace --cov-report=term-missing" [tool.mypy] -python_version = "3.10" +python_version = "3.11" strict = true packages = ["octaspace"] diff --git a/uv.lock b/uv.lock index 0cc0115..a21e6a8 100644 --- a/uv.lock +++ b/uv.lock @@ -1,28 +1,95 @@ version = 1 revision = 3 -requires-python = ">=3.10" +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] [[package]] name = "anyio" -version = "4.13.0" +version = "4.16.0" 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/a1/98/7e457a3b014976887a050561955c0b76e1e7e2d6e27b78ecef2ef80518f2/anyio-4.16.0.tar.gz", hash = "sha256:59bb03251907983d1603d27c3087ddb1e93299ea2163adcdba37899a4fb12ffb", size = 310971, upload-time = "2026-11-01T11:20:07.282Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/16/7757fb84c819accc7d7e9af5291b261e4abb8627ee040f99e7d35ef2457a/anyio-4.16.0-py3-none-any.whl", hash = "sha256:c66ae634e0b8228396b801741beb3294326d4b6102f7bba4041c0dfb58c29026", size = 134521, upload-time = "2026-11-01T11:20:05.59Z" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } + +[[package]] +name = "ast-serialize" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/d5/5dc1ee8a80c3b20a0b63c61b43ba6bc53517faecd1def47bcd752d22268e/ast_serialize-0.13.1.tar.gz", hash = "sha256:401cc84e8888d4d76f2c8d296fa006ed1a5ff993b6aff90dd777c03973efd025", size = 955137, upload-time = "2026-10-17T00:04:17.183Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a8/fbd136b9f70c59e75f59a8e974c84102976f1201828b4f886480ba956936/ast_serialize-0.13.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3e129d44b0cffbea24a7f40117ce49e30993c3d119e584d4817ebd6f87246456", size = 897306, upload-time = "2026-10-17T00:02:52.479Z" }, + { url = "https://files.pythonhosted.org/packages/50/3f/2ad51e9823a790e7a70fd781e9595ab3abeace27f3aefcaa1939457efd72/ast_serialize-0.13.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:523d45bab16ddaf1399c04c4fe81bbf180f81e33c2f7bcbe76be56b0f7311ab4", size = 1236082, upload-time = "2026-10-17T00:02:54.82Z" }, + { url = "https://files.pythonhosted.org/packages/c0/5c/8e959db5cc2237f9d4c560a8e2253c98f9746883fb4774651e4cda458901/ast_serialize-0.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:638333fb8ae72c62753f9e794fda20290883ddc65b0d85e26e08f8bb227567ad", size = 1221242, upload-time = "2026-10-17T00:02:56.395Z" }, + { url = "https://files.pythonhosted.org/packages/d6/05/ab1c56c3b7adcac88764bba609502d6b4989e449b2dd60eb478e1d20e369/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc34c670747d85d4232834665e247009413eaebe47c69eaf60a5c439aaecaed9", size = 1282105, upload-time = "2026-10-17T00:02:58.275Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f4/41981fcd2ddffe0760e5326be9dcbd2eda9478a8430a637e5123bd8fcb26/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8c8ebd419811c435812d242fb2a173e57dfede198c9ac9a0a584666ebc9b9331", size = 1287905, upload-time = "2026-10-17T00:03:00.068Z" }, + { url = "https://files.pythonhosted.org/packages/6a/43/b783addc5681153707121991b7b47892f7e9c8ba58a0d03b71a3b023ba47/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8899c038b1c15ae8b98e379057fb3ef398ceb0b21d1737a8156dd5a0923b7e51", size = 1541806, upload-time = "2026-10-17T00:03:01.503Z" }, + { url = "https://files.pythonhosted.org/packages/47/10/969db095038ee15270f65ad20d1129c5e4da605bf7e93b70ae640f1c8a9f/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e14c4de98f965647a74e6eaf37e20207124e45451ccd36e84a6f6ebab45e0372", size = 1309946, upload-time = "2026-10-17T00:03:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/06/dd/7c623d2fb0b868948952d1787e629a43b866f7d4caefd5445e20aefacec4/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3e8a3eff69af640db985360fef35073fb46956dfc3e5c614cf00920d2746dd", size = 1302773, upload-time = "2026-10-17T00:03:04.378Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c2/4d49469110a19a44fc6269f4fd59679bcf84df068f185f89b73bee4da19b/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:75d7750fe98f6f35a96c43aba8d98a2e623e22c778a0c17ab9ede92eb10f5d82", size = 1299579, upload-time = "2026-10-17T00:03:05.88Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9a/2d381f1f262dda15affc8ad643fba1619eb07e7d9f8bd904deffef183006/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e8ddd63e80fc280c2f790157386f27f9b9f6ccb9c4f8e803b863cbfb085769", size = 1355969, upload-time = "2026-10-17T00:03:07.321Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9c/2266794e6c25fa9ce51ce04a9bf4b7e330e3ac9351ba3fa14e487678acdb/ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9221e09496fb942901993c1812ee87950cc0ecbb514c7c0f1f5fa6d78f701ed8", size = 1460227, upload-time = "2026-10-17T00:03:08.733Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d6/14deb28e5d8f328f626a0778ea2cf98fe908f2bbcccd804cb6f449ee1782/ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:7a5c38bdb93669c596f94fb7204dd6ebe4fadb19058ac9a2faf2948c2a175f96", size = 1562691, upload-time = "2026-10-17T00:03:10.25Z" }, + { url = "https://files.pythonhosted.org/packages/f5/19/7a28c0e5ba54f0d3e054f847a2a549b748cb179ab433321a8dc1b39e398d/ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:acf4e9f055d75058ab01536da330648746f2f82b7697a85230547f2d5ebe895c", size = 1558669, upload-time = "2026-10-17T00:03:11.799Z" }, + { url = "https://files.pythonhosted.org/packages/18/9a/f9e2690eab58217f1d395a964a78efa7eaf813a1e38be8dfd9a8d45d0840/ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87427913c325adc811e81e7c9a757b061f47ef1e29114ac479aa0953a2687a98", size = 1665071, upload-time = "2026-10-17T00:03:13.284Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f3/ef5b159a0d7161b404f05e7ec21cf7735ee7edc10c000cd2cbbc7bb6b69a/ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:ad88921f95b92bc144d9ad059a0e16f8c1ae69f5239f2d3c6536fd2774f73dfe", size = 1473364, upload-time = "2026-10-17T00:03:14.563Z" }, + { url = "https://files.pythonhosted.org/packages/1d/87/6e8ac4a865124a12cc4f915bf1bcfac3bba426cd5f58d3712a873ee61012/ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f8b17e2921e56621bcb46792bd3e0b82ca3b85de78d5936e7b8174ba1758b0aa", size = 1500566, upload-time = "2026-10-17T00:03:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1e/29155f1621433008013ad5c940a03d41a676be0680c41527040472d37583/ast_serialize-0.13.1-cp314-cp314t-win32.whl", hash = "sha256:2f9a9ce8b0994f757b941f7f7fe384a6666b4162b941ebc5c248081baea18e47", size = 1128674, upload-time = "2026-10-17T00:03:17.481Z" }, + { url = "https://files.pythonhosted.org/packages/bb/64/2506da65143ea138de5822e9173e0b72320a2f70e23bd723f044033175f1/ast_serialize-0.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5d99910e6b9a9b9360bcca2ae99aee36cc912ce058df2a57d1c33099b6cb2593", size = 1163306, upload-time = "2026-10-17T00:03:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/6d/8f/c7cb22638899b0cf15908809c270560e46395e65604bc59e4b623ab9f405/ast_serialize-0.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:a789cbe14d0a0e9ec9b32a456e5abc81ad8799479c3cfd88a29e3442262aebe6", size = 1133775, upload-time = "2026-10-17T00:03:20.377Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e2/b0c140fe50fa39dd249e382f7733af6f816c90021fe7959a0b026f86237f/ast_serialize-0.13.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:23d768369b89d551077dc173da6892fad00443338417523b6732f4e8ba4a7ed5", size = 1236160, upload-time = "2026-10-17T00:03:21.945Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/a8a2ca21e3943555b48221f8fba546877a7ad39efaf6505506524967d7c4/ast_serialize-0.13.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:68f8829d114dab6c9db4a8bbb8d0e342958faee1b5e15a29022678df639960f8", size = 1222214, upload-time = "2026-10-17T00:03:23.462Z" }, + { url = "https://files.pythonhosted.org/packages/15/19/2f58f2ba64e4799b39de85c140aef09a03aa6ea53be75dd292a9d9b9d8ae/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e385b17d5bde892a6184003a9aff3518387908b73f8f5ea539313c07c8c0ede", size = 1282979, upload-time = "2026-10-17T00:03:24.695Z" }, + { url = "https://files.pythonhosted.org/packages/b1/af/ca6087fe84210c1ccd43e3182917bb70649fa1e69745a90c1d59ba08fea8/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:02686b281ec9b8e9614f6f0db27e12e4cac515f336c5d05b678bc9eb73ff2af5", size = 1288596, upload-time = "2026-10-17T00:03:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/b0/2c/4ef964c763329a0347125e745ec0451ba4fbaceec9efe2ac729b7cd55f17/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8ab2c2585b2e22e48cd4aff2136fce4eab4ced569b85f150ef9a5a5d6f34d49", size = 1545540, upload-time = "2026-10-17T00:03:27.52Z" }, + { url = "https://files.pythonhosted.org/packages/bb/85/aa961dbb75813cfa94de0ce9a16a9510c226b95f604c405421e727f44bb0/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c652d9f5d52d8b81eff3ff2c95c9dd0f281de45f6ad5d3fa95527550c396454a", size = 1309374, upload-time = "2026-10-17T00:03:29.091Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e6/f2b67c327d9f9533e41791804403ccc2dc457821195a2e51f0b587a90b90/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121e1d08d58b75c15491cc3003609cbad71ac2b0016043c36d30a6b2032b6605", size = 1303466, upload-time = "2026-10-17T00:03:30.451Z" }, + { url = "https://files.pythonhosted.org/packages/73/1d/33f18b81d1bb1f5d682943ea92fda8130a6593627f12f6201668d40824fd/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:30cf89b6a6844542d116c758725d13de5b97b9410a51ee577715f761270d0c1c", size = 1300576, upload-time = "2026-10-17T00:03:32.102Z" }, + { url = "https://files.pythonhosted.org/packages/25/cb/64914144925f1a4abdd9217e717b122255c38fc5ebe4342f92052f3bacbc/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:732aaae9e498c973fc63fd7f308009b57c4afa6ff59d0b674d894e57d0bca01a", size = 1355960, upload-time = "2026-10-17T00:03:33.676Z" }, + { url = "https://files.pythonhosted.org/packages/ee/80/bd29379fab2bce19adc0bf9177002b11b23dfe8d8fecf409998b458b4624/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:d9c1a920b2aa0c1e016bed7721689717034ca0dba64ac3a3ff243b003d4e8dd8", size = 1459779, upload-time = "2026-10-17T00:03:35.09Z" }, + { url = "https://files.pythonhosted.org/packages/83/89/012dbe733de05ffdf8e40ca0f9fa5d417cb16ec93a74463c57c001914198/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:847cad4c22953823f5e76d0b126c6f9da670cfa45b1df41b9324e22189d77611", size = 1563399, upload-time = "2026-10-17T00:03:37.285Z" }, + { url = "https://files.pythonhosted.org/packages/69/37/ee4bcb0358bee38744eb0f18560446572d095fe3bf6b63d6f739d08a839c/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:aca07de3ca8e3d9f2b716c3c31f4f384fe4bd2d03e09912417cff7a3e08036f7", size = 1558808, upload-time = "2026-10-17T00:03:38.711Z" }, + { url = "https://files.pythonhosted.org/packages/54/1c/ff44c5355251d61ef22934c4430bb31a61de4781d25a992154aac9b8c82a/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:5db12d843d931b9d1f1519ce66b9cbfe72d8563b75c1204d255322254d1964dd", size = 1666324, upload-time = "2026-10-17T00:03:40.376Z" }, + { url = "https://files.pythonhosted.org/packages/c8/bb/6da4bfc8aa3403e6447607131bf3463acaf954d990c6c8b6787c1902ddfe/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:fb8021f4dc3296de18737e3b8b34994411da725374d74a0b9eb32a738f2a23d0", size = 1474089, upload-time = "2026-10-17T00:03:41.715Z" }, + { url = "https://files.pythonhosted.org/packages/21/97/897e33bef2eefd880e6350e375cc66bc5bcd00a139be480fd655f5b3f095/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:0d764913374ba21df8cd618045b371ed104ce55c6276de7c59e554a4f5ba26fd", size = 1501542, upload-time = "2026-10-17T00:03:43.08Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4c/821211ac83870c27e4412381b7c7838336456b40575bf840e4680b2018d9/ast_serialize-0.13.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:2bad302ae2427454ef9dab71bd5c3ad67188860c39d36daee38f72031edb39c4", size = 1126118, upload-time = "2026-10-17T00:03:44.616Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/2b2d5e6c4fa352874a80618e742ffd32d249dee6c119f4233b51afc6b8f4/ast_serialize-0.13.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:032b09d86c785c3984e975378b7861f2a68ac44e22ba7b07b93a536b2524eeb8", size = 1163766, upload-time = "2026-10-17T00:03:45.986Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/d610cfcdabaa44fd62a54ca1fc57a7dba120643798b089a14914973562de/ast_serialize-0.13.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:929eda0c419e622ab49df3a4c45f81c57dcc10874bbad55d0470a4a03324dac3", size = 1134043, upload-time = "2026-10-17T00:03:48.453Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/0e30f9eddf7d44ccf9d25979030754fd0457af61fa4279acb8c293a04f97/ast_serialize-0.13.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bf53a3ba1cbe45399fc1e4ccd684b8f2a11cca450f89706b79122f478534c6f5", size = 897546, upload-time = "2026-10-17T00:03:50.093Z" }, + { url = "https://files.pythonhosted.org/packages/81/92/d705be88ff452b9ba0088b554a0cbd308fd01879da092b33fd2ad7d1aa60/ast_serialize-0.13.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4bcf1d5f4eefecaa325a34ecf079b2741189e7d0821523f92cb343a389a67489", size = 1242416, upload-time = "2026-10-17T00:03:51.521Z" }, + { url = "https://files.pythonhosted.org/packages/60/c2/98370845a7f35cb8a25782e78284b48e01bf3128339881d48d613f304f8f/ast_serialize-0.13.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1b158f1c9e3a0d386913924c57995d6df78224868a4d0f215bdd41587c97b34a", size = 1229729, upload-time = "2026-10-17T00:03:52.825Z" }, + { url = "https://files.pythonhosted.org/packages/d8/8b/2d7ffed76addd9d3fcf37fb171400942735c8912264c763561a85407f6a8/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a6201b69ce42e919f2fd9a559138d5410b5d8afff7c3a6b1f500b0481ff7d8", size = 1292255, upload-time = "2026-10-17T00:03:54.148Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2a/bb9e0e719d89cc950fadfcfcc9cb4bc7640419c4228c852f031ab8abf440/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eb0be64c9b8f1ed26637eef345abe75ea90351c92c85a8aec7838b3af971011", size = 1298501, upload-time = "2026-10-17T00:03:55.559Z" }, + { url = "https://files.pythonhosted.org/packages/7a/32/13ce07dce48226f5bd9cfd8f708ec101a8be05aa430cda4918ce31ce8c82/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f2427bddfca208e84de700044fadf9aef2769a70bae740a1bdabb89ee9e1117", size = 1553758, upload-time = "2026-10-17T00:03:56.952Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fb/9b51d186db0c20ca28cbaaefd50f03b573c89327b9a2627820cbce22196b/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88df960e67184a71874e5b2ee014f87d1254a0dbc1f903c7bd7b1ab7733ecaef", size = 1318258, upload-time = "2026-10-17T00:03:58.418Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fa/da311559dd3dae9af5825cf21660775a929b970f72e3786af3f2cd667944/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f99786e0d62082a5b121b3437f3167562133711df22128de1e9886f57f0b226", size = 1313260, upload-time = "2026-10-17T00:04:00.194Z" }, + { url = "https://files.pythonhosted.org/packages/2b/11/97a4b9e5b4776d72f47fcd9a09a497bbf68ec8c1ae074307a2c51e410d85/ast_serialize-0.13.1-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:a01b515b25a03184eff4acef25b981ae007916379682e908fc8785b342a0bf3e", size = 1309992, upload-time = "2026-10-17T00:04:01.575Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c4/62f85121a80cd451ef04f550cf0f7ea0ad9ff46cc4e9a5b9ae439b6527b1/ast_serialize-0.13.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:29422d7bcd9e7f39411571c36cec45ce8a7a9b3ad4b2e7aa64497c15a9e397f6", size = 1362715, upload-time = "2026-10-17T00:04:02.881Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fa/c5f4a545f72a7681e3295bab6e3600ee0862ec4391bed961fe9f735b67e0/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ffb0f548a7dc126067fdfb7b2325ab5dfcdeecc29faf5e0f0461c96d6ebfa3d", size = 1469931, upload-time = "2026-10-17T00:04:04.181Z" }, + { url = "https://files.pythonhosted.org/packages/38/a9/65aed4f8fa7a001a7cfdc03e68e844517ab041ba6fb614b7d78a1d6ac056/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2876444a5f87a7c76f371af3c07c2fda563a08bc8af52568df812e8aba4bf568", size = 1570945, upload-time = "2026-10-17T00:04:05.778Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8c/44b2da991cede79bfbf52089f33a5f0168945466b3d9cd38b5dcf08e7130/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:885a62ec6481ce58ab24c6357360d7a82023b62dd7e5e7ceccf59e9662a3272b", size = 1569978, upload-time = "2026-10-17T00:04:07.347Z" }, + { url = "https://files.pythonhosted.org/packages/46/e4/109b2a5beca700237767d5add920c2f8e1ba9bd5799b0faf143d0e9cf4ef/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:2015fd0165e463aac3b1aadc6a1fa1e5820c14ca950821f4fe02f8a3f6a388d6", size = 1676082, upload-time = "2026-10-17T00:04:08.7Z" }, + { url = "https://files.pythonhosted.org/packages/73/e0/7df70f6dbb11b5762da5416318f08316a1c51348e0d69ec79ca5f98f836a/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:eb41e1906502ac80d24ff343f45008552a3f5b5158607959fe6a07dbfc6eb037", size = 1483207, upload-time = "2026-10-17T00:04:10.162Z" }, + { url = "https://files.pythonhosted.org/packages/1f/bc/523aa1c08bd69b5654621beb3dc1a9035715a4a63762fb8d219d6fb3eb81/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ca9acc67f796aa2554fa3037dccc022a4b08b38b2ad153edfac9c9bb7cb1f3c6", size = 1512140, upload-time = "2026-10-17T00:04:11.589Z" }, + { url = "https://files.pythonhosted.org/packages/66/83/6607060659998f00afdea0d8375c1e6e2b7d515ab905090d9a5a02f48851/ast_serialize-0.13.1-cp39-abi3-win32.whl", hash = "sha256:4f98da562e5901d69c7dd30885594c5438f859edf354e9ac7517d188fb026c3a", size = 1131889, upload-time = "2026-10-17T00:04:12.953Z" }, + { url = "https://files.pythonhosted.org/packages/9f/6f/bafd6490fe7b355627fabe95f4417e3d02da93102c8552550eb9a0774e1e/ast_serialize-0.13.1-cp39-abi3-win_amd64.whl", hash = "sha256:ddf7cbc9c8abf70102c4a088efe154bb86bfb825b0a10e556d2b15972cf99bd7", size = 1171153, upload-time = "2026-10-17T00:04:14.362Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/4235f4b375a67a37ac595bf478a5407a744eaf7b64113c1c3a5432b8d954/ast_serialize-0.13.1-cp39-abi3-win_arm64.whl", hash = "sha256:48f11d96c9ec71119612f8e79a8c55c12f2b136716d68347433c561d27a25f43", size = 1139035, upload-time = "2026-10-17T00:04:15.767Z" }, ] [[package]] name = "certifi" -version = "2026.2.25" +version = "2026.10.23" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/11/b524cbd9bbdaca58608e4cf4053fd463417549e1959fa8d3aa18319f9555/certifi-2026.10.23.tar.gz", hash = "sha256:a9895e6e2758b371e0545e321a3a8fd1fcfd9829ada8824a936dfb16aaaf36cd", size = 136225, upload-time = "2026-10-23T11:36:16.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, + { url = "https://files.pythonhosted.org/packages/32/d1/a3652fe2c19134d0d15922177821d863cbd7e842d53a21bbe7e8e3a1b5ac/certifi-2026.10.23-py3-none-any.whl", hash = "sha256:e9b54de76e530bb6cf0ccd036787103acb58fd42ba930ea8bf948ddbaa2f61d1", size = 135082, upload-time = "2026-10-23T11:36:14.763Z" }, ] [[package]] @@ -36,132 +103,128 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.5" +version = "7.17.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/4a/740c3134227763ebc30a226d7f2f29b25e35898d162dd8b2dd792be83fb2/coverage-7.17.2.tar.gz", hash = "sha256:d05a1bef52f4d7f1b484a1cf46a8929e50963f910e51396373f06cd3b6248f38", size = 996756, upload-time = "2026-11-04T19:52:34.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/33/e8c48488c29a73fd089f9d71f9653c1be7478f2ad6b5bc870db11a55d23d/coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5", size = 219255, upload-time = "2026-03-17T10:29:51.081Z" }, - { url = "https://files.pythonhosted.org/packages/da/bd/b0ebe9f677d7f4b74a3e115eec7ddd4bcf892074963a00d91e8b164a6386/coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf", size = 219772, upload-time = "2026-03-17T10:29:52.867Z" }, - { url = "https://files.pythonhosted.org/packages/48/cc/5cb9502f4e01972f54eedd48218bb203fe81e294be606a2bc93970208013/coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8", size = 246532, upload-time = "2026-03-17T10:29:54.688Z" }, - { url = "https://files.pythonhosted.org/packages/7d/d8/3217636d86c7e7b12e126e4f30ef1581047da73140614523af7495ed5f2d/coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4", size = 248333, upload-time = "2026-03-17T10:29:56.221Z" }, - { url = "https://files.pythonhosted.org/packages/2b/30/2002ac6729ba2d4357438e2ed3c447ad8562866c8c63fc16f6dfc33afe56/coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d", size = 250211, upload-time = "2026-03-17T10:29:57.938Z" }, - { url = "https://files.pythonhosted.org/packages/6c/85/552496626d6b9359eb0e2f86f920037c9cbfba09b24d914c6e1528155f7d/coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930", size = 252125, upload-time = "2026-03-17T10:29:59.388Z" }, - { url = "https://files.pythonhosted.org/packages/44/21/40256eabdcbccdb6acf6b381b3016a154399a75fe39d406f790ae84d1f3c/coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d", size = 247219, upload-time = "2026-03-17T10:30:01.199Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e8/96e2a6c3f21a0ea77d7830b254a1542d0328acc8d7bdf6a284ba7e529f77/coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40", size = 248248, upload-time = "2026-03-17T10:30:03.317Z" }, - { url = "https://files.pythonhosted.org/packages/da/ba/8477f549e554827da390ec659f3c38e4b6d95470f4daafc2d8ff94eaa9c2/coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878", size = 246254, upload-time = "2026-03-17T10:30:04.832Z" }, - { url = "https://files.pythonhosted.org/packages/55/59/bc22aef0e6aa179d5b1b001e8b3654785e9adf27ef24c93dc4228ebd5d68/coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400", size = 250067, upload-time = "2026-03-17T10:30:06.535Z" }, - { url = "https://files.pythonhosted.org/packages/de/1b/c6a023a160806a5137dca53468fd97530d6acad24a22003b1578a9c2e429/coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0", size = 246521, upload-time = "2026-03-17T10:30:08.486Z" }, - { url = "https://files.pythonhosted.org/packages/2d/3f/3532c85a55aa2f899fa17c186f831cfa1aa434d88ff792a709636f64130e/coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0", size = 247126, upload-time = "2026-03-17T10:30:09.966Z" }, - { url = "https://files.pythonhosted.org/packages/aa/2e/b9d56af4a24ef45dfbcda88e06870cb7d57b2b0bfa3a888d79b4c8debd76/coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58", size = 221860, upload-time = "2026-03-17T10:30:11.393Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cc/d938417e7a4d7f0433ad4edee8bb2acdc60dc7ac5af19e2a07a048ecbee3/coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e", size = 222788, upload-time = "2026-03-17T10:30:12.886Z" }, - { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, - { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, - { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, - { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, - { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, - { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, - { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, - { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, - { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, - { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, - { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, - { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, - { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, - { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, - { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, - { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, - { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, - { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, - { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, - { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, - { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, - { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, - { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, - { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, - { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, - { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, - { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, - { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, - { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, - { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, - { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, - { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, - { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, - { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, - { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, - { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, - { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, - { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, - { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, - { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, - { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, - { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, - { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, - { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, - { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, - { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, - { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, - { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, - { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, - { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, - { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, - { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, - { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, - { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, - { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, - { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, - { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, - { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, - { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, - { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, - { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, - { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, - { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, - { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, - { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, - { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, - { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, - { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, - { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, - { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, - { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, - { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, -] - -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version <= '3.11'" }, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -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" } -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/5e/9d/e3db9071ebeda48e47e5be0fd0404219954e7888f64e7c37df324a79dc3a/coverage-7.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a3f49ac9c9e5fd4b450677eb44f4b14ea9ab71950b2896888c9f6a22d5cd1773", size = 227732, upload-time = "2026-11-04T19:49:22.217Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/260786d936940102052a51cb166f80f4a98e67fb6142aa050f1af48a2385/coverage-7.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3d3d9d79a2b6cdcdde5a1ab1ae89953faea5cb28c0eea17a413ad997bb3cc8a1", size = 228154, upload-time = "2026-11-04T19:49:25.066Z" }, + { url = "https://files.pythonhosted.org/packages/be/80/f5176b7f6aa9e05e37e1bb5eae473c5d015485c36206934f53b2b2795076/coverage-7.17.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b71aeaa4440cbece5498acda30838685793d3bb960aff87ec97fbb546715a5f", size = 260181, upload-time = "2026-11-04T19:49:26.523Z" }, + { url = "https://files.pythonhosted.org/packages/03/9f/4669ca11ce1932cc18fce766ead327ceb41a92055608c179b137067e2805/coverage-7.17.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:01303d9e68b6e6bdc5459b54e3bd0e3539c94c25b50ac7362b7cbcacbc815fbc", size = 262404, upload-time = "2026-11-04T19:49:28.113Z" }, + { url = "https://files.pythonhosted.org/packages/ae/99/efefaa02a4b2876ffa4a389c81a8074c888442b12c64046b448718fa5590/coverage-7.17.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1947bd9d82e405c892c50b0d8dc0c1dea5df2a962129ca238c500a6bcf887509", size = 264497, upload-time = "2026-11-04T19:49:29.773Z" }, + { url = "https://files.pythonhosted.org/packages/19/2f/a565096e5dcf8c511332d397d712b056f06091ab3efc10adb5a4d22b0cc2/coverage-7.17.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cd1a7472a972fed01c89589b1560846e32e942f8329a515c4059d4521c6579a", size = 266507, upload-time = "2026-11-04T19:49:31.507Z" }, + { url = "https://files.pythonhosted.org/packages/ab/09/87f9b02f97179f2c76fa5724e0ce00b9db2bbe16b7e21d89c8fc1f3a77d9/coverage-7.17.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b7fe217808239437b8509b869fde2e0cac48776a68c626bd6b15411b087e3de", size = 260297, upload-time = "2026-11-04T19:49:33.002Z" }, + { url = "https://files.pythonhosted.org/packages/db/12/70733b58d508bc80dc983408b06f1fd0c59fde0eb1379dd9473b3e290856/coverage-7.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:565f1f1a9632d82678525d668a10fe070a285603a9d3581505b0e1a66624ad50", size = 262196, upload-time = "2026-11-04T19:49:34.412Z" }, + { url = "https://files.pythonhosted.org/packages/33/de/0495b108a3091f2389800455b615939867a3ac19f24f0fc186f4d066b4f3/coverage-7.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:189cdffb3ebc53d6133de67952dac7cc88d1fcaff5480623fb00ef0bf1cef918", size = 260212, upload-time = "2026-11-04T19:49:38.462Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ff/39cda949e0e9bbc16620d8b843fd78929a46edda2566e54b9597bda61625/coverage-7.17.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f25209696631752468a25e5954fe119503468b3b29b6fa2c621ae766153c6f50", size = 264136, upload-time = "2026-11-04T19:49:40.266Z" }, + { url = "https://files.pythonhosted.org/packages/85/e5/0eb1bbfc80e1a03318d81fc044dc1c4fc075921f7f618298d42837dafa66/coverage-7.17.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:20faaaf3a49601afbcd6d46c17a594b0ae62631498def44e5eed94789962ea18", size = 259738, upload-time = "2026-11-04T19:49:41.79Z" }, + { url = "https://files.pythonhosted.org/packages/a7/18/098ea8ee450922eec7ab48a35b4a7fd6a49c4a97416d1e4cf66092c24dc4/coverage-7.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7c123b6bbc183a14372036769f033098026aa75fc8fba37d028b0b772356e90b", size = 260937, upload-time = "2026-11-04T19:49:43.467Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/a6ee041c4bc8b6ec7bc9f417daf89e3ee702182a48d573267ccaaed17c64/coverage-7.17.2-cp311-cp311-win32.whl", hash = "sha256:1c3146ef9d14c011517422489784a7ac344477e9cdacb05d9529e912721cd5d8", size = 229599, upload-time = "2026-11-04T19:49:45.014Z" }, + { url = "https://files.pythonhosted.org/packages/5a/df/7f077fb2e71b1d73fe8285be7c6ba43dc085f9eaa2384d4e8e55560073a3/coverage-7.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:51c6a60e63ea993113ea5cd24a401cc2faddc54aa7acfddaffa5d3b8c0161b9d", size = 230202, upload-time = "2026-11-04T19:49:46.548Z" }, + { url = "https://files.pythonhosted.org/packages/90/56/89dcd5907d0cc04b07746e62e1cb896c0f694e5af40aa0c980458825c6c5/coverage-7.17.2-cp311-cp311-win_arm64.whl", hash = "sha256:b08ee3ef53e80e5a50c68b3d9764363e373acba540e1b58daee0a9a7f23fdfb2", size = 230110, upload-time = "2026-11-04T19:49:48.005Z" }, + { url = "https://files.pythonhosted.org/packages/22/6a/a51808ac625c457326fb37b174d35e1badb1324c6def5d9668089c4b14ef/coverage-7.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:289a24443352713745f79c1b6aae4e99ced2f5ff3e86bb630c33c631d50f217c", size = 227838, upload-time = "2026-11-04T19:49:49.546Z" }, + { url = "https://files.pythonhosted.org/packages/31/24/f32ec1a5ef947294252e77c5d40ac0903ba1de4c92638edfcca8629c067f/coverage-7.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da4b0aef427eb96e422b444e5c2c3d38768fedb9141346294c38717a8b15b416", size = 228060, upload-time = "2026-11-04T19:49:51.221Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/3097367327c69e65521b9d9bf8b9d2ddecefa8c21bd08e17bc068bd3db93/coverage-7.17.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:90c6c5f93606b8803bd7f8a0b2ae05df7e35da4504a1c53cfe9294a1731e1f49", size = 260359, upload-time = "2026-11-04T19:49:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/73/65/774fc4ec86807b6ee66b46465bc49b34bb6d291e9bff6ddfa9708ccb4053/coverage-7.17.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:36314997533266bde3d07765289674304d61bfe49165bb1b4fa9be12cb4ba0d1", size = 263687, upload-time = "2026-11-04T19:49:54.351Z" }, + { url = "https://files.pythonhosted.org/packages/d8/72/17b5df5d8211e1880169b93197ba98b21b30ccf8f116284536113b7582c5/coverage-7.17.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1682d98ded28524bc6853cc4e7c65175b3e917f38b2e2ddebb28cafd8ab4de74", size = 264220, upload-time = "2026-11-04T19:49:56.172Z" }, + { url = "https://files.pythonhosted.org/packages/de/0c/603ba7fff1aa4c8064fdf4a1fff6d9fb51c72a5828429dfe858be48e38b3/coverage-7.17.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8eab0a5cff8dbc563d1416a6e079ba3c146712e47167c022a5df5ea01bd76df9", size = 266498, upload-time = "2026-11-04T19:49:57.718Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ca/f2ae6e47e18b35a0dbddb9d8f51be34a6d66b41120f2e0f7363ccc0812f4/coverage-7.17.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:635337a96895f2ffcfe2aa7554468a988f468fb67a8007c1a65e95148c1855a9", size = 260591, upload-time = "2026-11-04T19:49:59.159Z" }, + { url = "https://files.pythonhosted.org/packages/b2/4b/b09540a46395a6a1052ad114bf366e383c64e223374f4911a85620667930/coverage-7.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:79cd2d9742419b6d1aaeb1f3fb77a8e6a01c09ebe5bd1b27011752410a5e3781", size = 262107, upload-time = "2026-11-04T19:50:00.607Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c4/42783cb43481fafafffbc7be7fe6026258ffd30a575707733109c9ef0679/coverage-7.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b0209fff1ca92f1e2e344d54b5b3def74efbdb46ba807cd81976a5e9e569cac3", size = 260271, upload-time = "2026-11-04T19:50:02.06Z" }, + { url = "https://files.pythonhosted.org/packages/37/07/192ee81dd1ec4da58ae0f03fdd0045d2b821007e4194f7704d6e495733f0/coverage-7.17.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7a3984fb8c2407721f9bc05573fc9a770752efc756d55068ccbde1fe53e42233", size = 264320, upload-time = "2026-11-04T19:50:03.613Z" }, + { url = "https://files.pythonhosted.org/packages/dd/94/8d983310d98299a19ba23efbb324492f4bf8d8668d610eacd304e451f138/coverage-7.17.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:698ca1575135215be25ac9c5d14f0b5455135d29602b7ee81e511d64a0435c85", size = 259774, upload-time = "2026-11-04T19:50:05.363Z" }, + { url = "https://files.pythonhosted.org/packages/1a/85/6c923d71c76566c6c65a2af104d39d69d44ae708a700de017e6c56fedfc6/coverage-7.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:686b6f8f9e57afc3a44096b17ee8de7e11e0a12f2da819e25047af1eb3b0fc6f", size = 262258, upload-time = "2026-11-04T19:50:06.807Z" }, + { url = "https://files.pythonhosted.org/packages/54/f9/fd3c37b09891bfdfece486a6145345bf2b32332045a678afd293a7f2f05d/coverage-7.17.2-cp312-cp312-win32.whl", hash = "sha256:ee083412170118074bcc74361fcbc7104eb6d0b7c9c75eb3b84a4c324410c531", size = 229659, upload-time = "2026-11-04T19:50:08.184Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b8/4d395163c9bfae0e991abbd14dea9c9bbb4ade904e5f297cbfb7b8150216/coverage-7.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:e269dddf5ebac20dfcc5a2a459528a9d221ab828cb31dfdf19c7cfacdbcee673", size = 230240, upload-time = "2026-11-04T19:50:09.715Z" }, + { url = "https://files.pythonhosted.org/packages/a3/02/1024b8d4f60a9ecc0ee41a6000425caa63855d63e51da84c93c0b1dff894/coverage-7.17.2-cp312-cp312-win_arm64.whl", hash = "sha256:bac3c79c31f45274117ed64c65efeaed85665668aeba7d809c7c38801ada6ed4", size = 230043, upload-time = "2026-11-04T19:50:11.187Z" }, + { url = "https://files.pythonhosted.org/packages/e0/32/d750d7b10b0a9436349b3fb78491c5bb8f9b524f01ae6994d935d77d6ab8/coverage-7.17.2-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:882cade9b6dc43b2bc819bb444c0a13e4ed1f669a50e0de74adeec54fce486f3", size = 228978, upload-time = "2026-11-04T19:50:12.782Z" }, + { url = "https://files.pythonhosted.org/packages/63/33/468f8c63aff5deef81bfb8e44771f68ebba328c0aa46c8a6ff27dedd0fda/coverage-7.17.2-cp313-cp313-android_24_x86_64.whl", hash = "sha256:f5a875e92f10c6463027d8151459f2d92b4c13527402ed1301eaddabd79d8cf5", size = 229048, upload-time = "2026-11-04T19:50:14.255Z" }, + { url = "https://files.pythonhosted.org/packages/10/91/c8723e48c1497a3c56a3194973a416a6f0fb24560425685f960888e03b20/coverage-7.17.2-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e8da5e158d13556457f69245deacf6d8401a2c9c31afaddd18bea691e849b5fd", size = 227293, upload-time = "2026-11-04T19:50:15.857Z" }, + { url = "https://files.pythonhosted.org/packages/61/4f/2f57b3dc6ef31e12a99e6dcb820c4ab04d973ea1466f73c856bf920a740c/coverage-7.17.2-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:42133a2647982db7968db138c0f4a57f5836101d8d504600a87ef8ebfa22a4cf", size = 227599, upload-time = "2026-11-04T19:50:17.418Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fa/967e829238dda21c4bce17aef2f739187678c8e6e6fc880a8454945a7433/coverage-7.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e6c89d63d43738712ce1febddb7758ffeb56de08f1b2be68aeb9a9c7fee712c7", size = 227727, upload-time = "2026-11-04T19:50:18.924Z" }, + { url = "https://files.pythonhosted.org/packages/9d/df/803ba08e8937759dc3f18b93ece82f81b2fb446e3846e4d60a06a4e77647/coverage-7.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f94d465ee00ebe694ca866d66bbe45fa04618b1afcb377de934694f624fa7cf6", size = 227987, upload-time = "2026-11-04T19:50:20.867Z" }, + { url = "https://files.pythonhosted.org/packages/4a/12/92dcd9206e7bb817af42b8663cfeefdbb2c5a6c92cc7069d43e6892bbdeb/coverage-7.17.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7ce0acb27a823e8e5169282f538f0e182854cc8b472b88d45456f09b6fe364fd", size = 259154, upload-time = "2026-11-04T19:50:22.328Z" }, + { url = "https://files.pythonhosted.org/packages/48/2d/6b028df8050caef4ce0df177686c92dba16ab7cf004040e5518f95b57021/coverage-7.17.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:70daa8e4b49541b51c0baeba0d802b0d14d3214daeebc071b3db0b0c7dc84646", size = 262065, upload-time = "2026-11-04T19:50:23.745Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/429b500937ac446e36fcce517d040b0b8bd04a4bfa087bdf864175553fea/coverage-7.17.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:95bd8dc5b356f78f10a967ec50f8902d56f24c623753ac9fce0aba9d6213d5c5", size = 262997, upload-time = "2026-11-04T19:50:25.223Z" }, + { url = "https://files.pythonhosted.org/packages/c1/91/263224a77de3ab1ee42d110b09f1ac9a02638794d3e002a859822638256a/coverage-7.17.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a5565a9dd715a5a94ffcf9a0ad4b6b1501b5441fd67d660e78742adff5d37a0a", size = 265062, upload-time = "2026-11-04T19:50:26.829Z" }, + { url = "https://files.pythonhosted.org/packages/0f/44/99b72f2961f4ba54ac14ccb016f3b212cc9a9dfda4c37e07a208efe01f9f/coverage-7.17.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:091eba4248d31b03828fc657c96d14adfaa8754f5aec7b73298b82894858db06", size = 259201, upload-time = "2026-11-04T19:50:28.386Z" }, + { url = "https://files.pythonhosted.org/packages/be/a7/e97f63a7c345077e4e398d15e3aa5068edc28cb061f997f71fd0e32703e1/coverage-7.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d2742952851c7f88b1484385f68c42c6388153520efb40ff09fc852c3783aa5c", size = 261155, upload-time = "2026-11-04T19:50:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/16/0c/73e470df7db83962e2cdd6c1a7ca7dc8f8632c17a6d378d6d9124ff8bc6b/coverage-7.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1da1999e3c371edd669b48a1029c86dc0474cdc6ff152d58cb3f15cb3337af72", size = 259077, upload-time = "2026-11-04T19:50:31.437Z" }, + { url = "https://files.pythonhosted.org/packages/50/43/db8bc1b92d933b0b5f57d0e3377594c4f78ea31e84e8fbc22627fd17ecf4/coverage-7.17.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7804a6022fcd5e1d4de4dc676e985d6aea0119249e88c041e943f3cd07953527", size = 263172, upload-time = "2026-11-04T19:50:33.014Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f4/3e9e5cc9ccc911f8fb9595053da1ce58b9de8941b27e2f2f7058cdfc87f1/coverage-7.17.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6bdf3821fd0740b5369b9ab58b4e9d0d68a2c5c75a7ee0cd3beb624f9f15e4fa", size = 258926, upload-time = "2026-11-04T19:50:34.513Z" }, + { url = "https://files.pythonhosted.org/packages/72/9f/0d67883bf55adf50424728937cbee54d76fa137817e6bb20f97061d01816/coverage-7.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e09c37817768bb71fe3e70b6ce5e6ac1a0cd7398536c2f30a4460218c7b66e93", size = 261006, upload-time = "2026-11-04T19:50:35.956Z" }, + { url = "https://files.pythonhosted.org/packages/a8/22/71965edc14736073b55a40e521791d9233d5c3194fa6887a94f4431fbbcc/coverage-7.17.2-cp313-cp313-win32.whl", hash = "sha256:c10f2a2748dd084e44811bb9b26979763af9003115d9bf93c1da69011944719f", size = 229602, upload-time = "2026-11-04T19:50:37.564Z" }, + { url = "https://files.pythonhosted.org/packages/47/0d/715cb37f51f908a6f6f33fe8c6fc95ab638b17cb014b1650daae0893c160/coverage-7.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:8dc07ed34bcb206f69d5b54fd3874f7bf39ac941c9f7aec2b53b29874fcfd77a", size = 230329, upload-time = "2026-11-04T19:50:38.988Z" }, + { url = "https://files.pythonhosted.org/packages/39/f2/a5099bdc2a530f29c818a1c7e706669451c10e5f0eb62dcff76ef3568668/coverage-7.17.2-cp313-cp313-win_arm64.whl", hash = "sha256:bb74283661a239fdaf738ac0b07db4ed9b476c7532d3e713abf0f6a072781e60", size = 230055, upload-time = "2026-11-04T19:50:40.479Z" }, + { url = "https://files.pythonhosted.org/packages/96/f9/30d6ff7a66e1519d79940fdee542ead9a63601de60be5cc9fe62c74bab58/coverage-7.17.2-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:0439f77deb5c023003533ae8909a9aa1e174c847d655327035b1e542c7c8a54e", size = 229094, upload-time = "2026-11-04T19:50:42.193Z" }, + { url = "https://files.pythonhosted.org/packages/1a/db/82c722c37dce98fe1abc953768903752bdd415dfe8cfeb7b744a7236a735/coverage-7.17.2-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e86a66f96d80c770daeed78394c1d4f6e8d03e5a3e08c5dab5275d56414f296a", size = 229213, upload-time = "2026-11-04T19:50:43.779Z" }, + { url = "https://files.pythonhosted.org/packages/7d/07/71b68d870975776cb185f11cd80e053b01739f3646488fe8b9721744f56a/coverage-7.17.2-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:87b30f3daa0bedda6b770b303410a6def007844772aaa143dc52675b3d95cd8a", size = 227374, upload-time = "2026-11-04T19:50:45.258Z" }, + { url = "https://files.pythonhosted.org/packages/aa/23/ed60f79576175e108c77df2bdc116f46e367aab108542ddeecaab752f0f6/coverage-7.17.2-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:3f219efbc3ec71c377e278dee201633e1d4ad0d2d9bdaddfd9489b2869d584c4", size = 227676, upload-time = "2026-11-04T19:50:47.095Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/60a51850d3ff659b82cc211bc98fa126aab2ac9b81bd4ed6abc3e3720a09/coverage-7.17.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:36248fb6c03341501169539bcab35eb3a59159100be1246fad3c14cb73c17685", size = 227915, upload-time = "2026-11-04T19:50:48.581Z" }, + { url = "https://files.pythonhosted.org/packages/77/80/a9539c15076fb9b95d8cbdb7f0caec90857d80bddca3eeca439f5e2e64e2/coverage-7.17.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2a6281011cf85699763bf88ab55e0774f6696f87b390f39152b14ecefadccb59", size = 228072, upload-time = "2026-11-04T19:50:50.073Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/0a84b6032511834105fd4f8a8b4e76927783cea03dc4796032d5c4bfec8b/coverage-7.17.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3dfefb1ec16ca1a01e5da40ba162e268ced8bd3624b9eba4e9384ebfa14af7ec", size = 259991, upload-time = "2026-11-04T19:50:51.665Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e4/a3e6b8bcc219b00ea91e36f071c2659b253e5d6e1b258c31eb4524231718/coverage-7.17.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:34e5c7a2da36804c6e6f10b368eae01ad56f5a4ac5ecce7297300a3d46fdef2a", size = 262995, upload-time = "2026-11-04T19:50:53.236Z" }, + { url = "https://files.pythonhosted.org/packages/05/54/a666b65c5bb145d8796b7902ff1c0f60600a6c2a08949e20f2fb7af15de3/coverage-7.17.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e6bf531deaebf8f69ba6015bba3fb817cab6694dfb69ac2a708889dc59a3926e", size = 263980, upload-time = "2026-11-04T19:50:55.366Z" }, + { url = "https://files.pythonhosted.org/packages/a6/73/ac9d06da9e0c143b874a68b17d8dfeedc45acdf1bdcee5b22a42bece098e/coverage-7.17.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:88d4dc83b6ad09a2134cad9d8ec780b274b17ac7dfba8c2c6e92e3bc468fe85d", size = 266457, upload-time = "2026-11-04T19:50:57.04Z" }, + { url = "https://files.pythonhosted.org/packages/87/b1/8a500e16cf787383b00b5a86eb50a490b4d8ebde505f92e1aaca515afc0a/coverage-7.17.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dc17707fd463b4dfab53792e41d50a23316ac6b98539007aa2388a1d44a8535", size = 259768, upload-time = "2026-11-04T19:50:58.508Z" }, + { url = "https://files.pythonhosted.org/packages/f7/be/642762c5973b89e2b8bf66c95778bdd80e96ceea442ae8fa753732597b73/coverage-7.17.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:df0a34d445eea64f3766e9ca2ff57bcde600a89c7c0330434a7a37e8d6e2549f", size = 261831, upload-time = "2026-11-04T19:51:00.25Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b1/f7d6c0ef0a4aecf3f91f19df4ab3fa131fec8c89f554187511716584c9f5/coverage-7.17.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c5a98aff9eb681312ed403ecb7beb5f15fd25d6e0acc2554b9ca4b4d6f0b6705", size = 259871, upload-time = "2026-11-04T19:51:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/c7/c0/3ae087d52cfa5119ad9ecbd193c40089924d1101cef7333f15a86dccb444/coverage-7.17.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:7565c17092f2f4021abbe3f5007300c224f362364a388f5ddad9216dab44b611", size = 264169, upload-time = "2026-11-04T19:51:03.365Z" }, + { url = "https://files.pythonhosted.org/packages/c5/95/d324108991f892cb361ef8b2f91bd4cda9c2563e0954247409e9e6b911ab/coverage-7.17.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:555257578e20a46020c27b2af4ef99059cd906aa9b8e163baf9da9a5afb65a64", size = 259502, upload-time = "2026-11-04T19:51:04.91Z" }, + { url = "https://files.pythonhosted.org/packages/14/b5/13a62eac66cecbaaa098bb753a3693b44454a4d21d53db4f1a0953f35f0b/coverage-7.17.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:460ba2139866e758c3a8e9d9422fdc889942bcbc5ccaf710dc4fbd8707d14aa8", size = 261613, upload-time = "2026-11-04T19:51:06.414Z" }, + { url = "https://files.pythonhosted.org/packages/53/13/9ea31cfa72434449b9a98666760169d1e0ab31b3f6cdb12e68149106668e/coverage-7.17.2-cp314-cp314-win32.whl", hash = "sha256:e72cf9556f799c3fce7597c1bd7dbfc9104befbd08088deb3b9ae9fd98117c02", size = 229842, upload-time = "2026-11-04T19:51:08.61Z" }, + { url = "https://files.pythonhosted.org/packages/f6/cc/aa8f34b014979c973740f5a5ed6861ecfc6009082c70e72030ca1519e0f8/coverage-7.17.2-cp314-cp314-win_amd64.whl", hash = "sha256:205f9e329d9d5e47c320f8712326a01620689edae50531cc8934511e2c5e8f7e", size = 230474, upload-time = "2026-11-04T19:51:10.167Z" }, + { url = "https://files.pythonhosted.org/packages/49/64/16afaa19b3886dc157c3c5ebace5241b84b96a005cb28545584e66b80895/coverage-7.17.2-cp314-cp314-win_arm64.whl", hash = "sha256:a5909dd087006c70c128297312ba67d877123e871100645d9bd73ae021a4d01d", size = 230325, upload-time = "2026-11-04T19:51:11.63Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/acc865e1411c8ebc5c5d230367f3bfed5c605ed54bb41b84fc58f4e7f137/coverage-7.17.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:674bcd023a3743666d4d7a15238e57a00c968553780a36c80d3779fc410b912c", size = 228384, upload-time = "2026-11-04T19:51:13.729Z" }, + { url = "https://files.pythonhosted.org/packages/10/98/02c37c6a928ccfc84b7a43eda44d96d3b1ccbc93989c178754b7e67e7650/coverage-7.17.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ba45dc072bfbf1798e68ec81379b73473fc912174bff9f91924d77711a1d2b30", size = 228758, upload-time = "2026-11-04T19:51:15.394Z" }, + { url = "https://files.pythonhosted.org/packages/28/ab/d503b0aaebb92b171260469463915af5e16a64457b289d4cb844e8c366ac/coverage-7.17.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d8a5dc709da9b90ad5cc859a9b88c9b801e65ca2fbf7cbdbed5b9cbf457fcc9", size = 268749, upload-time = "2026-11-04T19:51:17.179Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e5/6d6ba15bde22fcccd07e14305be14291d041f732771a73f787297445d3e7/coverage-7.17.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:248873a144c067f981742ff4bbfb48b57e281a8cb7a3821d4e293bdb50b47970", size = 271301, upload-time = "2026-11-04T19:51:18.893Z" }, + { url = "https://files.pythonhosted.org/packages/63/e7/bd6dcec5b8130f5bd8679087a2bc3a50a53c4ee78d5ec449610020088ff3/coverage-7.17.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0332e2b27fc5cc761d6b8a33118b958e6a46be7f00fd30e03a5cf103350c0b3", size = 273924, upload-time = "2026-11-04T19:51:20.605Z" }, + { url = "https://files.pythonhosted.org/packages/55/c1/b3f06aa1f1c51f4295fdfa6500eb7409aaa6273cf5f27bafc25217f3547d/coverage-7.17.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:27fc4384a1154f7df30d304ccddc05ef457368c065670ecbd392606279b3329a", size = 275412, upload-time = "2026-11-04T19:51:22.09Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f0/b68ab3654af3541488b26856e3043c13c782d8e6104fc8149746ecaa83f0/coverage-7.17.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:603140eeae3f9adc562f91b1b98d4d1fc491bd1f66c5082482c568d0b7891bb1", size = 268638, upload-time = "2026-11-04T19:51:23.653Z" }, + { url = "https://files.pythonhosted.org/packages/fd/16/4a9d56bd4ca4572cbd20f9843813623cef1b88b15f979ec4f381704d2f84/coverage-7.17.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:be0fec33b69c40f845c76f67fb4495b62a84da8c31a579dc6e18c5a63fa6a1f3", size = 271211, upload-time = "2026-11-04T19:51:25.678Z" }, + { url = "https://files.pythonhosted.org/packages/78/d2/1c04b38bab92d2b7d62718d170b5bea43755fefee7e29cc056448c433d92/coverage-7.17.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:960e199965e506d78f62252355397a05c8cee8072ad2d82d932745517d566539", size = 268638, upload-time = "2026-11-04T19:51:27.262Z" }, + { url = "https://files.pythonhosted.org/packages/26/c9/2c639e9aee78b0522cb3dc245de09685e34b479bcc45d4d2a277d0de1174/coverage-7.17.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d7a363c0788435ff5324d9582d37c63f215c75f974d755cfd6cfc9fbe0e64b04", size = 272935, upload-time = "2026-11-04T19:51:28.96Z" }, + { url = "https://files.pythonhosted.org/packages/79/1f/11bdaf03c1b12f88094b57420b4238d3974ebec6a7d674b645a54e24f90c/coverage-7.17.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:afd81658f17b1a50bbc1d247a61fa435be0be471246136eb946f9cbfc437a4fa", size = 268057, upload-time = "2026-11-04T19:51:30.44Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f6/43d40d1b4d4892e54c68a241cd5b92efed4d5934109e0e5e24a94759f732/coverage-7.17.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d88fcc24470d73ed4360e910f8549718b913ba56fb8775d9ef929a647835dfea", size = 269986, upload-time = "2026-11-04T19:51:31.913Z" }, + { url = "https://files.pythonhosted.org/packages/8e/33/6874185b791c71684c401fc74abd71bbdb102e8ee41c8f380cf25af41d29/coverage-7.17.2-cp314-cp314t-win32.whl", hash = "sha256:2c42c222939b8699e7bc0e453149627f211ebbbb2f74c1a04331b0cd9834e2c8", size = 230160, upload-time = "2026-11-04T19:51:33.304Z" }, + { url = "https://files.pythonhosted.org/packages/16/e0/9057b4cf962ca6089b144f0d93a748f5e2d13876dd0f33f9d5a2c2ec49d2/coverage-7.17.2-cp314-cp314t-win_amd64.whl", hash = "sha256:b94c509e26603ba7f8cb1bf8bb63967c7be533493be5668d8442cd06ec853d76", size = 231086, upload-time = "2026-11-04T19:51:35.002Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/bd626297c942380279dff323e1d8a8937990d553119a4f887358488b1b54/coverage-7.17.2-cp314-cp314t-win_arm64.whl", hash = "sha256:eca90e028932de597e2c50f454ad8cc631281e14d20b9f801a649ab555deb580", size = 230657, upload-time = "2026-11-04T19:51:36.742Z" }, + { url = "https://files.pythonhosted.org/packages/ac/92/ff8badc05ff0c4ed26ca62fb3d155e9014ff65768b76d5f399e63d9e9a79/coverage-7.17.2-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:cbf751ae86ef0d80e719915c087b05776c8d9300141f6c3e56f536d78a62c301", size = 229092, upload-time = "2026-11-04T19:51:38.222Z" }, + { url = "https://files.pythonhosted.org/packages/5a/8b/c5a5392e5a5dceb68f3b1b8cdba925c7997699f351d811cd4a41add9c29e/coverage-7.17.2-cp315-cp315-android_24_x86_64.whl", hash = "sha256:249c4b4eaac9ced6fb85e465027ca26348111c2760108fffc356a16be4f54cd9", size = 229212, upload-time = "2026-11-04T19:51:39.738Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bc/9259a1fc49ad663861cbfb3df6a99d4c8f62a11b242a2db3358822d28b17/coverage-7.17.2-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e2fd4e664a34854cae8db58ca6132fb7b5a7efc67b14233e797194bd0d63b6b0", size = 227390, upload-time = "2026-11-04T19:51:41.16Z" }, + { url = "https://files.pythonhosted.org/packages/20/ad/83f03da8ab846af246d4a47b651e5d90229a4c3314e31e8eecade63b378f/coverage-7.17.2-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:0712a43e39410aa567f2e85846490f8dd6c839820e7edef2142767c5c03162ba", size = 227690, upload-time = "2026-11-04T19:51:42.658Z" }, + { url = "https://files.pythonhosted.org/packages/f3/08/e110687f24245edbbe9c0986a6ccd04f1f9a6e90e329cbd4347e2b81d76e/coverage-7.17.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:298849ca0e385767096995cb57f72059b469f43f9927b8393f29d134f7bb1062", size = 227922, upload-time = "2026-11-04T19:51:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/93/24/7cfcd6adb4a0c18a10de12fd695edbf8cd121fdca0a6a63cea1bffc029fe/coverage-7.17.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:81a432793440edef268dbadd493393bd6f70fa4b87f76c58114f7f77f1ccfaea", size = 228094, upload-time = "2026-11-04T19:51:46.108Z" }, + { url = "https://files.pythonhosted.org/packages/54/7a/af167a0bb6d0564209d35c8039b290327ecf7b3e34f838f9360b56d3a03c/coverage-7.17.2-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d14872ccef35972c4ac290f2432f43d0a7bb230cae2b5ed26871ab86221e8b5f", size = 259940, upload-time = "2026-11-04T19:51:47.599Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c6/4f3931c3ff80a6f2608209181adb00527970a4b43b64c3aa323383c37d51/coverage-7.17.2-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3bd065e75c077d6de8cd3fe68699a8b47be3ec595af6b66e3629926839094609", size = 263632, upload-time = "2026-11-04T19:51:49.047Z" }, + { url = "https://files.pythonhosted.org/packages/3e/71/31a4945459008952061147ea9b85853902507d865a7272a9f64be0c4a093/coverage-7.17.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d3626b9816f7eadd0993c3703a1d8ed124b579cebe5201b446f1c0d551fa245", size = 264713, upload-time = "2026-11-04T19:51:50.569Z" }, + { url = "https://files.pythonhosted.org/packages/82/6b/a4eb258b0373034f8f19aeea4c1bc4c89942b5b228812ee9952f033dc611/coverage-7.17.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9639c2ea019c948b52067a0f74485039eb84b7a2de828ba6d178e663fb2b8bd", size = 267091, upload-time = "2026-11-04T19:51:52.09Z" }, + { url = "https://files.pythonhosted.org/packages/02/ae/9410f2f4bbaa698ff3869c7c09e0fa10d74482a1ec8040063157612f07a0/coverage-7.17.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:19d508b0611486f8e23e28abcba77795ee865ff0e0cd936349ba692c8d0d8ca4", size = 260326, upload-time = "2026-11-04T19:51:53.508Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ee/97549ec1bdf6ff6d17612dbcf4b2205daa74510273ac1efd1460e3db275c/coverage-7.17.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cfe64d2501806a04507217852a3132a9a9076321435500bf3b11943a5c872732", size = 262595, upload-time = "2026-11-04T19:51:55.514Z" }, + { url = "https://files.pythonhosted.org/packages/36/72/76e04605522a6dd81bbf43e1b407ca611f415c04b67e18c123ee440b1d69/coverage-7.17.2-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:4274acd35aae5bbbd0908792260819b9720b171ccc099a8168f7c20ef5d6ab3a", size = 260546, upload-time = "2026-11-04T19:51:57.068Z" }, + { url = "https://files.pythonhosted.org/packages/30/8d/163e027635790ab232f3ec0261d2a26bec34c377bea2096554261444d0f5/coverage-7.17.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:85bccf6fe19ea3747147387971465f6a06197d6b9f6b644a97ba90ca5b09c5cd", size = 264877, upload-time = "2026-11-04T19:51:58.575Z" }, + { url = "https://files.pythonhosted.org/packages/18/35/11dd585a06be350285d42c17aa2fccd0af18d962b6979f03dd1a258eb705/coverage-7.17.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:d5024913651813801fd43b65b6409b0d50f8bae95316c8f1512dfbd8ca96cb1f", size = 259858, upload-time = "2026-11-04T19:52:00.361Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ed/f1f3b4bf6081122bf1820b1f38352377a75a2d5f39bb6331598e165b64af/coverage-7.17.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:99b64a12fe6017c254a8027790e0eaa22eaeec201e7dfda5eaac176a6984ef27", size = 262401, upload-time = "2026-11-04T19:52:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/ab/bb/61e07a606a9f4d0f5391ac9b62b1f008a997f87860ad233f99833fd6e6bd/coverage-7.17.2-cp315-cp315-win32.whl", hash = "sha256:74d91af590514dda16165d5d7cfa94183150c6a6a80a99f61068369ae3959455", size = 229840, upload-time = "2026-11-04T19:52:04.813Z" }, + { url = "https://files.pythonhosted.org/packages/86/40/2d4980d3374a023a34c5ea7cd5d2b93c02c5588a324ca7faf8a0a8c7dbac/coverage-7.17.2-cp315-cp315-win_amd64.whl", hash = "sha256:075566a44289afb634ceaafa7d8d377bb2667a5b4a90fbee2609efdcd015c66e", size = 230461, upload-time = "2026-11-04T19:52:06.727Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c8/b307327b1256f9ed8ffc3271db041913835cfee065fbaf45105978093c08/coverage-7.17.2-cp315-cp315-win_arm64.whl", hash = "sha256:be699e76b57fede8920a0f568eff23b270d9004414b7cf6744bdda851638be6e", size = 230322, upload-time = "2026-11-04T19:52:08.191Z" }, + { url = "https://files.pythonhosted.org/packages/40/18/0912e37f23c101faebd7c5e544c6ba6bd750715f81f2a2c7ea66f83a2295/coverage-7.17.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:ffbaadf8612781358b3f949ad68dce26c5a35beaafcc48125fc231dbcce7d362", size = 228380, upload-time = "2026-11-04T19:52:09.892Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a0/c4ce9bbf82b2e9dbf2e0326004c8c37d87a275f515fc7bb22344a6162050/coverage-7.17.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d4004fce7b3a8ae5ef2af835670763f12723e9b9d74837697607b89a17f77312", size = 228776, upload-time = "2026-11-04T19:52:11.351Z" }, + { url = "https://files.pythonhosted.org/packages/3f/05/9c7429c87589534755398a58fe9837fdb2870a22d99e87623fcb261e1d4e/coverage-7.17.2-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a838a10afb5b60a6cc6315348b176badaf2409d821208a3e3cda1dc608129e23", size = 269129, upload-time = "2026-11-04T19:52:12.815Z" }, + { url = "https://files.pythonhosted.org/packages/e9/22/d1ad2ea7c8871d66f8453d33cdeec39986e6e1f2a104628e9bcb171f0209/coverage-7.17.2-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0fc17882975fe485cfc95fb2f20eed937e05e0c0d8ffa5048857cda3aa838e8a", size = 271967, upload-time = "2026-11-04T19:52:14.685Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1d/5038de4bf6549903ec3d86959c6f18aff54911616b47aa96f4d6a5660c93/coverage-7.17.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a65faa1ee98e090a5fd7c116f6d3ab5c6a85fcc46849b8494835608715836399", size = 274667, upload-time = "2026-11-04T19:52:16.122Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ea/68a26b4b5eb255d92de906fd66e8d720b1df15e705949ea9a9cef2cc7f8b/coverage-7.17.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c17f716c837db550a202e7732cdfaa726b8515cba88be42193616913970d223", size = 276001, upload-time = "2026-11-04T19:52:17.639Z" }, + { url = "https://files.pythonhosted.org/packages/70/86/0f4238f09b22bc03a907346fabcabe5449626879cdedbc1edd362ab0e570/coverage-7.17.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:545ca42488c7f0e62324fb2621aa05737db843b75e84e65a271bbf976f496f66", size = 269687, upload-time = "2026-11-04T19:52:19.169Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/1885255425900f691c543d88394d9f0dacd2f2dc78422e660c747ca9c95c/coverage-7.17.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:8de787f533b13aeeea5d7139610eadb56f249cc739840b973603d1d18d0fb023", size = 272158, upload-time = "2026-11-04T19:52:20.714Z" }, + { url = "https://files.pythonhosted.org/packages/02/d6/0111601342d91e25d13e22a55f34600aa387c55b5027b90e78d941911821/coverage-7.17.2-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:485b2ed9e13b7e9209276843d7147e2f1cd1c816582655650a64c8f922093c58", size = 269182, upload-time = "2026-11-04T19:52:22.204Z" }, + { url = "https://files.pythonhosted.org/packages/70/48/d9bdc5346a9ad09711f55b6901ee05b57305e6b2b66e6edfd50f81f0d410/coverage-7.17.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:a472e23a7514478e1c8124ea056424d681e3173975650b0a06b6181658f1009a", size = 273790, upload-time = "2026-11-04T19:52:23.677Z" }, + { url = "https://files.pythonhosted.org/packages/98/75/3a4d8ce4aa59ed2e898364acf923b39471689d51876bb978c42e664cdb79/coverage-7.17.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:0a623de5c57c5c3f3aed830c73a3a5aad91dbf76d53bc30ec161be758c8ca408", size = 269277, upload-time = "2026-11-04T19:52:25.19Z" }, + { url = "https://files.pythonhosted.org/packages/4a/11/d15082e504e7852a25848ee0dcb59453579ec923c6d1f3010316e1fb6118/coverage-7.17.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2ad189c65706306644399e48530ad1aea1dfb311bd21d988df1ace422d09f3f8", size = 270890, upload-time = "2026-11-04T19:52:26.745Z" }, + { url = "https://files.pythonhosted.org/packages/f1/9d/6460101b0191afae9ed84c8d8136cc8cc09675f376ce08acb3118b8c27be/coverage-7.17.2-cp315-cp315t-win32.whl", hash = "sha256:6fd6f218e39a621a30dbaf9543831fa10fe713afc8a44b52fc42cfe277412609", size = 230148, upload-time = "2026-11-04T19:52:28.164Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f2/3462d9616445a8cb36ec2e027d1c0ae82f6f90f224d9851ba0b22b7425d6/coverage-7.17.2-cp315-cp315t-win_amd64.whl", hash = "sha256:ef07aae0a666ef5edcab5bc1bb590468c6f4120a8cdad50a010d042a59ab6e77", size = 231074, upload-time = "2026-11-04T19:52:29.714Z" }, + { url = "https://files.pythonhosted.org/packages/4c/06/abeca0cf853e77aba9464d4ada4603c8b6e744b667c13e1d4eefecc487af/coverage-7.17.2-cp315-cp315t-win_arm64.whl", hash = "sha256:c68f64d96857749a215657c172f7144ed026063489686b901774f271997923c2", size = 230648, upload-time = "2026-11-04T19:52:31.267Z" }, + { url = "https://files.pythonhosted.org/packages/da/91/1805cc144497d76a436f66a21fa57492bee7ed1257826fb992f03b9b2b6f/coverage-7.17.2-py3-none-any.whl", hash = "sha256:e674027ceb6bfd959f1bb035000fd20fa8b717dc6765adaaf8bca347822c8c7d", size = 219280, upload-time = "2026-11-04T19:52:32.726Z" }, ] [[package]] @@ -203,172 +266,205 @@ wheels = [ [[package]] name = "idna" -version = "3.11" +version = "3.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +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/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { 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]] name = "iniconfig" -version = "2.3.0" +version = "2.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" }, ] [[package]] name = "librt" -version = "0.9.0" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/6b/3d5c13fb3e3c4f43206c8f9dfed13778c2ed4f000bacaa0b7ce3c402a265/librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d", size = 184368, upload-time = "2026-04-09T16:06:26.173Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/f5/9dc696772d241814bacac7880bac32f2930b5a6ebc1f85317b83161a011c/librt-0.16.0.tar.gz", hash = "sha256:ac38d6d8d66bf3d744148dbbc0b8e193e195a51e364ed55e224631f5721891fc", size = 219839, upload-time = "2026-09-29T00:55:32.891Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/4a/c64265d71b84030174ff3ac2cd16d8b664072afab8c41fccd8e2ee5a6f8d/librt-0.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f8e12706dcb8ff6b3ed57514a19e45c49ad00bcd423e87b2b2e4b5f64578443", size = 67529, upload-time = "2026-04-09T16:04:27.373Z" }, - { url = "https://files.pythonhosted.org/packages/23/b1/30ca0b3a8bdac209a00145c66cf42e5e7da2cc056ffc6ebc5c7b430ddd34/librt-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4e3dda8345307fd7306db0ed0cb109a63a2c85ba780eb9dc2d09b2049a931f9c", size = 70248, upload-time = "2026-04-09T16:04:28.758Z" }, - { url = "https://files.pythonhosted.org/packages/fa/fc/c6018dc181478d6ac5aa24a5846b8185101eb90894346db239eb3ea53209/librt-0.9.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:de7dac64e3eb832ffc7b840eb8f52f76420cde1b845be51b2a0f6b870890645e", size = 202184, upload-time = "2026-04-09T16:04:29.893Z" }, - { url = "https://files.pythonhosted.org/packages/bf/58/d69629f002203370ef41ea69ff71c49a2c618aec39b226ff49986ecd8623/librt-0.9.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22a904cbdb678f7cb348c90d543d3c52f581663d687992fee47fd566dcbf5285", size = 212926, upload-time = "2026-04-09T16:04:31.126Z" }, - { url = "https://files.pythonhosted.org/packages/cc/55/01d859f57824e42bd02465c77bec31fa5ef9d8c2bcee702ccf8ef1b9f508/librt-0.9.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:224b9727eb8bc188bc3bcf29d969dba0cd61b01d9bac80c41575520cc4baabb2", size = 225664, upload-time = "2026-04-09T16:04:32.352Z" }, - { url = "https://files.pythonhosted.org/packages/9b/02/32f63ad0ef085a94a70315291efe1151a48b9947af12261882f8445b2a30/librt-0.9.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e94cbc6ad9a6aeea46d775cbb11f361022f778a9cc8cc90af653d3a594b057ce", size = 219534, upload-time = "2026-04-09T16:04:33.667Z" }, - { url = "https://files.pythonhosted.org/packages/6a/5a/9d77111a183c885acf3b3b6e4c00f5b5b07b5817028226499a55f1fedc59/librt-0.9.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7bc30ad339f4e1a01d4917d645e522a0bc0030644d8973f6346397c93ba1503f", size = 227322, upload-time = "2026-04-09T16:04:34.945Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e7/05d700c93063753e12ab230b972002a3f8f3b9c95d8a980c2f646c8b6963/librt-0.9.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:56d65b583cf43b8cf4c8fbe1e1da20fa3076cc32a1149a141507af1062718236", size = 223407, upload-time = "2026-04-09T16:04:36.22Z" }, - { url = "https://files.pythonhosted.org/packages/c0/26/26c3124823c67c987456977c683da9a27cc874befc194ddcead5f9988425/librt-0.9.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0a1be03168b2691ba61927e299b352a6315189199ca18a57b733f86cb3cc8d38", size = 221302, upload-time = "2026-04-09T16:04:37.62Z" }, - { url = "https://files.pythonhosted.org/packages/50/2b/c7cc2be5cf4ff7b017d948a789256288cb33a517687ff1995e72a7eea79f/librt-0.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:63c12efcd160e1d14da11af0c46c0217473e1e0d2ae1acbccc83f561ea4c2a7b", size = 243893, upload-time = "2026-04-09T16:04:38.909Z" }, - { url = "https://files.pythonhosted.org/packages/62/d3/da553d37417a337d12660450535d5fd51373caffbedf6962173c87867246/librt-0.9.0-cp310-cp310-win32.whl", hash = "sha256:e9002e98dcb1c0a66723592520decd86238ddcef168b37ff6cfb559200b4b774", size = 55375, upload-time = "2026-04-09T16:04:40.148Z" }, - { url = "https://files.pythonhosted.org/packages/9b/5a/46fa357bab8311b6442a83471591f2f9e5b15ecc1d2121a43725e0c529b8/librt-0.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:9fcb461fbf70654a52a7cc670e606f04449e2374c199b1825f754e16dacfedd8", size = 62581, upload-time = "2026-04-09T16:04:41.452Z" }, - { url = "https://files.pythonhosted.org/packages/e2/1e/2ec7afcebcf3efea593d13aee18bbcfdd3a243043d848ebf385055e9f636/librt-0.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90904fac73c478f4b83f4ed96c99c8208b75e6f9a8a1910548f69a00f1eaa671", size = 67155, upload-time = "2026-04-09T16:04:42.933Z" }, - { url = "https://files.pythonhosted.org/packages/18/77/72b85afd4435268338ad4ec6231b3da8c77363f212a0227c1ff3b45e4d35/librt-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:789fff71757facc0738e8d89e3b84e4f0251c1c975e85e81b152cdaca927cc2d", size = 69916, upload-time = "2026-04-09T16:04:44.042Z" }, - { url = "https://files.pythonhosted.org/packages/27/fb/948ea0204fbe2e78add6d46b48330e58d39897e425560674aee302dca81c/librt-0.9.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1bf465d1e5b0a27713862441f6467b5ab76385f4ecf8f1f3a44f8aa3c695b4b6", size = 199635, upload-time = "2026-04-09T16:04:45.5Z" }, - { url = "https://files.pythonhosted.org/packages/ac/cd/894a29e251b296a27957856804cfd21e93c194aa131de8bb8032021be07e/librt-0.9.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f819e0c6413e259a17a7c0d49f97f405abadd3c2a316a3b46c6440b7dbbedbb1", size = 211051, upload-time = "2026-04-09T16:04:47.016Z" }, - { url = "https://files.pythonhosted.org/packages/18/8f/dcaed0bc084a35f3721ff2d081158db569d2c57ea07d35623ddaca5cfc8e/librt-0.9.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0785c2fb4a81e1aece366aa3e2e039f4a4d7d21aaaded5227d7f3c703427882", size = 224031, upload-time = "2026-04-09T16:04:48.207Z" }, - { url = "https://files.pythonhosted.org/packages/03/44/88f6c1ed1132cd418601cc041fbd92fed28b3a09f39de81978e0822d13ff/librt-0.9.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80b25c7b570a86c03b5da69e665809deb39265476e8e21d96a9328f9762f9990", size = 218069, upload-time = "2026-04-09T16:04:50.025Z" }, - { url = "https://files.pythonhosted.org/packages/a3/90/7d02e981c2db12188d82b4410ff3e35bfdb844b26aecd02233626f46af2b/librt-0.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d4d16b608a1c43d7e33142099a75cd93af482dadce0bf82421e91cad077157f4", size = 224857, upload-time = "2026-04-09T16:04:51.684Z" }, - { url = "https://files.pythonhosted.org/packages/ef/c3/c77e706b7215ca32e928d47535cf13dbc3d25f096f84ddf8fbc06693e229/librt-0.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:194fc1a32e1e21fe809d38b5faea66cc65eaa00217c8901fbdb99866938adbdb", size = 219865, upload-time = "2026-04-09T16:04:52.949Z" }, - { url = "https://files.pythonhosted.org/packages/52/d1/32b0c1a0eb8461c70c11656c46a29f760b7c7edf3c36d6f102470c17170f/librt-0.9.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8c6bc1384d9738781cfd41d09ad7f6e8af13cfea2c75ece6bd6d2566cdea2076", size = 218451, upload-time = "2026-04-09T16:04:54.174Z" }, - { url = "https://files.pythonhosted.org/packages/74/d1/adfd0f9c44761b1d49b1bec66173389834c33ee2bd3c7fd2e2367f1942d4/librt-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15cb151e52a044f06e54ac7f7b47adbfc89b5c8e2b63e1175a9d587c43e8942a", size = 241300, upload-time = "2026-04-09T16:04:55.452Z" }, - { url = "https://files.pythonhosted.org/packages/09/b0/9074b64407712f0003c27f5b1d7655d1438979155f049720e8a1abd9b1a1/librt-0.9.0-cp311-cp311-win32.whl", hash = "sha256:f100bfe2acf8a3689af9d0cc660d89f17286c9c795f9f18f7b62dd1a6b247ae6", size = 55668, upload-time = "2026-04-09T16:04:56.689Z" }, - { url = "https://files.pythonhosted.org/packages/24/19/40b77b77ce80b9389fb03971431b09b6b913911c38d412059e0b3e2a9ef2/librt-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:0b73e4266307e51c95e09c0750b7ec383c561d2e97d58e473f6f6a209952fbb8", size = 62976, upload-time = "2026-04-09T16:04:57.733Z" }, - { url = "https://files.pythonhosted.org/packages/70/9d/9fa7a64041e29035cb8c575af5f0e3840be1b97b4c4d9061e0713f171849/librt-0.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:bc5518873822d2faa8ebdd2c1a4d7c8ef47b01a058495ab7924cb65bdbf5fc9a", size = 53502, upload-time = "2026-04-09T16:04:58.806Z" }, - { url = "https://files.pythonhosted.org/packages/bf/90/89ddba8e1c20b0922783cd93ed8e64f34dc05ab59c38a9c7e313632e20ff/librt-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4", size = 68332, upload-time = "2026-04-09T16:05:00.09Z" }, - { url = "https://files.pythonhosted.org/packages/a8/40/7aa4da1fb08bdeeb540cb07bfc8207cb32c5c41642f2594dbd0098a0662d/librt-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d", size = 70581, upload-time = "2026-04-09T16:05:01.213Z" }, - { url = "https://files.pythonhosted.org/packages/48/ac/73a2187e1031041e93b7e3a25aae37aa6f13b838c550f7e0f06f66766212/librt-0.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f", size = 203984, upload-time = "2026-04-09T16:05:02.542Z" }, - { url = "https://files.pythonhosted.org/packages/5e/3d/23460d571e9cbddb405b017681df04c142fb1b04cbfce77c54b08e28b108/librt-0.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27", size = 215762, upload-time = "2026-04-09T16:05:04.127Z" }, - { url = "https://files.pythonhosted.org/packages/de/1e/42dc7f8ab63e65b20640d058e63e97fd3e482c1edbda3570d813b4d0b927/librt-0.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2", size = 230288, upload-time = "2026-04-09T16:05:05.883Z" }, - { url = "https://files.pythonhosted.org/packages/dc/08/ca812b6d8259ad9ece703397f8ad5c03af5b5fedfce64279693d3ce4087c/librt-0.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b", size = 224103, upload-time = "2026-04-09T16:05:07.148Z" }, - { url = "https://files.pythonhosted.org/packages/b6/3f/620490fb2fa66ffd44e7f900254bc110ebec8dac6c1b7514d64662570e6f/librt-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265", size = 232122, upload-time = "2026-04-09T16:05:08.386Z" }, - { url = "https://files.pythonhosted.org/packages/e9/83/12864700a1b6a8be458cf5d05db209b0d8e94ae281e7ec261dbe616597b4/librt-0.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084", size = 225045, upload-time = "2026-04-09T16:05:09.707Z" }, - { url = "https://files.pythonhosted.org/packages/fd/1b/845d339c29dc7dbc87a2e992a1ba8d28d25d0e0372f9a0a2ecebde298186/librt-0.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8", size = 227372, upload-time = "2026-04-09T16:05:10.942Z" }, - { url = "https://files.pythonhosted.org/packages/8d/fe/277985610269d926a64c606f761d58d3db67b956dbbf40024921e95e7fcb/librt-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f", size = 248224, upload-time = "2026-04-09T16:05:12.254Z" }, - { url = "https://files.pythonhosted.org/packages/92/1b/ee486d244b8de6b8b5dbaefabe6bfdd4a72e08f6353edf7d16d27114da8d/librt-0.9.0-cp312-cp312-win32.whl", hash = "sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f", size = 55986, upload-time = "2026-04-09T16:05:13.529Z" }, - { url = "https://files.pythonhosted.org/packages/89/7a/ba1737012308c17dc6d5516143b5dce9a2c7ba3474afd54e11f44a4d1ef3/librt-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745", size = 63260, upload-time = "2026-04-09T16:05:14.68Z" }, - { url = "https://files.pythonhosted.org/packages/36/e4/01752c113da15127f18f7bf11142f5640038f062407a611c059d0036c6aa/librt-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9", size = 53694, upload-time = "2026-04-09T16:05:16.095Z" }, - { url = "https://files.pythonhosted.org/packages/5f/d7/1b3e26fffde1452d82f5666164858a81c26ebe808e7ae8c9c88628981540/librt-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e", size = 68367, upload-time = "2026-04-09T16:05:17.243Z" }, - { url = "https://files.pythonhosted.org/packages/a5/5b/c61b043ad2e091fbe1f2d35d14795e545d0b56b03edaa390fa1dcee3d160/librt-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22", size = 70595, upload-time = "2026-04-09T16:05:18.471Z" }, - { url = "https://files.pythonhosted.org/packages/a3/22/2448471196d8a73370aa2f23445455dc42712c21404081fcd7a03b9e0749/librt-0.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a", size = 204354, upload-time = "2026-04-09T16:05:19.593Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5e/39fc4b153c78cfd2c8a2dcb32700f2d41d2312aa1050513183be4540930d/librt-0.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5", size = 216238, upload-time = "2026-04-09T16:05:20.868Z" }, - { url = "https://files.pythonhosted.org/packages/d7/42/bc2d02d0fa7badfa63aa8d6dcd8793a9f7ef5a94396801684a51ed8d8287/librt-0.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11", size = 230589, upload-time = "2026-04-09T16:05:22.305Z" }, - { url = "https://files.pythonhosted.org/packages/c8/7b/e2d95cc513866373692aa5edf98080d5602dd07cabfb9e5d2f70df2f25f7/librt-0.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858", size = 224610, upload-time = "2026-04-09T16:05:23.647Z" }, - { url = "https://files.pythonhosted.org/packages/31/d5/6cec4607e998eaba57564d06a1295c21b0a0c8de76e4e74d699e627bd98c/librt-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e", size = 232558, upload-time = "2026-04-09T16:05:25.025Z" }, - { url = "https://files.pythonhosted.org/packages/95/8c/27f1d8d3aaf079d3eb26439bf0b32f1482340c3552e324f7db9dca858671/librt-0.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0", size = 225521, upload-time = "2026-04-09T16:05:26.311Z" }, - { url = "https://files.pythonhosted.org/packages/6b/d8/1e0d43b1c329b416017619469b3c3801a25a6a4ef4a1c68332aeaa6f72ca/librt-0.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2", size = 227789, upload-time = "2026-04-09T16:05:27.624Z" }, - { url = "https://files.pythonhosted.org/packages/2c/b4/d3d842e88610fcd4c8eec7067b0c23ef2d7d3bff31496eded6a83b0f99be/librt-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d", size = 248616, upload-time = "2026-04-09T16:05:29.181Z" }, - { url = "https://files.pythonhosted.org/packages/ec/28/527df8ad0d1eb6c8bdfa82fc190f1f7c4cca5a1b6d7b36aeabf95b52d74d/librt-0.9.0-cp313-cp313-win32.whl", hash = "sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd", size = 56039, upload-time = "2026-04-09T16:05:30.709Z" }, - { url = "https://files.pythonhosted.org/packages/f3/a7/413652ad0d92273ee5e30c000fc494b361171177c83e57c060ecd3c21538/librt-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519", size = 63264, upload-time = "2026-04-09T16:05:31.881Z" }, - { url = "https://files.pythonhosted.org/packages/a4/0a/92c244309b774e290ddb15e93363846ae7aa753d9586b8aad511c5e6145b/librt-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5", size = 53728, upload-time = "2026-04-09T16:05:33.31Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c1/184e539543f06ea2912f4b92a5ffaede4f9b392689e3f00acbf8134bee92/librt-0.9.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb", size = 67830, upload-time = "2026-04-09T16:05:34.517Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ad/23399bdcb7afca819acacdef31b37ee59de261bd66b503a7995c03c4b0dc/librt-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499", size = 70280, upload-time = "2026-04-09T16:05:35.649Z" }, - { url = "https://files.pythonhosted.org/packages/9f/0b/4542dc5a2b8772dbf92cafb9194701230157e73c14b017b6961a23598b03/librt-0.9.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f", size = 201925, upload-time = "2026-04-09T16:05:36.739Z" }, - { url = "https://files.pythonhosted.org/packages/31/d4/8ee7358b08fd0cfce051ef96695380f09b3c2c11b77c9bfbc367c921cce5/librt-0.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1", size = 212381, upload-time = "2026-04-09T16:05:38.043Z" }, - { url = "https://files.pythonhosted.org/packages/f2/94/a2025fe442abedf8b038038dab3dba942009ad42b38ea064a1a9e6094241/librt-0.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f", size = 227065, upload-time = "2026-04-09T16:05:39.394Z" }, - { url = "https://files.pythonhosted.org/packages/7c/e9/b9fcf6afa909f957cfbbf918802f9dada1bd5d3c1da43d722fd6a310dc3f/librt-0.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a", size = 221333, upload-time = "2026-04-09T16:05:40.999Z" }, - { url = "https://files.pythonhosted.org/packages/ac/7c/ba54cd6aa6a3c8cd12757a6870e0c79a64b1e6327f5248dcff98423f4d43/librt-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f", size = 229051, upload-time = "2026-04-09T16:05:42.605Z" }, - { url = "https://files.pythonhosted.org/packages/4b/4b/8cfdbad314c8677a0148bf0b70591d6d18587f9884d930276098a235461b/librt-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845", size = 222492, upload-time = "2026-04-09T16:05:43.842Z" }, - { url = "https://files.pythonhosted.org/packages/1f/d1/2eda69563a1a88706808decdce035e4b32755dbfbb0d05e1a65db9547ed1/librt-0.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b", size = 223849, upload-time = "2026-04-09T16:05:45.054Z" }, - { url = "https://files.pythonhosted.org/packages/04/44/b2ed37df6be5b3d42cfe36318e0598e80843d5c6308dd63d0bf4e0ce5028/librt-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b", size = 245001, upload-time = "2026-04-09T16:05:46.34Z" }, - { url = "https://files.pythonhosted.org/packages/47/e7/617e412426df89169dd2a9ed0cc8752d5763336252c65dbf945199915119/librt-0.9.0-cp314-cp314-win32.whl", hash = "sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9", size = 51799, upload-time = "2026-04-09T16:05:47.738Z" }, - { url = "https://files.pythonhosted.org/packages/24/ed/c22ca4db0ca3cbc285e4d9206108746beda561a9792289c3c31281d7e9df/librt-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e", size = 59165, upload-time = "2026-04-09T16:05:49.198Z" }, - { url = "https://files.pythonhosted.org/packages/24/56/875398fafa4cbc8f15b89366fc3287304ddd3314d861f182a4b87595ace0/librt-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f", size = 49292, upload-time = "2026-04-09T16:05:50.362Z" }, - { url = "https://files.pythonhosted.org/packages/4c/61/bc448ecbf9b2d69c5cff88fe41496b19ab2a1cbda0065e47d4d0d51c0867/librt-0.9.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4", size = 70175, upload-time = "2026-04-09T16:05:51.564Z" }, - { url = "https://files.pythonhosted.org/packages/60/f2/c47bb71069a73e2f04e70acbd196c1e5cc411578ac99039a224b98920fd4/librt-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228", size = 72951, upload-time = "2026-04-09T16:05:52.699Z" }, - { url = "https://files.pythonhosted.org/packages/29/19/0549df59060631732df758e8886d92088da5fdbedb35b80e4643664e8412/librt-0.9.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54", size = 225864, upload-time = "2026-04-09T16:05:53.895Z" }, - { url = "https://files.pythonhosted.org/packages/9d/f8/3b144396d302ac08e50f89e64452c38db84bc7b23f6c60479c5d3abd303c/librt-0.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71", size = 241155, upload-time = "2026-04-09T16:05:55.191Z" }, - { url = "https://files.pythonhosted.org/packages/7a/ce/ee67ec14581de4043e61d05786d2aed6c9b5338816b7859bcf07455c6a9f/librt-0.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938", size = 252235, upload-time = "2026-04-09T16:05:56.549Z" }, - { url = "https://files.pythonhosted.org/packages/8a/fa/0ead15daa2b293a54101550b08d4bafe387b7d4a9fc6d2b985602bae69b6/librt-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3", size = 244963, upload-time = "2026-04-09T16:05:57.858Z" }, - { url = "https://files.pythonhosted.org/packages/29/68/9fbf9a9aa704ba87689e40017e720aced8d9a4d2b46b82451d8142f91ec9/librt-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283", size = 257364, upload-time = "2026-04-09T16:05:59.686Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8d/9d60869f1b6716c762e45f66ed945b1e5dd649f7377684c3b176ae424648/librt-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee", size = 247661, upload-time = "2026-04-09T16:06:00.938Z" }, - { url = "https://files.pythonhosted.org/packages/70/ff/a5c365093962310bfdb4f6af256f191085078ffb529b3f0cbebb5b33ebe2/librt-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c", size = 248238, upload-time = "2026-04-09T16:06:02.537Z" }, - { url = "https://files.pythonhosted.org/packages/a0/3c/2d34365177f412c9e19c0a29f969d70f5343f27634b76b765a54d8b27705/librt-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15", size = 269457, upload-time = "2026-04-09T16:06:03.833Z" }, - { url = "https://files.pythonhosted.org/packages/bc/cd/de45b239ea3bdf626f982a00c14bfcf2e12d261c510ba7db62c5969a27cd/librt-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40", size = 52453, upload-time = "2026-04-09T16:06:05.229Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f9/bfb32ae428aa75c0c533915622176f0a17d6da7b72b5a3c6363685914f70/librt-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118", size = 60044, upload-time = "2026-04-09T16:06:06.398Z" }, - { url = "https://files.pythonhosted.org/packages/aa/47/7d70414bcdbb3bc1f458a8d10558f00bbfdb24e5a11740fc8197e12c3255/librt-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61", size = 50009, upload-time = "2026-04-09T16:06:07.995Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0e/b04fd2d76b6a78a02286938251b2c119fb36ddece92c76cb4886ef7d44b8/librt-0.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fe4372c52d4849096c6cc1cda2817d293ec51440c890474ed59ef38d46556f18", size = 148058, upload-time = "2026-09-29T00:44:40.903Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fc/e1ed24aadb32d38603c0c07f579d739eb4d90e64e90d525b024e4a999b4d/librt-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c72c5295a84bd249526da9bdca38f2e176d15c31c13bb0063c5053f4ca023421", size = 153073, upload-time = "2026-09-29T00:44:42.165Z" }, + { url = "https://files.pythonhosted.org/packages/be/38/133ab655409420e0c34a9a9836a7f4e5ae75fba758d7fe177099130d58ba/librt-0.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be56ba9c884143495b517f23fe794ae367d58cd89ea0fdd6d437e3c024a87f9f", size = 493052, upload-time = "2026-09-29T00:44:44.043Z" }, + { url = "https://files.pythonhosted.org/packages/94/42/aaa663bc9421f5bf7bbbe9d3316d7219749698ec7688ac3b519770a89c04/librt-0.16.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:ef46c1a29ffb8c72e882e22618ec618778eacd0578fb22c6e7cf9c11d15f357b", size = 485506, upload-time = "2026-09-29T00:44:45.479Z" }, + { url = "https://files.pythonhosted.org/packages/7b/59/83a6eb5087ba8b45b083859e2085256963e06d4c2531944cdd962670bfb5/librt-0.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d3c94211ee0c4f8d649ec06b7c115c0ec4eadb873a0e3154ca15cef3f814b071", size = 498708, upload-time = "2026-09-29T00:44:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/15a8d62b2bc12935080ec7277e2e0b9b233047781eb784a51d3e25af669a/librt-0.16.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94aed6a8308818b91677957d1bd03188869cd7aeb23c5dba7912a6c0402f7602", size = 515981, upload-time = "2026-09-29T00:44:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/4f/287cc282ac5dd815204c0779e79401bb0a568c3a1335fb319bf0838fb566/librt-0.16.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:349c0bcb87ebd07481b6ff781e25cdc699723dbe2212e57dabb27f7a13b7b87d", size = 508670, upload-time = "2026-09-29T00:44:49.726Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5b/ab7292add898ade0d2b5b75d32a923aae315861671aa5c39f5491309f7ae/librt-0.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:001bfd59a7d45b17e3e75f2a8c6405280b35e7b84471792778e718c4f368950e", size = 530393, upload-time = "2026-09-29T00:44:51.356Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9e/8e6bcf027d4bcbc16a4e864b3d158a1d3e5f55ffa53af0238020cad89271/librt-0.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:28e038895b998d7a0c7798922ce8a1dc157675df5cf1c9ef0aca809ed804b7a1", size = 534302, upload-time = "2026-09-29T00:44:52.893Z" }, + { url = "https://files.pythonhosted.org/packages/96/a4/8c5ab4f3b3a8e905ff8305d76976a2e666f52fe049edd398aed2402beb36/librt-0.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0dbe4096a7ecc00fa835d24510ad8545a4efef738dac96e0e63516783ccde905", size = 536150, upload-time = "2026-09-29T00:44:54.421Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5e/7c4bc267e3cd4b8bd288de9f62d61fff203829c9cc649ea2aa12e9218cf0/librt-0.16.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5cd5b092441053364af968ea12084692cb9d4a22f3ce9524e377880bf028761e", size = 514316, upload-time = "2026-09-29T00:44:55.998Z" }, + { url = "https://files.pythonhosted.org/packages/e0/75/c8bc6b50f43c73819334233f5a3259c81216742cef56d2140c3dd377c3f0/librt-0.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ddeb3c9dedb461bb457c6c7d9aa7fbf35329da313d1a7543d00c8d0f3473c96", size = 557731, upload-time = "2026-09-29T00:44:57.581Z" }, + { url = "https://files.pythonhosted.org/packages/93/fb/e241f91bc8de6c7ac1c6a20ea6dd381bc52312b44d711ffcd97140db1e57/librt-0.16.0-cp311-cp311-win32.whl", hash = "sha256:e05108e0849966f53a8d2d3112a7af881d0efaa479bc735bba91108f9f2350a7", size = 104922, upload-time = "2026-09-29T00:44:59.078Z" }, + { url = "https://files.pythonhosted.org/packages/d1/38/c892383d2bb1a6a80b2ec8ede84970a265a792c56614a06282bf3008517c/librt-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:5f49cff01bd608ef7d97104cb035c75455e79c2d70bf4a506cf773338ac1860d", size = 125093, upload-time = "2026-09-29T00:49:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/8bbde1ed0b21480163617648f638fdaec9374cf5e9c846ebcee34ec02a42/librt-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:1d28ae980ae2218f9c5b95d191e947296f918c9bf0b400d467a9430275bbe678", size = 115910, upload-time = "2026-09-29T00:52:40.131Z" }, + { url = "https://files.pythonhosted.org/packages/ad/76/bbdaeb87b7c47b5c7343e90222b9bfa4e4a8a83f647e02933ad0129225b1/librt-0.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe52bf4641069e7978a14253b036cb9002def1926317e710f2e249f8a8c47742", size = 151028, upload-time = "2026-09-29T00:52:41.508Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0c/ab8ed3dab0085931aec4a792c7eaac8dc6c5ff4691fda3a5360d9d8a9cd2/librt-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5bcc2c4726ced915b00de0c9856a4eeabfb3fddb93e10e0b8f735b7709358b6d", size = 155283, upload-time = "2026-09-29T00:52:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/eb/36/494e79d460c80c1f030661e8287c9eca5e1ad652dc2b2180b6cd42abce0a/librt-0.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff7baa55f8e7c69851419e50a666015d02a74198716fd45c0125a2112e0a389f", size = 503185, upload-time = "2026-09-29T00:52:44.638Z" }, + { url = "https://files.pythonhosted.org/packages/9b/34/a8464038dd9db6e4381fa2b6eb73dc9a50888d77102c4c139304ac35cddc/librt-0.16.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b95d5d92ab83d39e760a52091bb1baba664f3a2351e39b1e16801e5747c2f0e9", size = 496738, upload-time = "2026-09-29T00:52:46.441Z" }, + { url = "https://files.pythonhosted.org/packages/6a/53/e0e5e334ef0c6ed27039d323819368b9ef6712be87d55ee2bf9799398afd/librt-0.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6d085d70bce51d43c5c7c36d63490770180d8779e71c49305c87b4213918de7", size = 513818, upload-time = "2026-09-29T00:52:48.068Z" }, + { url = "https://files.pythonhosted.org/packages/ad/f7/7ce72cbf19d0addd05090b339152fd0548a02562c2866a603e6e3b3da2df/librt-0.16.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36e53948e99bbe3ffea257124cfcae1cfb01831555c9a9c903c9f9a72db7fd07", size = 531952, upload-time = "2026-09-29T00:52:49.816Z" }, + { url = "https://files.pythonhosted.org/packages/83/22/0b1bcb6a8e723c8b4fd60dfc8ae8ec6461c54073fbc8685efeb8d900d407/librt-0.16.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:54d11f726aae9df5a6ffbbf0a03a52449bbac84a53ef03669cb41cdfd4ae41bf", size = 524465, upload-time = "2026-09-29T00:52:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/64/2e/e9c23b8b9df1813da1be205deca9606beb7ddd033972246cd426d05374a0/librt-0.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4323193ac0cd025f85af531df8ba91bf24d1973b401697347a6282e8fd3fcf5e", size = 543191, upload-time = "2026-09-29T00:52:53.284Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e5/6a8b21b342c03ed7e230fa3afbfd2edc58e6e87ef1f0d11fa2b9a748c252/librt-0.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e42f8e098b9c5396fefa05fb1cc7e33b0e08fc51da106b5de4a45fd22aac6743", size = 546979, upload-time = "2026-09-29T00:52:54.932Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/b5129023eced1be01e01c22757f53be551d463b1bb7264f787927404c1d6/librt-0.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:39ec1d5a14e37baf1450a6cabf03fe552340808bf1ad9d71824ab90117716459", size = 555317, upload-time = "2026-09-29T00:52:56.869Z" }, + { url = "https://files.pythonhosted.org/packages/71/89/28bba5938c725fe91f06bf93f7fa6c6b150229df53a87b454b0d5c2a796e/librt-0.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d1aabe3925cbb4a08d15b7b20ba4011b53019da0c4173a25155139b7b1baed65", size = 535846, upload-time = "2026-09-29T00:52:58.475Z" }, + { url = "https://files.pythonhosted.org/packages/22/92/63773026614f888c5d4e370e395ce42ca604b89f70b3acdfedbf94851b80/librt-0.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:300c3ffdc459f4a779a8411ecb188e3ac0b1ff3a3a7b099642555dedae06c69b", size = 573700, upload-time = "2026-09-29T00:53:00.127Z" }, + { url = "https://files.pythonhosted.org/packages/66/8f/347d4677eefb57cd9f8e01d95a1dcee9a91b4e44e664173c32ff6f3752e5/librt-0.16.0-cp312-cp312-win32.whl", hash = "sha256:c17194318e4c0c0348b36f36c2ec7534436fe0a4c15582403162a4f08c80797a", size = 106158, upload-time = "2026-09-29T00:53:02.031Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2c/5193dc81127cd5ddfad031391b046bf32dda219b38463ab872407ca30646/librt-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:25a58a19ea8d83b68209f04912df765e9260635ef77646542ed4b4abe6bc7940", size = 126176, upload-time = "2026-09-29T00:53:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/ff/3d/9668a400c8dd81d162eba38b33fa49fa6205f1a64493570a13fbb815c3ee/librt-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7be7cf555bc30ec12622e9447299cc4a9b8ff307548b634794353db0c2065dc", size = 116572, upload-time = "2026-09-29T00:53:04.815Z" }, + { url = "https://files.pythonhosted.org/packages/46/cd/ae5e0e9dba45d1399aa04a5395bcc0bead40d9fa06dc903634a7b4d7473d/librt-0.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c5e6144e68b577f157519f2ba88ca20e3ed61c29b00e5cdfa76cd2d45acf059a", size = 151036, upload-time = "2026-09-29T00:53:06.284Z" }, + { url = "https://files.pythonhosted.org/packages/41/5a/48a16e323c5f9447a94cce7b59babf60fa04e62c3365ecf060c77ed8b320/librt-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:33f41443a1f4e1f099331b3d8120e409fbff84b9760bc1cc9ea496f37ddaa5cc", size = 155211, upload-time = "2026-09-29T00:53:07.72Z" }, + { url = "https://files.pythonhosted.org/packages/3f/29/0f59299eb4251a409b2e690ad4b7d9f8a676db829d7817ec961f32b44f7e/librt-0.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e510b7770bee609617a3374a96548eb114cae048023e3f049ee449e7ff2db32", size = 502587, upload-time = "2026-09-29T00:53:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/de/ba/d6fb4ef8d1537c396079d72289f16be7cd35a366e5065c51253fea2760b6/librt-0.16.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:efc49c462d4516b8a58b00b490078fa64689fd1fe66970cc190131d7afb8027e", size = 496146, upload-time = "2026-09-29T00:53:11.016Z" }, + { url = "https://files.pythonhosted.org/packages/52/fc/8c50dd4d7cc97c0ee8f252c8a3104980f234391cf1519b554e8b9de08b60/librt-0.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:92caf82ebef5e12d21c72242b70d1e92536f1711cf2a727a4c276de4b4469087", size = 513336, upload-time = "2026-09-29T00:53:12.655Z" }, + { url = "https://files.pythonhosted.org/packages/a9/59/16c409c56f708eda2db9a0553662845d45e3871c77d70a240dae3f3bdc56/librt-0.16.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17bac7f7a16b328fff77e440287693eb017abde913595b5827ebccbc21ecd8a6", size = 531689, upload-time = "2026-09-29T00:53:14.39Z" }, + { url = "https://files.pythonhosted.org/packages/88/82/d34772a6c29d1446dcca6e64d74062efd508523ab351aa16625a4689d5cc/librt-0.16.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5b976054553670829985ed767feb78fb6bcede0175327c4844dd5c281c1be659", size = 524537, upload-time = "2026-09-29T00:53:16.064Z" }, + { url = "https://files.pythonhosted.org/packages/77/8f/24c5631313746131ccee53bc91fdc8374f9cf25e0082a1fee9c93bb98acc/librt-0.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0058f9d68721094105917254c72ac0569117bb7b13b9769cf45d26d89f9d21cd", size = 543231, upload-time = "2026-09-29T00:53:17.939Z" }, + { url = "https://files.pythonhosted.org/packages/f2/cb/5f8e0d41dbd8b499c2265e939c31acc9ba59845565bf99539ad1c06aebcf/librt-0.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b7beaf3f4487b7d8adef1f158b49067cb4d5a19fa7a3bf31a4e7a820e435c5", size = 546499, upload-time = "2026-09-29T00:53:19.666Z" }, + { url = "https://files.pythonhosted.org/packages/be/61/063052de441d1385f59cea4223f184bf9e5d125de1ae3239b490aa1e486e/librt-0.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:468df902df016a06eb0e40b0747dc8d14e47d7a38b18b63b1fb167d85cb94d63", size = 555261, upload-time = "2026-09-29T00:53:21.292Z" }, + { url = "https://files.pythonhosted.org/packages/14/11/a2ada0529372268d6401afa9d457a095b68cd7753532b6f7f33049a19b43/librt-0.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:aea7b1f2b125dad5de85f049136651bff256c883c65e6b9209b2da0a1ac3cdef", size = 536029, upload-time = "2026-09-29T00:53:23.07Z" }, + { url = "https://files.pythonhosted.org/packages/23/9d/5bb6d38853382986dca702fc7e06c8256d30f5fa0676d882773b744610dc/librt-0.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a8afb6557920860b7a3a596eb804cf37e09e7cf8a803db2478c202acc72d8c2e", size = 573748, upload-time = "2026-09-29T00:53:24.696Z" }, + { url = "https://files.pythonhosted.org/packages/99/f6/0025cde35ff7f607684dc775a2b2d732cce2561cec050a6ee1fb2e1fc6fe/librt-0.16.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:77c7a2b4fe2c1369e0d5aa1cade26740a7b14be32fbc9a5535d617d20065c39d", size = 79861, upload-time = "2026-09-29T00:53:26.089Z" }, + { url = "https://files.pythonhosted.org/packages/93/93/303b8592909bd583f83f02818ffbea3f7647ca1e22b1cd5465d04b8145fe/librt-0.16.0-cp313-cp313-win32.whl", hash = "sha256:02d89c813d5ff74b17df72d3a34819d132cd168e56b81bf755b809bd9e46b8c4", size = 106236, upload-time = "2026-09-29T00:53:27.43Z" }, + { url = "https://files.pythonhosted.org/packages/cf/24/80bbb463c60ed18e29cb26aba386ddb76609580e4e7160710e550587018c/librt-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:14ed6ebe3e4f85f326d7920011ad30ff49ed9334e62cf88caef9ba973d9e3a92", size = 126180, upload-time = "2026-09-29T00:53:28.7Z" }, + { url = "https://files.pythonhosted.org/packages/43/80/b1a6fbdd7da825cdd55c71aa81eb6cfa82c513c360774152eacb50b4a771/librt-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:83d4041a3d9b2fd053a8a4e1f22878b3e5833e2712956382d5c048d791454e91", size = 116557, upload-time = "2026-09-29T00:53:30.012Z" }, + { url = "https://files.pythonhosted.org/packages/1e/93/9e0cf7da129a93c3dc7f45bc3cd4a660f2aaa995aa8a6f95c2583ef41239/librt-0.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:931a0bb0fcac88f263e269e46eb30ba8e21402cd3c62ca40cb97034c0693fab1", size = 149844, upload-time = "2026-09-29T00:53:31.391Z" }, + { url = "https://files.pythonhosted.org/packages/8f/26/8a90d2a8f2b2e471bb486b7aec117b8ae622715ed6c39853aec48ea20073/librt-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1bc17e54e5305f8d40b7ca203671ff5a9e59c1d0f8ea0f625dcca53a3984de11", size = 154111, upload-time = "2026-09-29T00:53:32.718Z" }, + { url = "https://files.pythonhosted.org/packages/35/ce/67abb46258da4d3e42ff5b141db6f38c59357bef84c7979f183b22f924f1/librt-0.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:877698bf6bca5721d8be345f2fe09778e40ecadea8b58c73075f2b1a53666bf2", size = 494234, upload-time = "2026-09-29T00:53:34.466Z" }, + { url = "https://files.pythonhosted.org/packages/12/f9/ea7162414a16f8f1bbd3b493ad6d22b926c5471916e078350bdbca8c4e5f/librt-0.16.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5981c011b306781ce561e18e14230a14524a3d8109b97553666c942c18f31a96", size = 491173, upload-time = "2026-09-29T00:53:36.124Z" }, + { url = "https://files.pythonhosted.org/packages/b1/09/9b3e869060dd33f9989b80ba4fea306f6db8cecefcdfe6d7346ef0603f65/librt-0.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:afced3dfc17cd805ecf7a3d77996a71cf5f2c75aa66eb0c21a9930f4fc992f86", size = 505547, upload-time = "2026-09-29T00:53:37.686Z" }, + { url = "https://files.pythonhosted.org/packages/50/07/79007d2165f649ea93e08c0962d1d62c70af9cde77965255095bf9d96f9a/librt-0.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca8052401c55d7511dda6760719fda7618067e83535d7d0010096d216c34b667", size = 523057, upload-time = "2026-09-29T00:53:39.301Z" }, + { url = "https://files.pythonhosted.org/packages/61/0c/8fbaff66d0ba376d8864653f5acce1569bae27e89648671f26f7eca67ab8/librt-0.16.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1e511762a074005bb0aa569166779834e75e438370226930d0ce1866d4b6a33b", size = 515158, upload-time = "2026-09-29T00:53:41.012Z" }, + { url = "https://files.pythonhosted.org/packages/df/2e/23ff0dece76f07a4124413a57682efa0bbeb5765ac0122bc0955513f82ca/librt-0.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1e8591bd8a5a628cd7f07954c6a1592359a878bf032957a8e9057a41d644311", size = 534183, upload-time = "2026-09-29T00:53:42.451Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/e11dea21d9a29486eba78887380374189d472734fa32cc50cb37ca44d3d0/librt-0.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4aaefb4ba6c07e1aeebb2795c8958148f1d6f9af3b555b53d23d766edb6d67a", size = 540663, upload-time = "2026-09-29T00:53:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/75/e873ae158a8b7f5359be33e7fd6c1fbe02d9a78a3e89a77de6b0e837f476/librt-0.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d92db7a0f6aee44f1baee94750457e8d2d1c6ccea41842de6268d34e8dc7eddd", size = 545908, upload-time = "2026-09-29T00:53:45.812Z" }, + { url = "https://files.pythonhosted.org/packages/ba/36/8939d3f6a93e11bd9592e6fe28d2b44f1c2dc4bed6e22e72359a91e18ffb/librt-0.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:378dfaffb38e59c24a87cde5713cd865d51ff7383fa12947f3907f306ea1ca55", size = 523302, upload-time = "2026-09-29T00:53:47.596Z" }, + { url = "https://files.pythonhosted.org/packages/71/14/35309f44a077f0f42ade0e2e7cd88c0cea760c661af205c01ea90d3c0e1f/librt-0.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3e0c39bdc85370422e8b637be76eb1fd07d30967551b03e62267dd156f553152", size = 565911, upload-time = "2026-09-29T00:53:49.278Z" }, + { url = "https://files.pythonhosted.org/packages/78/0c/df6255b94967f3159ebc46f08d8e783c12da6ee269ddb74b2efed63c640c/librt-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:1b384b90ab79a7bc30b566895809a636e0666f21f3cf12b54823d025b7e83839", size = 79119, upload-time = "2026-09-29T00:53:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/6b/44/d30d5a5461378c9d33f36736c6791c3b4c4ba4b1ffe0da7350aedcb2c9a0/librt-0.16.0-cp314-cp314-win32.whl", hash = "sha256:52327da75a94012e7f932f913d20d3876bed3c102be00e6c3e8600ff7bdd58a7", size = 100140, upload-time = "2026-09-29T00:53:52.205Z" }, + { url = "https://files.pythonhosted.org/packages/c2/98/769712f356a1e897df3581bb0c3100375d054a000de26099360ea65b5111/librt-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:3f0b8114c44b2ac06ff5dacd08e07e8e807ff4f46083f2a1602685122559be41", size = 120464, upload-time = "2026-09-29T00:53:53.495Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d3/ae2abccc8bdc8b063c1613a77686e17b74e9d3d60cfe6f12c63fa2821b9f/librt-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:8caf96a4ef8fb27d0ac0d1ad8337d26a240acd4a02fe4345d0a8f264753e8f99", size = 110889, upload-time = "2026-09-29T00:53:54.817Z" }, + { url = "https://files.pythonhosted.org/packages/e4/26/0737d4be058dd6376eade7dd8b380d4b869b6394cee929393a5a431c45bb/librt-0.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:953107e2f68d0f3512c48f898b0dbf0ce5cc52bba0f318d847c985dc555ee4cc", size = 159838, upload-time = "2026-09-29T00:53:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/01/96/9bc96531d7c620e9949af904470f02e3fa8f35129ab8e8f281c51eaa3788/librt-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad37d5b9abd49c9a655dcda7ea52a8a752884062ef1ee71ae17c2f2a0f81fe6a", size = 162018, upload-time = "2026-09-29T00:53:57.532Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fa/b0289dcb186eb3f97221ba00da5f4bd3ba7fa5e99752d48aa8d336615334/librt-0.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c4aa329c17bd1aaea4f6e89335d8ccd494b3a5830b6654462273e50e11023f0", size = 703983, upload-time = "2026-09-29T00:53:59.024Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ab/05ebbbde7530fc5eeb58fbd1581522a9f64f132fa703c4c595eed6e14760/librt-0.16.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0ead24d2562a49473dddd9efef8581f020007eb0054389c3ee3ffad38b1ca4c9", size = 683847, upload-time = "2026-09-29T00:54:00.671Z" }, + { url = "https://files.pythonhosted.org/packages/a3/75/f52aeecd4dbadbddf80725ba7de126d8bd5d0eb66247eae17a81ed90dd4d/librt-0.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02118f56a9c36ddd07dfd9b919d9ecc117ba20a90987d56aa4c429fa34509188", size = 698045, upload-time = "2026-09-29T00:54:02.247Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/fa277a835cd6eb42380591ceb85f48c5b4d2b2d7e2cb9869e1e17d24d237/librt-0.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e29522c62e28595ff7e324c6834ade51127707f0e255b18d1c1cf03d39c1048", size = 724640, upload-time = "2026-09-29T00:54:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/25/e4/2cf64354f3fde8ebd591b3b48f96510bee24ac5f7d1e567adbbe210abdbd/librt-0.16.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ff4b2367926b69c6215635902cccb04048e73094e9862900d27cb2c6bbff143", size = 731748, upload-time = "2026-09-29T00:54:05.741Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c9/c180af3e94e01aa529fa93d7733fec2abc47f222345400cf21d5481d5f8a/librt-0.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c6f1b27bf1632a7e016af9f145f82be95e1edd7721a646505c21059257cb5a04", size = 754960, upload-time = "2026-09-29T00:54:07.38Z" }, + { url = "https://files.pythonhosted.org/packages/95/d6/01073aa78c58f356b10d9c57b3fe9abb143df338142bcd316df417b89db0/librt-0.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5696d7f52e7b37217cb3a8f92c744fe835942602fdd4c1a8bc4741d3bfdce15e", size = 747000, upload-time = "2026-09-29T00:54:09.06Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d8/1de3783908658d697a8cfc00582f61299ffba7796d7260c112e4700b1109/librt-0.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6072e92dd876ff6ceeb6cf371e35e51f479349837391341f479b08df4564242b", size = 749569, upload-time = "2026-09-29T00:54:10.702Z" }, + { url = "https://files.pythonhosted.org/packages/b7/32/e817f66c96d6caa8bb8435ff93c4c220624d59efc506be59ab98fcd01d0c/librt-0.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:39ca4f2f2fe05de8e63493da592d84311adabe5bef52b193851981da9816b302", size = 729483, upload-time = "2026-09-29T00:54:12.25Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c9/23992ccd2b9d22798fdd0f61353183a47414e651da782ad83eb680f50833/librt-0.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9807485a908f00355820f18e91e045ffdcdc5adb68aaec40a1e2b88c5f7bba1", size = 776584, upload-time = "2026-09-29T00:54:13.837Z" }, + { url = "https://files.pythonhosted.org/packages/67/3b/e8af957f08e6e2e8e566b099e43d2748418aa565df6ee1d9d3331209fdfe/librt-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:94be5cb7bca4df6201f4183e9e4fa2086c655283d20b38cd84500a69057575a7", size = 104402, upload-time = "2026-09-29T00:54:15.568Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/946e6443d7cd32347a086043395e421e52fd603c9163d2ec970ceab8eed6/librt-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d46ca272b251d033dd4527b0dec5f261a28a52bd5fa0f99c117b0a1f8588cc2d", size = 126227, upload-time = "2026-09-29T00:54:17.165Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a3/bc4f9959d3c62bcbf9e5fd3470a8bbd8bf33224ed4c25d7fba173201b8ac/librt-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b9d6d4b14e92d876f8026b54c20c445f36425214c1081dc76f74e40db386b82b", size = 116493, upload-time = "2026-09-29T00:54:18.567Z" }, + { url = "https://files.pythonhosted.org/packages/b6/4b/10fdb42dfab4c1533e1570e686b18e86ff4328b406361b39fb3016667638/librt-0.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:6fe436af2eaf630474f491af5d032cbe45f93fcff5c3b9fe4ab194a7255b20ff", size = 149829, upload-time = "2026-09-29T00:54:19.933Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/a90ca13f1d3d91af1680000a4038536907018fbd51764767287a05d28b8b/librt-0.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:8ff5d26c529336be9bd7ae04483235d77778ee7d6444a95353102b542601ce81", size = 154522, upload-time = "2026-09-29T00:54:21.306Z" }, + { url = "https://files.pythonhosted.org/packages/5c/dd/bcf364eacfa070bb1fc88d503111ae7177914197ba1fa717c748926e7930/librt-0.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:909d8e3c1faee44cb762b1c519ff8613dcc5ceae5c99987a00917b5a31fd1d6a", size = 497593, upload-time = "2026-09-29T00:54:22.798Z" }, + { url = "https://files.pythonhosted.org/packages/18/c1/2c4e81e347bdabfe8346bfc6dc37ae5e154d61c88e30848ec0606025a5e0/librt-0.16.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:6d4a64283ee61824b5790de882bc68e2d9d7a5143537cb7a966f7354f71646d4", size = 480459, upload-time = "2026-09-29T00:54:24.364Z" }, + { url = "https://files.pythonhosted.org/packages/ab/d7/fef2a3cb8400701be496f6e459876f650b3451f407e30cb243de571ae615/librt-0.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5810ba811297fdf37a1531a57667cb8ace0842013ca8606bf9eb7c24cf4be154", size = 507814, upload-time = "2026-09-29T00:54:25.997Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6f/53762927a32e9dc9eb1d1c1f3528da281290c86d96929a8af652f671266e/librt-0.16.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e56aaf8c167548dc8e5d6f3bd0f48dcdd299a23c73be3f744aab79d99e9c7f5d", size = 525114, upload-time = "2026-09-29T00:54:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/6c/67/0b9d031f303c4e8c691a9a8ef9d272f13b530c11cc563b819df621b6a348/librt-0.16.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f36c58e33b304b525c6c9c5076399c6ebf1109e17b9051a05a407b091b9215b", size = 520465, upload-time = "2026-09-29T00:54:29.331Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/2056a3a85864e882eb17a203a10ddb26fa748bc9718ec67e79059ab46cae/librt-0.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:242e00b3d4fa37c3d3c1ca5f5c9adb7d909ddb1eac9c41f2787320d00caa0af2", size = 537257, upload-time = "2026-09-29T00:54:30.915Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/e0d79790c163cbc0909e209a6f62e30bb713b647f905a176cdc64848fd4d/librt-0.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:0253721561787b8df8443eb347b7a6461015354e5bdd37ee38a41fef220d2bb0", size = 527442, upload-time = "2026-09-29T00:54:32.478Z" }, + { url = "https://files.pythonhosted.org/packages/aa/50/1c0c95aba7af51f4752ea34fbf2eb79b36e7cae3a72536248e8c735de735/librt-0.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3e483a8d69ede8067db70c0e83007423b6925de6fd53afed01d66160f2e9398c", size = 548139, upload-time = "2026-09-29T00:54:34.183Z" }, + { url = "https://files.pythonhosted.org/packages/98/91/a8a43dd5138d4f55f88846b8f0c85454a4fad69952cebfcff9948f831290/librt-0.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:69ba927445cfaaffb4081003ef5224c55a5c2ab67ef956f416ef744916e44121", size = 529708, upload-time = "2026-09-29T00:54:35.761Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/d5226881ab2b7c20d9d587b37bdd4a0ec8775a96d00ca87ac9f385587db4/librt-0.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d6a365f2ab45a984d0e00eee0dd17f599ceab8cadab6ea07b6111c8132fc0e42", size = 567781, upload-time = "2026-09-29T00:54:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/bb/bf/2345ba57a626e8c78c4ddcc724636a8df5a593fe46c1ee76bbf477e32b0d/librt-0.16.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f01f3805f2dae4781c0c34b440e31740d082950bdaf89a6f601ad589a28af57a", size = 79177, upload-time = "2026-09-29T00:54:38.792Z" }, + { url = "https://files.pythonhosted.org/packages/1e/40/99e77936cc9207f629b077bf7cbc5e2f0827cddd7c29792f04ef881bd2c3/librt-0.16.0-cp315-cp315-win32.whl", hash = "sha256:b0e3e721c75d2e79a76d4422c79d7ba705fe1bbafec907037fe7a657a480a0e3", size = 100127, upload-time = "2026-09-29T00:54:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/56/1e/801fe26bc622061b9dfd010e166d94142cb774b733217e6d98d1c0cf2638/librt-0.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:bc02954b1295de798bbdb0b4e2d8a28c2117de8b5c73dcbeb27dc32572dfb971", size = 120480, upload-time = "2026-09-29T00:54:41.722Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a9/d533983055bd36e112627384c2c038845d1df882540b8bcefb566475640b/librt-0.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:c5db585d43449a5f54303d4b2774e45e1babd975cfe1630a3d708c0b80c3e560", size = 110915, upload-time = "2026-09-29T00:54:43.052Z" }, + { url = "https://files.pythonhosted.org/packages/55/fe/d62238fa9c653b0e0613290349467cb65711b5800e8e474f45e942a0ad95/librt-0.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f06c689cb14afd9b612727553a5ec5a40febf113ca41c4413a2b0b334285884b", size = 159714, upload-time = "2026-09-29T00:54:44.497Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ac/f31efe7818700be72ba4f9af8a80fa67c39808c8d26dacc55dc1f6f35172/librt-0.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:13b4e8aba90b0b1c82474e9844aa9ffe7ad3faa484350e1da64cb8188d903134", size = 162086, upload-time = "2026-09-29T00:54:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/d9/16/4d7487bf86a9d7e8e18f37ba5538789233ea693637379b75701bd35ec9de/librt-0.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a269c46ae327d8e6f8c1f85f7516cb52c0fa48127565a1105a4f4a05ff2a0b4", size = 712727, upload-time = "2026-09-29T00:54:47.486Z" }, + { url = "https://files.pythonhosted.org/packages/77/74/50cd550ccc1a517b9ed62347625c01ddf8c4afad66490312677c011458f6/librt-0.16.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a33e0dae1f8592146a4764d54ce842b278732d21a84e17c3bbe6b1bc158a2248", size = 681106, upload-time = "2026-09-29T00:54:49.126Z" }, + { url = "https://files.pythonhosted.org/packages/df/5d/7293f712975ee6fdd2251411fd9ef1c62bc99c7b83ecbe62dbc999b1fab7/librt-0.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47ada6ea32636492c61aa8ad27ae3b9404bfe7a97e3ba946d1984236cc741da0", size = 705921, upload-time = "2026-09-29T00:54:50.905Z" }, + { url = "https://files.pythonhosted.org/packages/67/f7/8aab946f11d59d1bece9ffc65d994b200c789a8ca5a95c17e17e609f912a/librt-0.16.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43bd6e642d8a248c114327f98dd25ac5a7cb5aa168ef02f0559b91874df16b8", size = 731786, upload-time = "2026-09-29T00:54:52.584Z" }, + { url = "https://files.pythonhosted.org/packages/43/76/1c42ab31e7cb8384ebf6d3af607213c495222474f4940443ae7639ab7685/librt-0.16.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3d1bb7841a816ace6449bb26d3f9560dbfa20e71c568d23f0f62bf1e68f50b1", size = 744964, upload-time = "2026-09-29T00:54:54.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2e/4b19982d933d2dfced671e840b219e4c1cd3f507df6e6dabb51bbd4e3850/librt-0.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3931f7a3db322e7f44e02a280e3949326ce9579ad388ee8d691dc7c76da9fb70", size = 765305, upload-time = "2026-09-29T00:54:55.815Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1b/e872583de2dcb3ac7746e7a2321aeeb168274f3952a87dc66e05ddb29faf/librt-0.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f4462528b6000afe8f16907b5c7c2553abf1df005ba5140e6eb394541c3624c3", size = 744959, upload-time = "2026-09-29T00:54:57.532Z" }, + { url = "https://files.pythonhosted.org/packages/10/de/a18c6bcfb297af2674233e90b3c3661c3a0af0f1434a0c966d2ef708a835/librt-0.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:80039ba9b6a7d5f1a0175a4cca6bbefead87bd854c80abad1cb30afe47a830db", size = 758138, upload-time = "2026-09-29T00:54:59.484Z" }, + { url = "https://files.pythonhosted.org/packages/22/1c/0df1d732539c297bb1a093e1fe3204d2faf76a9022cf4e1d1c2fce059790/librt-0.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7a1d272724b581bb6bc769dfdafed6da2ecc9886ba2450311de55a4ac2e1e9cd", size = 744268, upload-time = "2026-09-29T00:55:01.179Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f7/ccaf31331f20c91a5bd9bd48ffc3f743f9c81bb5720cc7b2a720ca204f0a/librt-0.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:fbe4fb8c5445f7496d7f7f6bb0807875d09d47e6771ffa175fb2df2895fb86ba", size = 785783, upload-time = "2026-09-29T00:55:02.938Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ea/7421d9e6db894cd6cd788b604b3394a163ded6b942052b6f96c23ccf08e1/librt-0.16.0-cp315-cp315t-win32.whl", hash = "sha256:375bfe6b572a8f6cfc398709356046173bf27e64c4c5edaf5f7062f051fb4bf9", size = 104355, upload-time = "2026-09-29T00:55:04.533Z" }, + { url = "https://files.pythonhosted.org/packages/85/6d/7c31a506eb847bc58aeb2402d58e17605e821f68ab5df5e66fe274a6df41/librt-0.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:bd3150023d3dc2bc70f3784e59ffa1140d56ddba3d8125b3d6f9f85221279bfc", size = 126134, upload-time = "2026-09-29T00:55:06.017Z" }, + { url = "https://files.pythonhosted.org/packages/36/69/7a5d10ac409c4da0355e054a14371871da9b5557fcc42772cd00181c6cce/librt-0.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:8ceafb70f2a4f0826f11031942e59c0728fd98da112dc346d4352bde1e486866", size = 116391, upload-time = "2026-09-29T00:55:07.484Z" }, ] [[package]] name = "mypy" -version = "1.20.1" +version = "2.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "ast-serialize" }, { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/3d/5b373635b3146264eb7a68d09e5ca11c305bbb058dfffbb47c47daf4f632/mypy-1.20.1.tar.gz", hash = "sha256:6fc3f4ecd52de81648fed1945498bf42fa2993ddfad67c9056df36ae5757f804", size = 3815892, upload-time = "2026-04-13T02:46:51.474Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/3c/78ecece71ee4b5445389a4ccd11627cfb9c69e4de554d11060171f376305/mypy-2.4.1.tar.gz", hash = "sha256:63b590965691e540a335fa74dadf31a7069142f6b72e234689bbd244edf55623", size = 4066728, upload-time = "2026-10-28T19:23:13.286Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/21/4b/b1fa23297c8a5c403aabaac0649549efc5a0af7095f3dd33e7482863f973/mypy-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3ba5d1e712ada9c3b6223dcbc5a31dac334ed62991e5caa17bcf5a4ddc349af0", size = 14426426, upload-time = "2026-04-13T02:46:37.828Z" }, - { url = "https://files.pythonhosted.org/packages/22/53/82923480aee5507a46df22428316e28b2b710d08506a128b2acef81ab18e/mypy-1.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e731284c117b0987fb1e6c5013a56f33e7faa1fce594066ab83876183ce1c66", size = 13307651, upload-time = "2026-04-13T02:46:22.676Z" }, - { url = "https://files.pythonhosted.org/packages/4e/0c/91905b393c790440fa273f0903ee2b07cce95bb6deccac87e6eb343d077a/mypy-1.20.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8e945b872a05f4fbefabe2249c0b07b6b194e5e11a86ebee9edf855de09806c", size = 13746066, upload-time = "2026-04-13T02:45:15.345Z" }, - { url = "https://files.pythonhosted.org/packages/88/b9/8a7017270438e34544e19dd6284cad54fd65dde3c35418a2ce07a1897804/mypy-1.20.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fc88acef0dc9b15246502b418980478c1bfc9702057a0e1e7598d01a7af8937", size = 14617944, upload-time = "2026-04-13T02:45:44.954Z" }, - { url = "https://files.pythonhosted.org/packages/0c/cf/5a61ceec3fc133e0f559d1e1f9adf4150abdbc2ad8eb831ec26fc8459196/mypy-1.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:14911a115c73608f155f648b978c5055d16ff974e6b1b5512d7fedf4fa8b15c6", size = 14918205, upload-time = "2026-04-13T02:45:42.653Z" }, - { url = "https://files.pythonhosted.org/packages/6f/80/afb1c665e9c426c78e4711cce04e446b645867bfb97936158886103c1648/mypy-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:76d9b4c992cca3331d9793ef197ae360ea44953cf35beb2526e95b9e074f2866", size = 10823344, upload-time = "2026-04-13T02:46:07.607Z" }, - { url = "https://files.pythonhosted.org/packages/11/68/7ad64b49b7663c88fef76a2ac689ea73e17804832ac4cb5416bcff17775b/mypy-1.20.1-cp310-cp310-win_arm64.whl", hash = "sha256:b408722f80be44845da555671a5ef3a0c63f51ca5752b0c20e992dc9c0fbd3cd", size = 9760694, upload-time = "2026-04-13T02:46:49.369Z" }, - { url = "https://files.pythonhosted.org/packages/82/0d/555ab7453cc4a4a8643b7f21c842b1a84c36b15392061ae7b052ee119320/mypy-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c01eb9bac2c6a962d00f9d23421cd2913840e65bba365167d057bd0b4171a92e", size = 14336012, upload-time = "2026-04-13T02:45:39.935Z" }, - { url = "https://files.pythonhosted.org/packages/57/26/85a28893f7db8a16ebb41d1e9dfcb4475844d06a88480b6639e32a74d6ef/mypy-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55d12ddbd8a9cac5b276878bd534fa39fff5bf543dc6ae18f25d30c8d7d27fca", size = 13224636, upload-time = "2026-04-13T02:45:49.659Z" }, - { url = "https://files.pythonhosted.org/packages/93/41/bd4cd3c2caeb6c448b669222b8cfcbdee4a03b89431527b56fca9e56b6f3/mypy-1.20.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0aa322c1468b6cdfc927a44ce130f79bb44bcd34eb4a009eb9f96571fd80955", size = 13663471, upload-time = "2026-04-13T02:46:20.276Z" }, - { url = "https://files.pythonhosted.org/packages/3e/56/7ee8c471e10402d64b6517ae10434541baca053cffd81090e4097d5609d4/mypy-1.20.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3f8bc95899cf676b6e2285779a08a998cc3a7b26f1026752df9d2741df3c79e8", size = 14532344, upload-time = "2026-04-13T02:46:44.205Z" }, - { url = "https://files.pythonhosted.org/packages/b5/95/b37d1fa859a433f6156742e12f62b0bb75af658544fb6dada9363918743a/mypy-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:47c2b90191a870a04041e910277494b0d92f0711be9e524d45c074fe60c00b65", size = 14776670, upload-time = "2026-04-13T02:45:52.481Z" }, - { url = "https://files.pythonhosted.org/packages/03/77/b302e4cb0b80d2bdf6bf4fce5864bb4cbfa461f7099cea544eaf2457df78/mypy-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:9857dc8d2ec1a392ffbda518075beb00ac58859979c79f9e6bdcb7277082c2f2", size = 10816524, upload-time = "2026-04-13T02:45:37.711Z" }, - { url = "https://files.pythonhosted.org/packages/7f/21/d969d7a68eb964993ebcc6170d5ecaf0cf65830c58ac3344562e16dc42a9/mypy-1.20.1-cp311-cp311-win_arm64.whl", hash = "sha256:09d8df92bb25b6065ab91b178da843dda67b33eb819321679a6e98a907ce0e10", size = 9750419, upload-time = "2026-04-13T02:45:08.542Z" }, - { url = "https://files.pythonhosted.org/packages/69/1b/75a7c825a02781ca10bc2f2f12fba2af5202f6d6005aad8d2d1f264d8d78/mypy-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:36ee2b9c6599c230fea89bbd79f401f9f9f8e9fcf0c777827789b19b7da90f51", size = 14494077, upload-time = "2026-04-13T02:45:55.085Z" }, - { url = "https://files.pythonhosted.org/packages/b0/54/5e5a569ea5c2b4d48b729fb32aa936eeb4246e4fc3e6f5b3d36a2dfbefb9/mypy-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fba3fb0968a7b48806b0c90f38d39296f10766885a94c83bd21399de1e14eb28", size = 13319495, upload-time = "2026-04-13T02:45:29.674Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a4/a1945b19f33e91721b59deee3abb484f2fa5922adc33bb166daf5325d76d/mypy-1.20.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef1415a637cd3627d6304dfbeddbadd21079dafc2a8a753c477ce4fc0c2af54f", size = 13696948, upload-time = "2026-04-13T02:46:15.006Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c6/75e969781c2359b2f9c15b061f28ec6d67c8b61865ceda176e85c8e7f2de/mypy-1.20.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef3461b1ad5cd446e540016e90b5984657edda39f982f4cc45ca317b628f5a37", size = 14706744, upload-time = "2026-04-13T02:46:00.482Z" }, - { url = "https://files.pythonhosted.org/packages/a8/6e/b221b1de981fc4262fe3e0bf9ec272d292dfe42394a689c2d49765c144c4/mypy-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:542dd63c9e1339b6092eb25bd515f3a32a1453aee8c9521d2ddb17dacd840237", size = 14949035, upload-time = "2026-04-13T02:45:06.021Z" }, - { url = "https://files.pythonhosted.org/packages/ca/4b/298ba2de0aafc0da3ff2288da06884aae7ba6489bc247c933f87847c41b3/mypy-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:1d55c7cd8ca22e31f93af2a01160a9e95465b5878de23dba7e48116052f20a8d", size = 10883216, upload-time = "2026-04-13T02:45:47.232Z" }, - { url = "https://files.pythonhosted.org/packages/c7/f9/5e25b8f0b8cb92f080bfed9c21d3279b2a0b6a601cdca369a039ba84789d/mypy-1.20.1-cp312-cp312-win_arm64.whl", hash = "sha256:f5b84a79070586e0d353ee07b719d9d0a4aa7c8ee90c0ea97747e98cbe193019", size = 9814299, upload-time = "2026-04-13T02:45:21.934Z" }, - { url = "https://files.pythonhosted.org/packages/21/e8/ef0991aa24c8f225df10b034f3c2681213cb54cf247623c6dec9a5744e70/mypy-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f3886c03e40afefd327bd70b3f634b39ea82e87f314edaa4d0cce4b927ddcc1", size = 14500739, upload-time = "2026-04-13T02:46:05.442Z" }, - { url = "https://files.pythonhosted.org/packages/23/73/416ebec3047636ed89fa871dc8c54bf05e9e20aa9499da59790d7adb312d/mypy-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e860eb3904f9764e83bafd70c8250bdffdc7dde6b82f486e8156348bf7ceb184", size = 13314735, upload-time = "2026-04-13T02:46:47.154Z" }, - { url = "https://files.pythonhosted.org/packages/10/1e/1505022d9c9ac2e014a384eb17638fb37bf8e9d0a833ea60605b66f8f7ba/mypy-1.20.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4b5aac6e785719da51a84f5d09e9e843d473170a9045b1ea7ea1af86225df4b", size = 13704356, upload-time = "2026-04-13T02:45:19.773Z" }, - { url = "https://files.pythonhosted.org/packages/98/91/275b01f5eba5c467a3318ec214dd865abb66e9c811231c8587287b92876a/mypy-1.20.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f37b6cd0fe2ad3a20f05ace48ca3523fc52ff86940e34937b439613b6854472e", size = 14696420, upload-time = "2026-04-13T02:45:24.205Z" }, - { url = "https://files.pythonhosted.org/packages/a1/57/b3779e134e1b7250d05f874252780d0a88c068bc054bcff99ca20a3a2986/mypy-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4bbb0f6b54ce7cc350ef4a770650d15fa70edd99ad5267e227133eda9c94218", size = 14936093, upload-time = "2026-04-13T02:45:32.087Z" }, - { url = "https://files.pythonhosted.org/packages/be/33/81b64991b0f3f278c3b55c335888794af190b2d59031a5ad1401bcb69f1e/mypy-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:c3dc20f8ec76eecd77148cdd2f1542ed496e51e185713bf488a414f862deb8f2", size = 10889659, upload-time = "2026-04-13T02:46:02.926Z" }, - { url = "https://files.pythonhosted.org/packages/1b/fd/7adcb8053572edf5ef8f3db59599dfeeee3be9cc4c8c97e2d28f66f42ac5/mypy-1.20.1-cp313-cp313-win_arm64.whl", hash = "sha256:a9d62bbac5d6d46718e2b0330b25e6264463ed832722b8f7d4440ff1be3ca895", size = 9815515, upload-time = "2026-04-13T02:46:32.103Z" }, - { url = "https://files.pythonhosted.org/packages/40/cd/db831e84c81d57d4886d99feee14e372f64bbec6a9cb1a88a19e243f2ef5/mypy-1.20.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:12927b9c0ed794daedcf1dab055b6c613d9d5659ac511e8d936d96f19c087d12", size = 14483064, upload-time = "2026-04-13T02:45:26.901Z" }, - { url = "https://files.pythonhosted.org/packages/d5/82/74e62e7097fa67da328ac8ece8de09133448c04d20ddeaeba251a3000f01/mypy-1.20.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:752507dd481e958b2c08fc966d3806c962af5a9433b5bf8f3bdd7175c20e34fe", size = 13335694, upload-time = "2026-04-13T02:46:12.514Z" }, - { url = "https://files.pythonhosted.org/packages/74/c4/97e9a0abe4f3cdbbf4d079cb87a03b786efeccf5bf2b89fe4f96939ab2e6/mypy-1.20.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c614655b5a065e56274c6cbbe405f7cf7e96c0654db7ba39bc680238837f7b08", size = 13726365, upload-time = "2026-04-13T02:45:17.422Z" }, - { url = "https://files.pythonhosted.org/packages/d7/aa/a19d884a8d28fcd3c065776323029f204dbc774e70ec9c85eba228b680de/mypy-1.20.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c3f6221a76f34d5100c6d35b3ef6b947054123c3f8d6938a4ba00b1308aa572", size = 14693472, upload-time = "2026-04-13T02:46:41.253Z" }, - { url = "https://files.pythonhosted.org/packages/84/44/cc9324bd21cf786592b44bf3b5d224b3923c1230ec9898d508d00241d465/mypy-1.20.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4bdfc06303ac06500af71ea0cdbe995c502b3c9ba32f3f8313523c137a25d1b6", size = 14919266, upload-time = "2026-04-13T02:46:28.37Z" }, - { url = "https://files.pythonhosted.org/packages/6e/dc/779abb25a8c63e8f44bf5a336217fa92790fa17e0c40e0c725d10cb01bbd/mypy-1.20.1-cp314-cp314-win_amd64.whl", hash = "sha256:0131edd7eba289973d1ba1003d1a37c426b85cdef76650cd02da6420898a5eb3", size = 11049713, upload-time = "2026-04-13T02:45:57.673Z" }, - { url = "https://files.pythonhosted.org/packages/28/08/4172be2ad7de9119b5a92ca36abbf641afdc5cb1ef4ae0c3a8182f29674f/mypy-1.20.1-cp314-cp314-win_arm64.whl", hash = "sha256:33f02904feb2c07e1fdf7909026206396c9deeb9e6f34d466b4cfedb0aadbbe4", size = 9999819, upload-time = "2026-04-13T02:46:35.039Z" }, - { url = "https://files.pythonhosted.org/packages/2d/af/af9e46b0c8eabbce9fc04a477564170f47a1c22b308822282a59b7ff315f/mypy-1.20.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:168472149dd8cc505c98cefd21ad77e4257ed6022cd5ed2fe2999bed56977a5a", size = 15547508, upload-time = "2026-04-13T02:46:25.588Z" }, - { url = "https://files.pythonhosted.org/packages/a7/cd/39c9e4ad6ba33e069e5837d772a9e6c304b4a5452a14a975d52b36444650/mypy-1.20.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:eb674600309a8f22790cca883a97c90299f948183ebb210fbef6bcee07cb1986", size = 14399557, upload-time = "2026-04-13T02:46:10.021Z" }, - { url = "https://files.pythonhosted.org/packages/83/c1/3fd71bdc118ffc502bf57559c909927bb7e011f327f7bb8e0488e98a5870/mypy-1.20.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef2b2e4cc464ba9795459f2586923abd58a0055487cbe558cb538ea6e6bc142a", size = 15045789, upload-time = "2026-04-13T02:45:10.81Z" }, - { url = "https://files.pythonhosted.org/packages/8e/73/6f07ff8b57a7d7b3e6e5bf34685d17632382395c8bb53364ec331661f83e/mypy-1.20.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dee461d396dd46b3f0ed5a098dbc9b8860c81c46ad44fa071afcfbc149f167c9", size = 15850795, upload-time = "2026-04-13T02:45:03.349Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e2/f7dffec1c7767078f9e9adf0c786d1fe0ff30964a77eb213c09b8b58cb76/mypy-1.20.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e364926308b3e66f1361f81a566fc1b2f8cd47fc8525e8136d4058a65a4b4f02", size = 16088539, upload-time = "2026-04-13T02:46:17.841Z" }, - { url = "https://files.pythonhosted.org/packages/1a/76/e0dee71035316e75a69d73aec2f03c39c21c967b97e277fd0ef8fd6aec66/mypy-1.20.1-cp314-cp314t-win_amd64.whl", hash = "sha256:a0c17fbd746d38c70cbc42647cfd884f845a9708a4b160a8b4f7e70d41f4d7fa", size = 12575567, upload-time = "2026-04-13T02:45:34.795Z" }, - { url = "https://files.pythonhosted.org/packages/22/a8/7ed43c9d9c3d1468f86605e323a5d97e411a448790a00f07e779f3211a46/mypy-1.20.1-cp314-cp314t-win_arm64.whl", hash = "sha256:db2cb89654626a912efda69c0d5c1d22d948265e2069010d3dde3abf751c7d08", size = 10378823, upload-time = "2026-04-13T02:45:13.35Z" }, - { url = "https://files.pythonhosted.org/packages/d8/28/926bd972388e65a39ee98e188ccf67e81beb3aacfd5d6b310051772d974b/mypy-1.20.1-py3-none-any.whl", hash = "sha256:1aae28507f253fe82d883790d1c0a0d35798a810117c88184097fe8881052f06", size = 2636553, upload-time = "2026-04-13T02:46:30.45Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8b/2bc7f489528db504221ad7eff6b3f83979ee57d066103c72a85d09046a30/mypy-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:79a1e79199e980fb41f219cb3d4746d25c64b0b623840d3ed57df1cdc13a6502", size = 14321495, upload-time = "2026-10-28T19:21:16.612Z" }, + { url = "https://files.pythonhosted.org/packages/18/9a/b2f037b4982dce33645296e4394b031ade60f697f468761fa3ea8e2f8ece/mypy-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e18d0da3898b3bcd16192f15a206e7b8b8dd0f4a0b3e51aa5bd5c2afe749e2c", size = 14467468, upload-time = "2026-10-28T19:22:10.629Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5a/379f9bfa18bf3f8d43668fbd9ae4f2922dd027a559c461ca9d41cde1d32e/mypy-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b69a15fc2e75858d6b15e91ed3a0dcff1a533265183b64734138791fe0f49aa5", size = 15421496, upload-time = "2026-10-28T19:22:48.005Z" }, + { url = "https://files.pythonhosted.org/packages/83/50/3bde18a2824ec296432cebe01752ccd0ab8a2ad784abe4390040d019b3bf/mypy-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:70971cc0bcb1d3f03d2eb68fa430a6481ccc03558dc001d9534d7e5fa274c392", size = 15670425, upload-time = "2026-10-28T19:21:57.026Z" }, + { url = "https://files.pythonhosted.org/packages/6a/36/8b722b88c325d317354de7d10d8fb9d4dc0f958433e873a91fc4ed21a06e/mypy-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:6f7225951a277dd0d6add900731d487174b7d24ac133c09d670ed63b4d8cb90d", size = 11300972, upload-time = "2026-10-28T19:21:48.724Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d6/fb2bab1245658d68af8abfe776362a780c621fe0f364f2c7b53039a945ee/mypy-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:be4c03f33c55500191df2ed4a9b5e7edbdd1fff067fe361a5a9b1b5d3262da6e", size = 10873349, upload-time = "2026-10-28T19:21:32.104Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1a/0be9dc6af5e0e04128f18a5e36b4ca44bfb0085857d37aa6a7de1ac2b0cb/mypy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d63737426a048a27705a4eedb828669f4bc0873612a1b46b0b9fe892b2ebaaaf", size = 14381321, upload-time = "2026-10-28T19:21:54.223Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a6/7907e36711a8a8c703f961f076fab61793383694140bd1fd87589f3f68d4/mypy-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7344869cad05f073b8cae2826774fa0161e3eb2f5c685b76813574008d77d750", size = 14466373, upload-time = "2026-10-28T19:22:25.211Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b8/7313212b29de570c25c9859d7c8e4ef2251a6d34338a1e20031d94297721/mypy-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a20ff6cd2af7a670c64f089bfa5fbcebe8ff3bf896c5e6ac7c9862c7960e4494", size = 15553666, upload-time = "2026-10-28T19:22:36.511Z" }, + { url = "https://files.pythonhosted.org/packages/23/8f/92966b6082ff49da9142e1e527eb9af4359a9a61632a1cfc313da96b37fe/mypy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9ed1195502cca32be5ef756b1693477f4991db2f2e34eaa90cc4ad6bea4f9531", size = 15802641, upload-time = "2026-10-28T19:21:13.421Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b4/874f6fd54b005020ad63e2cf7a59b178d16a771d478849b29b842cb0cf14/mypy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:4c194c5d878130fca1ae662dfb1feb6274afbab0b40032c80147377e0ea803d5", size = 11383067, upload-time = "2026-10-28T19:22:03.516Z" }, + { url = "https://files.pythonhosted.org/packages/b0/41/5ae8e6528c2faa96e4a921a999d2c7ac7c565bee184b4282bf62ffa76b5d/mypy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:71eda06e5bae51976ce869fda429159c9cec678bd02f25bb197f0265c8f991be", size = 10946882, upload-time = "2026-10-28T19:21:39.51Z" }, + { url = "https://files.pythonhosted.org/packages/6a/65/cd19bc06b67d2df4828220245fd98569ba40fac00c60ba48bef1817d8ac3/mypy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f1613bfe6726169a5680ec3dad5fe1f6d96285c12aded51ad432eadedd39d45", size = 14214179, upload-time = "2026-10-28T19:22:27.49Z" }, + { url = "https://files.pythonhosted.org/packages/c9/db/4d54ca4f49a0113d0c956980272aefdc6eec5458f7a72b350785b9ef2249/mypy-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:680ba5d9f0e3fe0c746b3d14df15f2a718e4bc308a1145616a1ea691ce7c354b", size = 14376217, upload-time = "2026-10-28T19:21:08.019Z" }, + { url = "https://files.pythonhosted.org/packages/a2/45/089b4724b85f0f408941be5298f817ee89181c1ce031a73d60ebb150c2c7/mypy-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb70621535c22fd4b1a0450d37ed1d6c5eb39656a1cebfb189084c12de54ba7", size = 15467623, upload-time = "2026-10-28T19:21:21.555Z" }, + { url = "https://files.pythonhosted.org/packages/27/4c/15edbc64b0b1412bfe94c74192b3b56554ab642b85a31bb06cef6313101f/mypy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4af5ad558033bf71a44a1be149ed61633522824f84c1b6bc2445d3fe2d2a5296", size = 15729867, upload-time = "2026-10-28T19:21:37.557Z" }, + { url = "https://files.pythonhosted.org/packages/76/5b/7d160fb685994b65b0ca86d352f3afb5424f96a9e6914719f7b861fb6701/mypy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:acdd82cb3a2eb10720e87706a128d75785d005a8c47e36a4973221524f41fc56", size = 11390802, upload-time = "2026-10-28T19:22:18.081Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ea/453d27f8d531f57c5f14aa73094eaa48e3fb77f680bdede2045cb64fa58b/mypy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:133886ef1a7a8cf673df55d865e41580c6c46aa9b8e31e1325dfe315c038f745", size = 10950427, upload-time = "2026-10-28T19:22:59.003Z" }, + { url = "https://files.pythonhosted.org/packages/7d/75/282d43d6fea7be984ac63e75ffd8eee146ba2f3b077bb1ca5bf3f973e0c4/mypy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c45485061ba08f700acc12cfc8b177e6997e6bc353605babfc0abb081e079456", size = 14233251, upload-time = "2026-10-28T19:22:44.977Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a8/9021263cc7e761451c7d8131bdfab3c0a5c2d67aa2d694670019e1ab86a7/mypy-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727890a32f1858c09549eb55a8b234fe990d460450b28f03e8213ef925f1247c", size = 14394953, upload-time = "2026-10-28T19:21:29.492Z" }, + { url = "https://files.pythonhosted.org/packages/16/10/4192a5cea09ebbc41c534f57bd0d8f0fb42a8e810632cda6e37f072c59fd/mypy-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:beb254fec368c4e7f7f82a5f310c6470b893ec88a90b66426da971c6826b0453", size = 15442824, upload-time = "2026-10-28T19:22:42.068Z" }, + { url = "https://files.pythonhosted.org/packages/99/e6/d3b0a7474aa4f696594a47a218b6796d9c4346fc81ed288904278cddd9bd/mypy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ec416d5d2ae689e681f5727e328723f18d8b5dc8c903a146b7851dc5727aad8a", size = 15717227, upload-time = "2026-10-28T19:22:54.026Z" }, + { url = "https://files.pythonhosted.org/packages/48/6a/2ec530dacd4f77ca50d0fa05b854098f2d75e5d18c587d2612b44f10d68c/mypy-2.4.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:0e95ad0bec03ced9a73b42514208abd24c789e67f0e1eba476e276dd7a2dea3a", size = 7898637, upload-time = "2026-10-28T19:21:41.709Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/de9d7bad12e3868d5f1f4279aed20b1b568b2c5e166f2d3a05b211dca0b1/mypy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:51f742d4154824837ca6e92aa81f00eec222700a3c337bd258e5562a3880d75a", size = 11576656, upload-time = "2026-10-28T19:22:13.232Z" }, + { url = "https://files.pythonhosted.org/packages/b8/12/4fa9883b9b59e5c96ff62845ad019dc47786031c45d67eaccf452cffffd8/mypy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:f44dd4c5ab22df21206dd926c98381ebdbf5bfd696df37dd38ded2d23143fcb6", size = 11174403, upload-time = "2026-10-28T19:23:01.346Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f2/827e356fc4f74c60b8da42663336e035b49d948a3c11224b0fc7543086b1/mypy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2875280687e64740e6ddcef43bd4c5852ad45ff34549740f6aea3f0373870b93", size = 15713427, upload-time = "2026-10-28T19:21:26.757Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fa/e58a4fcae1d238189eac758036c0921992a86e3e6209190efd96d61fc5d4/mypy-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea0714d2bccaf6d010c970e1d3ba2c9c2add79d2030aad2ef03167d788948276", size = 15938481, upload-time = "2026-10-28T19:22:34.371Z" }, + { url = "https://files.pythonhosted.org/packages/a1/54/a20f881801b88900923f2be1541a4b438ad0f0883e03836641fdc3d7adf4/mypy-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c6ff0e7ea546943001a91639c9f0c64b60d239268fa5d29e4a6c87f82c2373", size = 17243006, upload-time = "2026-10-28T19:22:39.425Z" }, + { url = "https://files.pythonhosted.org/packages/de/1c/ff84a6133a6e89c390bff6b25d9bad6598e7891b205f4b30b496906d636d/mypy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a5b5da107dc2c93fb0a22f7a45dbaea5dc131afc7056b5468d78e819f9a138e6", size = 17557728, upload-time = "2026-10-28T19:21:43.607Z" }, + { url = "https://files.pythonhosted.org/packages/15/4a/effa47d39b8fadf679c08e7c78559e523f9769a42a75662d2d15b2b9cdd6/mypy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c3a4d0c47053cc90035dd3a2c321112b593ad42935d0cbfd747288e1564fa8ca", size = 12370826, upload-time = "2026-10-28T19:22:29.779Z" }, + { url = "https://files.pythonhosted.org/packages/46/9f/b214ececb1a97fbb61ab3dfd48ae3cffd92ae2c148af4c9dc94c6c56772b/mypy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0c5bf267645351887b72903fd2f5fbddcf85234e0e6c4ad94e2fcfb4f0c3f971", size = 11851083, upload-time = "2026-10-28T19:23:11.288Z" }, + { url = "https://files.pythonhosted.org/packages/57/5b/e44a2edb0b25a9f1429de770e1d0238af23302671dd40c77ac5efeb0c559/mypy-2.4.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:db599d03e7d60d15f2e0a11665c18bc52a88699fcab990fdf8cecd5be8ac801c", size = 14243144, upload-time = "2026-10-28T19:21:10.975Z" }, + { url = "https://files.pythonhosted.org/packages/59/d4/5c99dc3a28e11d2f0fcbb02df63d2d3e574dedc0f9f69d50901df305a7a6/mypy-2.4.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6c7210e40dd423af7d65cbe428b5906d11dc60659337bdae4e5fdf02ce32ed7", size = 14409316, upload-time = "2026-10-28T19:22:15.515Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c9/3108b38f4c30eabd80194f5d9771a87e27196c5e0ddb5bf4b99c6ed1e97e/mypy-2.4.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2d4a51db92059eecc2c33b2a5194b28585ebfd917292c015fe87a77dbff6cd2", size = 15445573, upload-time = "2026-10-28T19:22:05.635Z" }, + { url = "https://files.pythonhosted.org/packages/dd/12/84a53eceaf11c46dfd7931fb6b8902d01fb33787d3a991e81c461458150b/mypy-2.4.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:01b7718552d017f88d1151f3cc9d6556d778a90bd58060a2c3d87d070f9099fb", size = 15732989, upload-time = "2026-10-28T19:23:06.512Z" }, + { url = "https://files.pythonhosted.org/packages/65/ba/bf017dc2abdf83b2f74e29c43834b7af67dc1f1b20202f02dae60b4781a0/mypy-2.4.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:3467b1d13000812ab85dc2a3834b9379ec77798ca1bcabe434e23fb21fead692", size = 7898932, upload-time = "2026-10-28T19:21:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/76/05/0c22b1f0390120a8592d31209cb6b9f932b6a0b426fe643e550de15139e3/mypy-2.4.1-cp315-cp315-win_amd64.whl", hash = "sha256:080cf64a60b90a73435542feecf4171904cc5710658165b40c6869115d4aa100", size = 11577577, upload-time = "2026-10-28T19:22:01.302Z" }, + { url = "https://files.pythonhosted.org/packages/73/fb/ac01e173629697ddac2f11e51efd10347d673f9a7311b03f4aba710ebccd/mypy-2.4.1-cp315-cp315-win_arm64.whl", hash = "sha256:f9118a3f50de264b614861bb11814833a742138ac3c8e2e49a860b6977fb2704", size = 11173540, upload-time = "2026-10-28T19:21:46.444Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e4/b9b9c8eb09e70927ee65b1456cfed35710b6e4ff0b07f364ceaf2202307c/mypy-2.4.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9d7dafd56179d296651df9e1844b91c9517c379398f017a2a435d077a28943c1", size = 15706623, upload-time = "2026-10-28T19:22:20.528Z" }, + { url = "https://files.pythonhosted.org/packages/26/74/1ceb1ca884add0565e618d013dd24333039100dc0fcc42661561f2e1cae5/mypy-2.4.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d923cec43316f0e6736b8298fd00abeaaf348a091c20e7296b3aebceecb63eca", size = 15944095, upload-time = "2026-10-28T19:21:04.779Z" }, + { url = "https://files.pythonhosted.org/packages/2b/68/27bcf9b4efa22733b6050485ae37845b94c9b8851abe4fc60632d905c32d/mypy-2.4.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6e081b466e88d4343adffe11aff0f02df34b5b9070b4b700053a0b7294c0be1", size = 17223065, upload-time = "2026-10-28T19:23:03.724Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a3/ed5724b317bf585c29f3c8be2792e97d45925d65a9c8af40ceeb267072e6/mypy-2.4.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:70196470ac300f444a302a738541101893c2c57804078b33690286a7899149e9", size = 17560589, upload-time = "2026-10-28T19:21:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/42/2f/005185e6ab4752fd6cc2d1a6d1421fe21979770afe2490c00c555f34a913/mypy-2.4.1-cp315-cp315t-win_amd64.whl", hash = "sha256:de95648e0a6d46fa9929b956ed2c87c5dc9933d4d59945e6390e9d753c74e7c1", size = 12353324, upload-time = "2026-10-28T19:22:51.481Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d6/20420cc9c780087139662534d128c2f3274361f8bb7e9cf0d5495a846ed5/mypy-2.4.1-cp315-cp315t-win_arm64.whl", hash = "sha256:ef8d5be90032261197ac388d945d8ff4f476c95880798a1911760af64ddfe71f", size = 11845599, upload-time = "2026-10-28T19:22:23.136Z" }, + { url = "https://files.pythonhosted.org/packages/21/5c/6e8010f8f2c4ad817364592d4b3883633417269b913fc9d5174fdeb7e565/mypy-2.4.1-py3-none-any.whl", hash = "sha256:1784648d6ec8c2b5465e3f26241b845a0b3cb4e56c993c64abe16989e7698999", size = 2801286, upload-time = "2026-10-28T19:21:19.516Z" }, ] [[package]] name = "mypy-extensions" -version = "1.1.0" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/6f/43edf3be2748d0e40974a5ee046a8bb4c9961a4bdd3e7d5c6dcbf32d1af6/mypy_extensions-1.2.0.tar.gz", hash = "sha256:c6e275a07f1c2fc096b62940452d0eaa3f3af7de45d704787866240a7c6d5a51", size = 6566, upload-time = "2026-11-03T16:26:33.692Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/7b1adac7e97ce7bbda34792dc6e9b2f5cc5cffd1875883f78a818b2a2687/mypy_extensions-1.2.0-py3-none-any.whl", hash = "sha256:993e7a20aa5671eb41a7132cf555af30062da99729601601b9a8382b11cae768", size = 5114, upload-time = "2026-11-03T16:26:32.731Z" }, ] [[package]] @@ -403,72 +499,70 @@ requires-dist = [{ name = "httpx", specifier = ">=0.27,<1.0" }] [package.metadata.requires-dev] dev = [ - { name = "mypy", specifier = ">=1.11" }, - { name = "pytest", specifier = ">=8.0" }, - { name = "pytest-cov", specifier = ">=5.0" }, - { name = "respx", specifier = ">=0.21" }, - { name = "ruff", specifier = ">=0.11" }, + { name = "mypy", specifier = ">=2.4" }, + { name = "pytest", specifier = ">=9.2" }, + { name = "pytest-cov", specifier = ">=7.1" }, + { name = "respx", specifier = ">=0.23" }, + { name = "ruff", specifier = ">=0.17" }, ] -lint = [{ name = "ruff", specifier = ">=0.11" }] +lint = [{ name = "ruff", specifier = ">=0.17" }] test = [ - { name = "pytest", specifier = ">=8.0" }, - { name = "pytest-cov", specifier = ">=5.0" }, - { name = "respx", specifier = ">=0.21" }, + { name = "pytest", specifier = ">=9.2" }, + { name = "pytest-cov", specifier = ">=7.1" }, + { name = "respx", specifier = ">=0.23" }, ] -typecheck = [{ name = "mypy", specifier = ">=1.11" }] +typecheck = [{ name = "mypy", specifier = ">=2.4" }] [[package]] name = "packaging" -version = "26.0" +version = "26.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, ] [[package]] name = "pathspec" -version = "1.0.4" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] [[package]] name = "pluggy" -version = "1.6.0" +version = "1.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/db/7fc19e6f2dc92a966727031389fc2e08b558f0f25eb7403c1119ad4713cd/pluggy-1.7.0.tar.gz", hash = "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8", size = 123304, upload-time = "2026-10-15T09:50:58.343Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, + { url = "https://files.pythonhosted.org/packages/40/9e/2b38731e0fc536806f16490e1a12d7f0dc2a1235aa8cc07bcc75416a7daa/pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec", size = 27082, upload-time = "2026-10-15T09:50:56.808Z" }, ] [[package]] name = "pygments" -version = "2.20.0" +version = "2.21.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +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/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, + { 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 = "pytest" -version = "9.0.3" +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/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +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/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, + { 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]] @@ -476,7 +570,7 @@ name = "pytest-cov" version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage", extra = ["toml"] }, + { name = "coverage" }, { name = "pluggy" }, { name = "pytest" }, ] @@ -499,88 +593,34 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" }, - { url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" }, - { url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" }, - { url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" }, - { url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" }, - { url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" }, - { url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" }, - { url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" }, - { url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" }, - { url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" }, - { url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" }, - { url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" }, - { url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" }, - { url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" }, - { url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" }, -] - -[[package]] -name = "tomli" -version = "2.4.1" +version = "0.17.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +sdist = { url = "https://files.pythonhosted.org/packages/84/0e/3d154514b5bad104f7e1c13169c2d09aa9959fccdc06eb47610ccdfa83ed/ruff-0.17.4.tar.gz", hash = "sha256:f32d178b50e826e00f6c6c1be3f56333068acb9b67fe167143527fa9b81e890d", size = 5357750, upload-time = "2026-11-05T18:52:17.638Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, - { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, - { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, - { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, - { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, - { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, - { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, - { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, - { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, - { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, - { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, - { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, - { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, - { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, - { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, - { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, - { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, - { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, - { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, - { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, - { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, - { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, - { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, - { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, - { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, - { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, - { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, - { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, - { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, - { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, - { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, - { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, - { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, - { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, - { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, - { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, - { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, - { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, - { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, - { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, - { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, + { url = "https://files.pythonhosted.org/packages/e0/61/c83c78bbb6108f6c6270a106cf12b54175f4f42da088b9bb880f18e31289/ruff-0.17.4-py3-none-linux_armv6l.whl", hash = "sha256:2b08c4ff98dc234f4a3871a3105353b922fa875ea1073ca31001eb8c06bb4d89", size = 10252195, upload-time = "2026-11-05T18:51:36.594Z" }, + { url = "https://files.pythonhosted.org/packages/0a/12/dd158708378866e8b53e145a465dbc51a7cccb9fa197a74d67eaedde7234/ruff-0.17.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:13e781c3c5f663e7fba737eaaeb70dd943dd55171daf316afb25cd5268cfe0c7", size = 10266044, upload-time = "2026-11-05T18:51:39.502Z" }, + { url = "https://files.pythonhosted.org/packages/28/32/89a94a8eb11fd505a381ae6f04e190d038d9958695209dc1786ce5cfcda3/ruff-0.17.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ed2f416349944f908f9574215994602242b0836844eba48eec61473033afca0", size = 9432227, upload-time = "2026-11-05T18:51:41.968Z" }, + { url = "https://files.pythonhosted.org/packages/58/de/a392dc38b215798ae72e639d228b7f6c02d826a95d1c0ad7c4c0ee49ef5b/ruff-0.17.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ea6e0017c785f7282092b11e46338e4ebed1b9e575ebf2e55a8a1eda968241a", size = 10136900, upload-time = "2026-11-05T18:51:44.562Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8f/a880f60f42005f6ffdf58f77af34c7ee327000b5607f48a2f6a69d8b242c/ruff-0.17.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7e061dcc0933c7031a616ac8d66ac540309b2aa21ab558101a8f53e9ac7a1185", size = 10051245, upload-time = "2026-11-05T18:51:46.7Z" }, + { url = "https://files.pythonhosted.org/packages/09/55/98844264108af7d5ec27ca3a9cd5a989094ca657a14599c4b69a537a0e9a/ruff-0.17.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f99e95a1515161138be3cbd1b06c06e739d8feb38ade35564556999fffc3686e", size = 10704341, upload-time = "2026-11-05T18:51:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/28/a2/5edf97659c3ac7c73f978003bd314936f671037fdac6b978e9de3e4b2b65/ruff-0.17.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f28cf0dd37cb172fa5a8d2e92e6ada3a9dbd0609f094ed24039929eef0b7524", size = 11394465, upload-time = "2026-11-05T18:51:51.534Z" }, + { url = "https://files.pythonhosted.org/packages/f2/8f/51371ae856b2bef4d15a044a574695fbc0f6ae63aafa42a2a22428070fb6/ruff-0.17.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b21c771b296888e8a35c6f48555f4b44e4483f61675d6ed51810a5203469ce6", size = 10988262, upload-time = "2026-11-05T18:51:53.91Z" }, + { url = "https://files.pythonhosted.org/packages/af/1b/62874f14c7873f27c7887d2b1ea41de990e14ba9518801ffe8eacb5d51a6/ruff-0.17.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cd025509b2e15721b426fc70fd946d71f43b3db3b08e946dfec2d696d911690", size = 10572829, upload-time = "2026-11-05T18:51:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/98/30/860b9dae67432b16b88558a9cb2d3194825fa62838f8b9cc49f5d9022095/ruff-0.17.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:49b77f7b3921669466882a4881168801bdb06ec2cd83fb936049016cf5fdece1", size = 10617545, upload-time = "2026-11-05T18:51:58.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/08/781a4b41a1c83fd6e30fea610d6502a6f94e49cf94693fd37bdcbc6bfe11/ruff-0.17.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e3b84eb74f260d1c2dbd236fd9d4c011d4fdd0a52bdf8aea57a10e3a1b71fca5", size = 10111955, upload-time = "2026-11-05T18:52:00.59Z" }, + { url = "https://files.pythonhosted.org/packages/3c/e9/94b1c7b677120f30839254518924f73a8157e9bcfa200edcd5796fecdb99/ruff-0.17.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:20e43d7a08e712e33067ba887e6574546c79bd4831596ce8f16803ac7ff9ad90", size = 10058355, upload-time = "2026-11-05T18:52:03.61Z" }, + { url = "https://files.pythonhosted.org/packages/27/5f/a825910b9f309336dc33cb8c0774126662c1d2f765699b3e0a1bd538d01f/ruff-0.17.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0d1f4848fd2ec0499be4046db9b7dfcc952666885f00233cc7033515282556a8", size = 10415932, upload-time = "2026-11-05T18:52:06.264Z" }, + { url = "https://files.pythonhosted.org/packages/8b/b8/0796468423413ea4df1a6e2442ae26cf49b8f7dda0eecf246e0d5d0dde54/ruff-0.17.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6007e7f3a0e55b0fbdb947af69cf24c21b4928df6be8ef3e767c69b5ccccf496", size = 10826033, upload-time = "2026-11-05T18:52:08.675Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ab/d8e407669ffe5014429baf780c2df47e001968a6e4c17f615e2e7b13a9ba/ruff-0.17.4-py3-none-win32.whl", hash = "sha256:3971a78cba76298b284c8321e6d40d93045ed348bec5c369d85099efc97fb7cf", size = 9931993, upload-time = "2026-11-05T18:52:11.169Z" }, + { url = "https://files.pythonhosted.org/packages/bd/15/42af8a72d8946e3c9efd2bbf6641e462b6832783b0d043d63ef5b280ba7f/ruff-0.17.4-py3-none-win_amd64.whl", hash = "sha256:67c5a894fc054cf41f75f7afa24815de5a74329d97634126c2a1015740ff4e8c", size = 10292922, upload-time = "2026-11-05T18:52:13.436Z" }, + { url = "https://files.pythonhosted.org/packages/68/9e/f1dbffcfb8ff25dadbc21f87a8b73c3848abe4abada9d4b08ccef8bec40c/ruff-0.17.4-py3-none-win_arm64.whl", hash = "sha256:181ae38a9a8fb94925bf7d0794327b48d7fd7f4b50cfd84c0e999cc09ef8d131", size = 9889396, upload-time = "2026-11-05T18:52:15.549Z" }, ] [[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" }, ] From 867a0faaa5c886d949ab10f9f1ca35c24c8ac201 Mon Sep 17 00:00:00 2001 From: secretpray Date: Sun, 8 Nov 2026 13:50:25 +0200 Subject: [PATCH 3/5] docs: rewrite the README and add user guides Rewrite the README for users of the published package: installation, a runnable quick start, where to find the API token in the dashboard, error handling and an overview of the resources. Links are absolute so that they work on PyPI. Add guides for client configuration, every resource method, and errors, retries and several API hosts. Use a failover host that resolves (api.octa.computer). Add docstrings to the public classes and methods. Date 0.1.0 in the changelog by its PyPI upload and add an Unreleased section. --- CHANGELOG.md | 36 +- README.md | 632 +++--------------- docs/errors-and-reliability.md | 138 ++++ docs/getting-started.md | 210 ++++++ docs/resources.md | 268 ++++++++ examples/basic.py | 13 +- examples/failover.py | 10 +- examples/persistent_client.py | 11 +- src/octaspace/__init__.py | 17 + src/octaspace/client.py | 21 + src/octaspace/config.py | 28 + src/octaspace/errors.py | 17 + src/octaspace/hooks.py | 11 + src/octaspace/models/account.py | 4 + src/octaspace/models/node.py | 4 + src/octaspace/models/session.py | 2 + src/octaspace/resources/accounts.py | 16 + src/octaspace/resources/apps.py | 9 + src/octaspace/resources/base.py | 2 + src/octaspace/resources/idle_jobs.py | 21 + src/octaspace/resources/network.py | 6 + src/octaspace/resources/nodes.py | 40 ++ src/octaspace/resources/services/__init__.py | 14 + .../resources/services/machine_rental.py | 31 +- src/octaspace/resources/services/render.py | 17 + .../resources/services/session_proxy.py | 23 + src/octaspace/resources/services/vpn.py | 17 + src/octaspace/resources/sessions.py | 10 + src/octaspace/response.py | 18 + 29 files changed, 1101 insertions(+), 545 deletions(-) create mode 100644 docs/errors-and-reliability.md create mode 100644 docs/getting-started.md create mode 100644 docs/resources.md diff --git a/CHANGELOG.md b/CHANGELOG.md index e419f04..abaf06f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,36 @@ # Changelog -All notable changes to this project will be documented in this file. +All notable changes to this project are documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.1.0] - 2026-04-13 +## [Unreleased] -- Initial Python SDK scaffold for OctaSpace. +### Changed + +- **Breaking:** Python 3.11 or later is required; Python 3.10 is no longer + supported. + +### Removed + +- **Breaking:** the `octaspace-smoke` command and `python -m octaspace`. + +## [0.1.0] - 2026-04-14 + +### Added + +- `OctaSpaceClient` with the resources `accounts`, `nodes`, `sessions`, + `apps`, `network`, `idle_jobs` and `services` (`mr`, `vpn`, `render` and + `session(uuid)`). +- `APIResponse` with the parsed JSON, raw body, headers and request ID. +- Error classes for each HTTP status, network errors and timeouts. +- Automatic retries with exponential backoff and `Retry-After`. +- Several API hosts with failover (`base_urls`). +- Keep-alive connection pool, `on_request` / `on_response` hooks and logging. +- `ClientConfig.from_env()` and a shared client (`octaspace.configure()`, + `octaspace.client()`). +- Models `Account`, `Balance`, `Node` and `Session`. + +[Unreleased]: https://github.com/octaspace/python-sdk/compare/v0.1.0...HEAD +[0.1.0]: https://github.com/octaspace/python-sdk/releases/tag/v0.1.0 diff --git a/README.md b/README.md index 3e8b2e1..1ddb271 100644 --- a/README.md +++ b/README.md @@ -1,591 +1,165 @@ -# octaspace +# OctaSpace Python SDK -Official Python SDK for the [OctaSpace API](https://api.octa.space/api-docs). +[![PyPI](https://img.shields.io/pypi/v/octaspace)](https://pypi.org/project/octaspace/) +[![Python](https://img.shields.io/pypi/pyversions/octaspace)](https://pypi.org/project/octaspace/) +[![CI](https://github.com/octaspace/python-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/octaspace/python-sdk/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/octaspace/python-sdk/blob/main/LICENSE) -## Features +The official Python library for the [OctaSpace](https://octa.space) API. Rent +machines, VPN gateways and render nodes, manage your sessions and the nodes you +host, and read network statistics from Python code. -- Resource-oriented API: `client.nodes.list()`, `client.services.session(uuid).stop()` -- Sync-first client built on `httpx` -- Keep-alive mode with configurable pool limits -- URL rotation / failover across multiple base URLs -- Retry with exponential backoff and `Retry-After` handling -- Typed error hierarchy with request ID and raw response access -- `on_request` / `on_response` hooks for logging, tracing, and diagnostics -- Shared client helpers for application-style integrations -- Browser playground for manual QA against mock scenarios or the live API -- Typed models for `Account`, `Balance`, `Node`, and `Session` +**Contents:** [Installation](#installation) · [Quick start](#quick-start) · +[Authentication](#authentication) · [Handling errors](#handling-errors) · +[API overview](#api-overview) · [Documentation](#documentation) · +[Versioning](#versioning) · [License](#license) ## Installation -### From PyPI - -Use this after the first public PyPI release. Until then, use the local development setup below. - ```bash pip install octaspace ``` -If you use `uv`: +or, with [uv](https://docs.astral.sh/uv/): ```bash uv add octaspace ``` -### Local Development Setup - -Recommended modern setup from the repository root: - -```bash -uv sync --group dev -``` - -This installs the SDK in editable mode together with test, lint, typecheck, and playground dependencies. - -If you prefer a plain virtualenv + `pip` flow: - -```bash -python3 -m venv .venv -.venv/bin/python -m pip install -e . pytest pytest-cov respx mypy ruff hatchling fastapi jinja2 python-multipart uvicorn -``` - -## Quick Start - -```python -from octaspace import OctaSpaceClient - -# Public endpoint -with OctaSpaceClient() as client: - print(client.network.info().data) - -# Authenticated endpoints -with OctaSpaceClient(api_key="your-api-key") as client: - print(client.accounts.profile().data) - print(client.accounts.balance().data) - print(client.nodes.list().data) -``` - -### Resource Examples - -```python -from octaspace import OctaSpaceClient - -client = OctaSpaceClient(api_key="your-api-key") - -# Accounts -client.accounts.profile() # GET /accounts -client.accounts.balance() # GET /accounts/balance -client.accounts.generate_wallet() # POST /accounts - -# Nodes -client.nodes.list() # GET /nodes -client.nodes.find(123) # GET /nodes/:id -client.nodes.download_ident(123) # GET /nodes/:id/ident -client.nodes.download_logs(123) # GET /nodes/:id/logs -client.nodes.update_prices(123, gpu_hour=0.5) # PATCH /nodes/:id/prices -client.nodes.reboot(123) # GET /nodes/:id/reboot - -# Sessions -client.sessions.list() # GET /sessions -client.sessions.list(recent=True) # GET /sessions?recent=true - -# Session proxy -session = client.services.session("uuid-here") -session.info() # GET /services/:uuid/info -session.logs() # GET /services/:uuid/logs -session.stop(score=5) # POST /services/:uuid/stop - -# Services -client.services.mr.list() # GET /services/mr -client.services.mr.create( - node_id=1, - disk_size=10, - image="ubuntu:24.04", - app="249b4cb3-3db1-4c06-98a4-772ba88cd81c", -) # POST /services/mr - -client.services.vpn.list() # GET /services/vpn -client.services.vpn.create(node_id=1, subkind="wg") # POST /services/vpn - -client.services.render.list() # GET /services/render -client.services.render.create(node_id=1, disk_size=100) # POST /services/render - -# Apps -client.apps.list() # GET /apps - -# Network -client.network.info() # GET /network - -# Idle Jobs -client.idle_jobs.find(node_id=69, job_id=42) # GET /idle_jobs/:node_id/:job_id -client.idle_jobs.logs(node_id=69, job_id=42) # GET /idle_jobs/:node_id/:job_id/logs - -client.close() -``` - -## Shared Client And App Integration - -Like the Ruby SDK, the Python package exposes a process-level shared client for app-style integrations. - -### Global configuration - -```python -import octaspace - -octaspace.configure( - api_key="your-api-key", - keep_alive=True, - pool_size=10, -) -``` - -### Shared client - -`octaspace.client()` with no arguments returns a lazily initialized shared client based on the current global configuration: - -```python -import octaspace - -octa_client = octaspace.client() -print(octa_client.network.info().data) -``` - -Pass arguments to create a one-off client instead: +Requires Python 3.11 or later. The only runtime dependency is +[httpx](https://www.python-httpx.org/). -```python -import octaspace +## Quick start -user_client = octaspace.client(api_key="per-user-token") -print(user_client.accounts.profile().data) -user_client.close() -``` +1. [Copy your API token](#authentication) and export it: -### Shutdown + ```bash + export OCTA_API_KEY="your-api-token" + ``` -When your process stops, or when a worker is recycling, close the shared client explicitly: +2. Run this program: -```python -import octaspace + ```python + import os -octaspace.shutdown_shared_client() -``` + from octaspace import AuthenticationError, OctaSpaceClient -You can also reset the global configuration: + with OctaSpaceClient(api_key=os.environ["OCTA_API_KEY"]) as client: + try: + balance = client.accounts.balance().data["balance"] + print(f"Balance: {balance / 10**18:.6f} OCTA") -```python -import octaspace + for machine in client.services.mr.list().data: + gpus = ", ".join(gpu["model"] for gpu in machine.get("gpus", [])) + print(machine["node_id"], machine["country"], gpus or "no GPU") + except AuthenticationError: + raise SystemExit("The API token is missing, wrong or regenerated.") from None + ``` -octaspace.reset_configuration() -``` +Every method returns an `APIResponse`; the parsed JSON is in `.data`. Create +one client, reuse it, and close it when you are done (the `with` block does +that for you). -## Configuration Reference - -| Option | Default | Description | -| --- | --- | --- | -| `api_key` | `None` | API key sent as the `Authorization` header | -| `base_url` | `https://api.octa.space` | Primary API endpoint | -| `base_urls` | `None` | Multiple endpoints enabling rotation / failover | -| `keep_alive` | `False` | Reuse pooled connections instead of forcing `Connection: close` | -| `pool_size` | `5` | Maximum keep-alive connections | -| `max_connections` | `20` | Total connection cap | -| `pool_timeout` | `5.0` | Seconds to wait for an available pooled connection | -| `idle_timeout` | `60.0` | Keep-alive expiry in seconds | -| `open_timeout` | `10.0` | TCP connect timeout | -| `read_timeout` | `30.0` | Response read timeout | -| `write_timeout` | `30.0` | Request body write timeout | -| `max_retries` | `2` | Retry attempts on retryable responses | -| `retry_interval` | `0.5` | Base retry interval in seconds | -| `backoff_factor` | `2.0` | Exponential backoff multiplier | -| `ssl_verify` | `True` | Toggle TLS certificate verification | -| `on_request` | `None` | Callable receiving `RequestContext` before each request | -| `on_response` | `None` | Callable receiving `APIResponse` after each response | -| `logger` | `None` | Standard-library logger used by hook dispatch | -| `user_agent` | auto | Override the default SDK user agent | - -### Environment Loading - -`ClientConfig.from_env()` reads configuration from environment variables: +## Authentication -```python -from octaspace.config import ClientConfig +The SDK authenticates with the **API token** of your OctaSpace account. You +find it in the OctaSpace dashboard: -config = ClientConfig.from_env() -``` +1. Sign in to the [OctaSpace dashboard](https://cube.octa.space). +2. In the sidebar, open **Account → Credentials**. +3. Copy the value of the **API Token** field. -Supported variables: - -- `OCTA_API_KEY` -- `OCTA_BASE_URL` -- `OCTA_BASE_URLS` -- `OCTA_KEEP_ALIVE` -- `OCTA_POOL_SIZE` -- `OCTA_MAX_CONNECTIONS` -- `OCTA_POOL_TIMEOUT` -- `OCTA_IDLE_TIMEOUT` -- `OCTA_OPEN_TIMEOUT` -- `OCTA_READ_TIMEOUT` -- `OCTA_WRITE_TIMEOUT` -- `OCTA_MAX_RETRIES` -- `OCTA_RETRY_INTERVAL` -- `OCTA_BACKOFF_FACTOR` -- `OCTA_SSL_VERIFY` -- `OCTA_LOGGER` -- `OCTA_USER_AGENT` - -`OCTA_BASE_URLS` accepts a comma-separated list. - -## Keep-Alive Mode +Your account has one API token. It authenticates API requests to manage your +account programmatically. -```python -from octaspace import OctaSpaceClient - -with OctaSpaceClient( - api_key="your-api-key", - keep_alive=True, - pool_size=10, - idle_timeout=120, -) as client: - print(client.transport_stats()) -``` - -Example transport stats: +Pass the token as `api_key`. The SDK sends it in the `Authorization: ` +header. ```python -{ - "mode": "persistent", - "limits": { - "max_keepalive_connections": 10, - "max_connections": 20, - "keepalive_expiry": 120.0, - }, - "url": "https://api.octa.space", -} -``` - -## URL Rotation / Failover +import os -```python from octaspace import OctaSpaceClient -with OctaSpaceClient( - api_key="your-api-key", - base_urls=[ - "https://api.octa.space", - "https://api2.octa.space", - ], -) as client: - print(client.transport_stats()) +client = OctaSpaceClient(api_key=os.environ["OCTA_API_KEY"]) ``` -Behaviour: - -- healthy endpoints are selected round-robin -- connection and timeout failures temporarily mark a URL as failed -- failed URLs automatically re-enter rotation after cooldown -- if only one URL is configured, no rotator is created +> **Important:** Regenerating the token (the refresh button next to +> **API Token**) invalidates the previous one at once, for every program that +> uses it. Update the token everywhere after you regenerate it. -## Hooks - -```python -import logging +> **Warning:** The API token acts on behalf of your account: it can start +> sessions that are charged to your balance. Keep it out of source code and +> version control. If it leaks, regenerate it under **Account → Credentials**. -from octaspace import OctaSpaceClient +### Calls without an API key -logger = logging.getLogger("octaspace-demo") +These methods also work without a token. Create the client without `api_key` +to use them anonymously: -def on_request(ctx) -> None: - logger.info("-> %s %s", ctx.method.upper(), ctx.path) +| Method | Without a token | +| --- | --- | +| `network.info()` | Works | +| `apps.list()` | Works | +| `services.mr.list()` | Works | -def on_response(response) -> None: - logger.info("<- %s %s", response.status_code, response.request_id) +Every other method needs a token. Without one, the API rejects the call, +usually with `AuthenticationError`. -with OctaSpaceClient( - api_key="your-api-key", - on_request=on_request, - on_response=on_response, - logger=logger, -) as client: - client.network.info() -``` +## Handling errors -## Error Handling +Every error the SDK raises is an `octaspace.OctaSpaceError`. Catch the specific +class you can handle: ```python -from octaspace import ( - APIConnectionError, - APIPermissionError, - APITimeoutError, - AuthenticationError, - NotFoundError, - OctaSpaceClient, - RateLimitError, - ValidationError, -) +from octaspace import APIError, AuthenticationError, NotFoundError, RateLimitError try: - with OctaSpaceClient(api_key="token") as client: - client.nodes.find(999_999) -except NotFoundError as exc: - print("Not found", exc.request_id) + client.services.session("96e33710-ec12-11ed-8588-fa163e49e8aa").info() except AuthenticationError: - print("Invalid API key") -except APIPermissionError: - print("Permission denied") -except ValidationError as exc: - print("Validation failed", exc) + ... # the API token is missing, wrong or regenerated +except NotFoundError: + ... # unknown session except RateLimitError as exc: - print("Retry after", exc.retry_after) -except (APIConnectionError, APITimeoutError) as exc: - print("Network issue", exc) -``` - -### Error Hierarchy - -```text -OctaSpaceError -├── ConfigurationError -├── NetworkError -│ ├── APIConnectionError -│ └── APITimeoutError -└── APIError - ├── AuthenticationError 401 - ├── APIPermissionError 403 - ├── NotFoundError 404 - ├── ValidationError 422 - ├── RateLimitError 429 - └── ServerError 5xx - ├── BadGatewayError 502 - ├── ServiceUnavailableError 503 - └── GatewayTimeoutError 504 + ... # too many requests; wait exc.retry_after seconds +except APIError as exc: + print(exc.status_code, exc) # any other error response, with the API's message ``` -All `APIError` subclasses expose: +See [Errors and reliability](https://github.com/octaspace/python-sdk/blob/main/docs/errors-and-reliability.md). -- `status_code` -- `request_id` -- `response` +## API overview -`RateLimitError` also exposes `retry_after`. +| Resource | Dashboard section | Operations | Guide | +| --- | --- | --- | --- | +| `client.services.mr` | Marketplace → Compute | List machines, start a session | [Machine rental](https://github.com/octaspace/python-sdk/blob/main/docs/resources.md#machine-rental) | +| `client.services.vpn` | Marketplace → VPN | List gateways, start a session | [VPN](https://github.com/octaspace/python-sdk/blob/main/docs/resources.md#vpn) | +| `client.services.render` | Render → Launch Nodes | List render nodes, start a session | [Render](https://github.com/octaspace/python-sdk/blob/main/docs/resources.md#render) | +| `client.sessions`, `client.services.session(uuid)` | Marketplace → Sessions | List, info, logs, stop | [Sessions](https://github.com/octaspace/python-sdk/blob/main/docs/resources.md#sessions) | +| `client.apps` | Marketplace → Applications | List applications | [Applications](https://github.com/octaspace/python-sdk/blob/main/docs/resources.md#applications) | +| `client.nodes` | Hosting → Nodes | Your nodes, ident, logs, prices, reboot | [Nodes](https://github.com/octaspace/python-sdk/blob/main/docs/resources.md#nodes) | +| `client.idle_jobs` | Hosting → Idle Jobs | Job details and logs | [Idle jobs](https://github.com/octaspace/python-sdk/blob/main/docs/resources.md#idle-jobs) | +| `client.accounts` | Account → Profile, Account → Billing | Profile, balance, deposit wallet | [Accounts](https://github.com/octaspace/python-sdk/blob/main/docs/resources.md#accounts) | +| `client.network` | — | Network and market statistics | [Network](https://github.com/octaspace/python-sdk/blob/main/docs/resources.md#network) | -## Typed Models - -Resources return `APIResponse` objects by default. If you want structured helper objects, use the typed models explicitly: - -```python -from octaspace import OctaSpaceClient -from octaspace.models import Node - -with OctaSpaceClient(api_key="your-api-key") as client: - response = client.nodes.find(123) - node = Node.from_dict(response.data) - print(node.id) - print(node.state) - print(node.online) -``` +The fields of every response are described in the +[OctaSpace API reference](https://api.octa.space/api-docs). -Available models: `Node`, `Account`, `Balance`, `Session`. +## Documentation -## Browser Playground - -The repository includes a development-only FastAPI playground. It is **not included in the published PyPI wheel** and exists for manual SDK QA, mirroring the role of the Ruby dummy app. - -### Start - -From the repository root: - -```bash -uv sync --group playground -uvicorn playground.main:app --reload -``` - -Open: - -```text -http://127.0.0.1:8000/playground/dashboard -``` - -### What The Playground Covers - -- the same page set and general layout direction as the Ruby dummy app -- sidebar runtime settings for API key, transport scenario, and base URL -- direct pages for dashboard, network, account, apps, nodes, sessions, services, idle jobs, and diagnostics -- footer request log for every SDK call triggered through the playground -- deterministic offline QA via the `Fixtures` scenario -- failure simulation via `401`, `403`, `404`, `429`, `500`, `slow`, `timeout`, and `network-error` -- live QA against the real API when `Transport = Real API` - -### Routes - -| Route | Content | +| Guide | Covers | | --- | --- | -| `/playground/dashboard` | Network + account summary | -| `/playground/network` | Public network endpoint and transport summary | -| `/playground/account` | Profile + balance view | -| `/playground/apps` | App catalog | -| `/playground/nodes` | Node list + detail panel | -| `/playground/nodes/{id}` | Standalone node detail | -| `/playground/sessions` | Current and recent sessions | -| `/playground/services` | Machine Rental, Render, and VPN tabs | -| `/playground/idle-jobs` | Idle job details + logs | -| `/playground/diagnostics` | Direct SDK call execution + smoke suite | - -Notes: - -- the sidebar intentionally mirrors the Ruby dummy app navigation, so `Account` exists as a direct route but is not shown in the nav -- request log lives in the footer, not on a separate `Requests` page -- diagnostics includes mutation methods; only run them against disposable targets when using real credentials - -Detailed QA steps live in: - -- [`docs/runbooks/browser-playground.md`](docs/runbooks/browser-playground.md) -- [`docs/runbooks/console-verification.md`](docs/runbooks/console-verification.md) - -## Repository Smoke Command - -The repository exposes both a module entry point and a script entry point: - -```bash -.venv/bin/python -m octaspace -.venv/bin/octaspace-smoke -``` - -If `OCTA_API_KEY` is present, smoke output includes `accounts.balance()` as well as `network.info()`. - -## Console And REPL Usage - -### Quick REPL Session - -```bash -.venv/bin/python -``` - -```python ->>> from octaspace import OctaSpaceClient ->>> client = OctaSpaceClient() ->>> client.network.info().data ->>> client.close() -``` - -### Authenticated REPL Session - -```bash -read -s OCTA_API_KEY -export OCTA_API_KEY -.venv/bin/python -``` - -`read -s OCTA_API_KEY` and `export OCTA_API_KEY` are shell commands, not Python statements. Run them in `zsh` / `bash`, type the token when prompted, press `Enter`, and only then start Python. - -```python ->>> import os ->>> from octaspace import OctaSpaceClient ->>> client = OctaSpaceClient(api_key=os.environ["OCTA_API_KEY"], keep_alive=True, pool_size=10) ->>> client.accounts.profile().data ->>> client.accounts.balance().data ->>> len(client.nodes.list().data) ->>> len(client.sessions.list().data) ->>> client.transport_stats() ->>> client.close() -``` - -If you are already inside Python REPL, set the variable there instead of using `read`: - -```python ->>> import os, getpass ->>> os.environ["OCTA_API_KEY"] = getpass.getpass("OCTA_API_KEY: ") -``` - -### Local Mock Console Script - -```bash -.venv/bin/python examples/console_mock.py -``` - -Expected output includes: - -- `transport_stats` -- `network` -- `profile` -- `balance` -- `nodes` -- `session_info` -- `session_stop` - -### Local Real Console Script - -```bash -.venv/bin/python examples/console_real.py -``` - -Optional environment variables: - -- `OCTA_API_KEY` -- `OCTA_SESSION_UUID` -- `OCTA_ALLOW_STOP=1` - -If `OCTA_API_KEY` is unset, the script still verifies the public network endpoint and skips authenticated checks explicitly. - -## Development - -### Local Checks - -```bash -.venv/bin/ruff check src tests examples playground -.venv/bin/mypy src/octaspace playground -.venv/bin/pytest -``` - -Or with `uv` if installed: - -```bash -uv run ruff check src tests examples playground -uv run mypy src/octaspace playground -uv run pytest -``` - -### Build And Wheel Smoke Test - -Build the distribution: - -```bash -.venv/bin/python -m build -``` - -Or with `uv`: - -```bash -uv build -``` - -Inspect the wheel contents (should only contain `octaspace/`, no `playground/`): - -```bash -python3 -m zipfile -l dist/octaspace-*.whl | grep -v dist-info -``` - -Create a clean environment and install the built wheel: - -```bash -python3 -m venv /tmp/octaspace-wheel-test -/tmp/octaspace-wheel-test/bin/python -m pip install dist/octaspace-*.whl -/tmp/octaspace-wheel-test/bin/python -c "from octaspace import OctaSpaceClient; print(OctaSpaceClient().network.info().data)" -``` - -### Verification Runbooks - -- [`docs/runbooks/console-verification.md`](docs/runbooks/console-verification.md) -- [`docs/runbooks/browser-playground.md`](docs/runbooks/browser-playground.md) -- [`docs/runbooks/publish.md`](docs/runbooks/publish.md) +| [Client configuration](https://github.com/octaspace/python-sdk/blob/main/docs/getting-started.md) | Options and defaults, environment variables, shared client, keep-alive, hooks and logging | +| [Resources](https://github.com/octaspace/python-sdk/blob/main/docs/resources.md) | Every method, with an example | +| [Errors and reliability](https://github.com/octaspace/python-sdk/blob/main/docs/errors-and-reliability.md) | Error classes, retries, several API hosts | +| [Changelog](https://github.com/octaspace/python-sdk/blob/main/CHANGELOG.md) | Changes in each release | -Release gate: +## Versioning -1. `ruff` clean -2. `mypy` clean -3. `pytest` all passing -4. console verification completed -5. browser playground verification completed -6. wheel smoke test passing -7. follow `docs/runbooks/publish.md` for TestPyPI → PyPI +The library follows [Semantic Versioning](https://semver.org). Until version +1.0, a minor release can contain breaking changes; the +[changelog](https://github.com/octaspace/python-sdk/blob/main/CHANGELOG.md) +marks them as **Breaking**. ## License -[MIT](LICENSE) +[MIT](https://github.com/octaspace/python-sdk/blob/main/LICENSE) diff --git a/docs/errors-and-reliability.md b/docs/errors-and-reliability.md new file mode 100644 index 0000000..8975fe6 --- /dev/null +++ b/docs/errors-and-reliability.md @@ -0,0 +1,138 @@ +# Errors and reliability + +[← Back to README](../README.md) + +**On this page:** [Error classes](#error-classes) · +[Reading an error](#reading-an-error) · [Retries](#retries) · +[Several API hosts](#several-api-hosts) + +The examples assume a client created as in the +[Quick start](../README.md#quick-start): + +```python +import os + +from octaspace import OctaSpaceClient + +client = OctaSpaceClient(api_key=os.environ["OCTA_API_KEY"]) +``` + +## Error classes + +Every error the SDK raises is an `octaspace.OctaSpaceError`. An error response +from the API raises a subclass of `APIError`: + +| Class | Status | Typical cause | +| --- | --- | --- | +| `AuthenticationError` | 401 | The API token is missing, wrong or regenerated | +| `APIPermissionError` | 403 | Access denied | +| `NotFoundError` | 404 | Unknown session, or a node that is not yours | +| `ValidationError` | 422 | A parameter is invalid | +| `RateLimitError` | 429 | Too many requests | +| `ServerError` | 5xx | A temporary problem on the API side | +| `BadGatewayError`, `ServiceUnavailableError`, `GatewayTimeoutError` | 502, 503, 504 | Subclasses of `ServerError` | +| `APIError` | any other | For example 400 when a session cannot be started (insufficient balance) or prices are invalid | + +When the API cannot be reached at all, the error is a `NetworkError`: +`APIConnectionError` (DNS, connection or TLS failure) or `APITimeoutError`. +An invalid client option raises `ConfigurationError`. All classes can be +imported from `octaspace`. + +```text +OctaSpaceError +├── ConfigurationError +├── NetworkError +│ ├── APIConnectionError +│ └── APITimeoutError +└── APIError + ├── AuthenticationError + ├── APIPermissionError + ├── NotFoundError + ├── ValidationError + ├── RateLimitError + └── ServerError + ├── BadGatewayError + ├── ServiceUnavailableError + └── GatewayTimeoutError +``` + +## Reading an error + +An `APIError` has the HTTP `status_code`, the `request_id` (or `None`) and the +full `response`. Its message is the API's reason when the response has one: + +```python +from octaspace import APIError + +try: + client.services.mr.create(node_id=312, image="ubuntu:24.04", disk_size=20) +except APIError as exc: + print(f"status {exc.status_code}: {exc}") # status 400: Insufficient balance + print(exc.response.data) +``` + +On a `RateLimitError`, `retry_after` is the number of seconds the API asks you +to wait, or `None`: + +```python +import time + +from octaspace import RateLimitError + +try: + sessions = client.sessions.list().data +except RateLimitError as exc: + time.sleep(exc.retry_after or 60) + sessions = client.sessions.list().data +``` + +> [!TIP] +> Include the request ID, if there is one, when you contact support about a +> failed call. + +## Retries + +GET requests are retried automatically on the statuses 408, 429, 500, 502, +503 and 504, up to `max_retries` times (default 2), with exponential backoff +and a little jitter. When the response has a `Retry-After` header, the SDK +waits that many seconds instead. After the last attempt the error is raised. + +> [!IMPORTANT] +> Starting a session (`create()`), stopping it (`stop()`), setting prices and +> generating a wallet are never retried, so that they cannot run twice. +> Decide yourself whether to repeat them after an error. `nodes.reboot()` is a +> GET request and is retried like the others. + +A connection error or a timeout is not retried on the same host. With +[several API hosts](#several-api-hosts) the request moves to the next host. + +Set the number of retries and the backoff with `max_retries`, +`retry_interval` and `backoff_factor`; see +[Client configuration](getting-started.md#options). + +## Several API hosts + +`base_urls` takes a list of API hosts. `https://api.octa.space` and +`https://api.octa.computer` serve the same API: + +```python +import os + +from octaspace import OctaSpaceClient + +with OctaSpaceClient( + api_key=os.environ["OCTA_API_KEY"], + base_urls=["https://api.octa.space", "https://api.octa.computer"], +) as client: + client.nodes.list() +``` + +Requests are spread over the hosts in turn. When a host cannot be reached or +times out, the request is sent to the next host, and the failed host is left +out for 30 seconds. An HTTP error response, including 5xx, does not move the +request to another host. + +> [!WARNING] +> This also applies to `create()` and `stop()`. A request that timed out may +> still have reached the API: before you start a session again, check +> `client.sessions.list()`. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..f7f68d6 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,210 @@ +# Client configuration + +[← Back to README](../README.md) + +**On this page:** [Creating a client](#creating-a-client) · +[Options](#options) · [Environment variables](#environment-variables) · +[Shared client](#shared-client) · [Timeouts](#timeouts) · +[Keep-alive connections](#keep-alive-connections) · +[Logging and hooks](#logging-and-hooks) · [Responses](#responses) + +## Creating a client + +`OctaSpaceClient` takes your [API token](../README.md#authentication) as +`api_key` and optional settings as keyword arguments: + +```python +import os + +from octaspace import OctaSpaceClient + +client = OctaSpaceClient( + api_key=os.environ["OCTA_API_KEY"], + read_timeout=15, + max_retries=3, + user_agent="my-service/1.0", +) +``` + +The client holds open connections. Close it when you no longer need it, or use +it as a context manager, which closes it on exit: + +```python +with OctaSpaceClient(api_key=os.environ["OCTA_API_KEY"]) as client: + client.accounts.profile() + +client = OctaSpaceClient(api_key=os.environ["OCTA_API_KEY"]) +try: + client.accounts.profile() +finally: + client.close() +``` + +An unknown or invalid option raises `octaspace.ConfigurationError`. + +## Options + +| Option | Default | Environment variable | Effect | +| --- | --- | --- | --- | +| `api_key` | `None` | `OCTA_API_KEY` | Your API token; without it only the [calls without a key](../README.md#calls-without-an-api-key) work | +| `base_url` | `https://api.octa.space` | `OCTA_BASE_URL` | Another API host | +| `base_urls` | `None` | `OCTA_BASE_URLS` (comma-separated) | Several API hosts, see [Several API hosts](errors-and-reliability.md#several-api-hosts); overrides `base_url` | +| `open_timeout` | `10.0` | `OCTA_OPEN_TIMEOUT` | Seconds to wait for a connection | +| `read_timeout` | `30.0` | `OCTA_READ_TIMEOUT` | Seconds to wait for a response | +| `write_timeout` | `30.0` | `OCTA_WRITE_TIMEOUT` | Seconds to wait while sending a request | +| `max_retries` | `2` | `OCTA_MAX_RETRIES` | Retries of failed requests that are safe to repeat; `0` turns them off. See [Retries](errors-and-reliability.md#retries) | +| `retry_interval` / `backoff_factor` | `0.5` / `2.0` | `OCTA_RETRY_INTERVAL` / `OCTA_BACKOFF_FACTOR` | Backoff between retries: `retry_interval × backoff_factor^attempt` seconds | +| `keep_alive` | `False` | `OCTA_KEEP_ALIVE` | Reuse connections, see [below](#keep-alive-connections) | +| `pool_size` | `5` | `OCTA_POOL_SIZE` | Connections kept open with `keep_alive` | +| `max_connections` | `20` | `OCTA_MAX_CONNECTIONS` | Most connections open at the same time | +| `pool_timeout` | `5.0` | `OCTA_POOL_TIMEOUT` | Seconds to wait for a free connection | +| `idle_timeout` | `60.0` | `OCTA_IDLE_TIMEOUT` | Seconds an unused connection stays open with `keep_alive` | +| `ssl_verify` | `True` | `OCTA_SSL_VERIFY` | Verify the server's TLS certificate | +| `user_agent` | `octaspace-python/` | `OCTA_USER_AGENT` | Value of the `User-Agent` header | +| `logger` | `None` | `OCTA_LOGGER` (logger name) | A `logging.Logger`, see [below](#logging-and-hooks) | +| `on_request` / `on_response` | `None` | — | Callables run before each request and after each response | + +Timeouts and intervals are in seconds. + +## Environment variables + +`OctaSpaceClient` does not read the environment by itself. To configure a +client from the variables in the table above, build a `ClientConfig` with +`from_env()`: + +```python +from octaspace import ClientConfig, OctaSpaceClient + +config = ClientConfig.from_env() +with OctaSpaceClient(config=config) as client: + client.accounts.balance() +``` + +Keyword arguments to `from_env()` override the environment: +`ClientConfig.from_env(max_retries=0)`. Boolean variables accept `1`, `true`, +`yes` and `on`. + +## Shared client + +To set options once for the whole process, call `octaspace.configure()`. +`octaspace.client()` without arguments returns one shared client built from +that configuration and creates it on first use: + +```python +import os + +import octaspace + +octaspace.configure(api_key=os.environ["OCTA_API_KEY"], keep_alive=True) + +octaspace.client().accounts.balance() +``` + +Calling `configure()` again closes the shared client; the next +`octaspace.client()` call creates a new one with the new options. Close it when +your process or worker stops: + +```python +octaspace.shutdown_shared_client() +``` + +`octaspace.client(api_key=..., **options)` with arguments returns a new client +based on the global configuration instead of the shared one. You own that +client: close it yourself. + +```python +with octaspace.client(api_key=os.environ["OCTA_API_KEY"]) as user_client: + user_client.accounts.profile() +``` + +`octaspace.reset_configuration()` closes the shared client and restores the +defaults. + +## Timeouts + +The timeouts apply to each attempt of a request. A request that is retried can +take longer in total; set `max_retries=0` if you need a hard limit. + +A request that takes too long raises `octaspace.APITimeoutError`. + +## Keep-alive connections + +By default, every request opens a new connection and sends +`Connection: close`. With `keep_alive=True` the client keeps up to `pool_size` +connections open and reuses them, which saves a TLS handshake per request: + +```python +with OctaSpaceClient( + api_key=os.environ["OCTA_API_KEY"], + keep_alive=True, + pool_size=10, + idle_timeout=120, +) as client: + print(client.transport_stats()) +``` + +`transport_stats()` returns the connection mode, the pool limits and the API +host in use: + +```python +{ + "mode": "persistent", + "limits": {"max_keepalive_connections": 10, "max_connections": 20, "keepalive_expiry": 120}, + "url": "https://api.octa.space", +} +``` + +## Logging and hooks + +Pass a `logging.Logger` as `logger`. The SDK logs the method and URL of each +request at `DEBUG` level: + +```text +octaspace -> GET https://api.octa.space/accounts/balance +``` + +> [!NOTE] +> The API token, other headers and request and response bodies are never +> logged. + +`on_request` receives a `RequestContext` with `method` (lower case), `path` and +`params`; `on_response` receives the `APIResponse`. Use them for metrics or +tracing: + +```python +from octaspace import OctaSpaceClient +from octaspace.hooks import RequestContext +from octaspace.response import APIResponse + + +def on_request(ctx: RequestContext) -> None: + print("->", ctx.method.upper(), ctx.path) + + +def on_response(response: APIResponse) -> None: + print("<-", response.status_code) + + +client = OctaSpaceClient( + api_key=os.environ["OCTA_API_KEY"], + on_request=on_request, + on_response=on_response, +) +``` + +An exception raised in a hook propagates to the caller of the SDK method. + +## Responses + +Every resource method returns an `octaspace.response.APIResponse`: + +| Attribute | Meaning | +| --- | --- | +| `data` | The parsed JSON body, or `None` if the body is empty or not JSON | +| `body` | The raw body as `bytes` | +| `status_code` | HTTP status | +| `headers` | Response headers | +| `request_id` | Value of the `X-Request-Id` header, or `None` | + +The fields of each response are described in the +[OctaSpace API reference](https://api.octa.space/api-docs). diff --git a/docs/resources.md b/docs/resources.md new file mode 100644 index 0000000..0d573d6 --- /dev/null +++ b/docs/resources.md @@ -0,0 +1,268 @@ +# Resources + +[← Back to README](../README.md) + +**On this page:** [Conventions](#conventions) · +[Machine rental](#machine-rental) · [VPN](#vpn) · [Render](#render) · +[Sessions](#sessions) · [Applications](#applications) · [Nodes](#nodes) · +[Idle jobs](#idle-jobs) · [Accounts](#accounts) · [Network](#network) + +The examples assume a client created as in the +[Quick start](../README.md#quick-start): + +```python +import os + +from octaspace import OctaSpaceClient + +client = OctaSpaceClient(api_key=os.environ["OCTA_API_KEY"]) +``` + +## Conventions + +| Concept | In the API | +| --- | --- | +| Results | Every method returns an `APIResponse`; `.data` holds the parsed JSON (a `dict` or a `list`). Field names are those of the [API reference](https://api.octa.space/api-docs). | +| Node | Identified by an integer `node_id`, for example `312`. | +| Session | Identified by its UUID, for example `96e33710-ec12-11ed-8588-fa163e49e8aa`. | +| Balance and charges | Integers in Wei of OCTA: 1 OCTA = 10¹⁸ Wei. | +| Sizes | `disk_size` in GB; `free_disk`, `total_memory` and traffic (`rx`, `tx`) in bytes. | +| Time | `started_at` in Unix milliseconds, `duration` in seconds. | + +## Machine rental + +Rent a machine and run your own container on it. In the dashboard: +**Marketplace → Compute**. + +### List machines + +```python +for machine in client.services.mr.list().data: + print(machine["node_id"], machine["city"], machine["cpu_cores"], machine["free_disk"]) +``` + +Also works without an API token. + +### Start a session + +```python +response = client.services.mr.create( + node_id=312, + image="ubuntu:24.04", + disk_size=20, # GB + http_ports=[8888], + ports=[22], + envs={"APP_ENV": "production"}, + start_command="python3 -m http.server 8888", +) +uuid = response.data["uuid"] +``` + +`node_id`, `image` and `disk_size` are required. To run an application from +**Marketplace → Applications**, pass its UUID as `app` (see +[Applications](#applications)). `http_ports` are exposed as HTTPS links, +`ports` as TCP/UDP ports. + +The session is created at once; its container is still being set up. Check +`is_ready` with [`info()`](#session-details). A rejected request, for example +for an insufficient balance, raises `octaspace.APIError` with status 400 and +the reason as its message. + +## VPN + +Rent a VPN gateway. In the dashboard: **Marketplace → VPN**. + +### List gateways + +```python +for gateway in client.services.vpn.list().data: + print(gateway["node_id"], gateway["country"], gateway["residential"]) +``` + +### Start a session + +```python +response = client.services.vpn.create(node_id=312, subkind="wg") +uuid = response.data["uuid"] +``` + +`subkind` is the VPN protocol, `wg` (WireGuard) by default. The +configuration to import into your VPN client is in `vpn_config` of +[`info()`](#session-details). + +## Render + +Rent a render node. In the dashboard: **Render → Launch Nodes**. + +### List render nodes + +```python +for node in client.services.render.list().data: + print(node["node_id"], node["city"], node["blender_optix_score"]) +``` + +### Start a session + +```python +response = client.services.render.create(node_id=312, disk_size=100) +uuid = response.data["uuid"] +``` + +`create()` sends its keyword arguments as the request body. To start a +dedicated render worker for each GPU, pass the field by its API name: +`client.services.render.create(node_id=312, disk_size=100, **{"multi-gpu-worker": True})`. + +## Sessions + +Your sessions of every kind. In the dashboard: **Marketplace → Sessions**. + +### List sessions + +```python +for session in client.sessions.list().data: + print(session["uuid"], session["service"], session["app_name"]) + +finished = client.sessions.list(recent=True).data +``` + +Without arguments, `list()` returns the active sessions; `recent=True` returns +the recent ones. + +### Session details + +```python +session = client.services.session("96e33710-ec12-11ed-8588-fa163e49e8aa") +info = session.info().data +if info["is_ready"]: + print(info["urls"], info["ssh_direct"]) +``` + +The fields depend on the kind of session: links to exposed HTTP ports +(`urls`), SSH access (`ssh_direct`, `ssh_proxy`, `ssh_web`), the VPN +configuration (`vpn_config`), the amount charged so far (`charge_amount`, in +Wei), traffic and duration. An unknown session raises `NotFoundError`. + +### Logs + +```python +logs = session.logs().data +print(logs["container"]) +``` + +`container` holds the container output, `system` the platform's events for +the session. + +### Stop + +```python +session.stop(score=5) +``` + +`score` is your optional rating of the session, an integer. Stopping ends the +session and its charges. + +## Applications + +Ready-made images with their ports and settings. In the dashboard: +**Marketplace → Applications**. + +```python +for app in client.apps.list().data: + print(app["uuid"], app["name"], app["category"], app["image"]) +``` + +Also works without an API token. Pass an application's `uuid` and `image` to +[`services.mr.create()`](#start-a-session). + +## Nodes + +The nodes you host. In the dashboard: **Hosting → Nodes**. + +### List and get + +```python +nodes = client.nodes.list().data +node = client.nodes.find(312).data +print(node["state"], node["os_version"], node["location"]) +``` + +`find()` of a node that is not yours or does not exist raises +`NotFoundError`. + +### Ident and logs + +```python +ident = client.nodes.download_ident(312).body +logs = client.nodes.download_logs(312).body +``` + +The node's identity file and its logs. Use `.body` (bytes) when the response +is not JSON. + +### Set prices + +```python +client.nodes.update_prices(312, base=25, storage=2, traffic=1) +``` + +Pass one or more of `base`, `storage` and `traffic`, each an integer from 0 +to 10000; see `node_prices` in the +[API reference](https://api.octa.space/api-docs) for the units. Any other +field or value raises `APIError` with status 400. + +### Reboot + +```python +client.nodes.reboot(312) +``` + +## Idle jobs + +The jobs your nodes run while nobody rents them. In the dashboard: +**Hosting → Idle Jobs**. + +```python +job = client.idle_jobs.find(node_id=312, job_id=42).data +logs = client.idle_jobs.logs(node_id=312, job_id=42).data +``` + +## Accounts + +In the dashboard: **Account → Profile** and **Account → Billing**. + +### Profile + +```python +profile = client.accounts.profile().data +print(profile["email"], profile["wallet"]) +``` + +### Balance + +```python +balance = client.accounts.balance().data["balance"] +print(f"{balance / 10**18:.6f} OCTA") +``` + +The balance is an integer in Wei. Use `decimal.Decimal(balance) / 10**18` when +you need an exact amount. + +### Generate a deposit wallet + +```python +address = client.accounts.generate_wallet().data["address"] +``` + +Creates the deposit wallet address of your account. + +## Network + +Statistics of the OctaSpace network and its GPU market. Works without an API +token. + +```python +network = client.network.info().data +print(network["market_price"]) +for model, stats in network["marketplace"]["gpus"].items(): + print(model, stats["count"], stats["avg_price"]) +``` diff --git a/examples/basic.py b/examples/basic.py index f4a7289..94e7387 100644 --- a/examples/basic.py +++ b/examples/basic.py @@ -1,5 +1,12 @@ +"""Print the account balance and the machines available for rent.""" + +import os + from octaspace import OctaSpaceClient -with OctaSpaceClient(api_key="your-api-key") as client: - print(client.network.info().data) - print(client.accounts.profile().data) +with OctaSpaceClient(api_key=os.environ["OCTA_API_KEY"]) as client: + balance = client.accounts.balance().data["balance"] + print(f"Balance: {balance / 10**18:.6f} OCTA") + + for machine in client.services.mr.list().data: + print(machine["node_id"], machine["country"], machine["city"]) diff --git a/examples/failover.py b/examples/failover.py index d30a193..e7b11ac 100644 --- a/examples/failover.py +++ b/examples/failover.py @@ -1,8 +1,12 @@ +"""Send requests to the next API host when one cannot be reached.""" + +import os + from octaspace import OctaSpaceClient with OctaSpaceClient( - api_key="your-api-key", - base_urls=["https://api.octa.space", "https://api2.octa.space"], + api_key=os.environ["OCTA_API_KEY"], + base_urls=["https://api.octa.space", "https://api.octa.computer"], ) as client: + print(len(client.nodes.list().data), "nodes") print(client.transport_stats()) - print(client.nodes.list().data) diff --git a/examples/persistent_client.py b/examples/persistent_client.py index 0954cc1..58b1f61 100644 --- a/examples/persistent_client.py +++ b/examples/persistent_client.py @@ -1,5 +1,10 @@ +"""Reuse connections across requests with keep-alive.""" + +import os + from octaspace import OctaSpaceClient -with OctaSpaceClient(api_key="your-api-key", keep_alive=True, pool_size=10) as client: - print(client.transport_stats()) - print(client.sessions.list().data) +with OctaSpaceClient(api_key=os.environ["OCTA_API_KEY"], keep_alive=True, pool_size=10) as client: + for session in client.sessions.list().data: + info = client.services.session(session["uuid"]).info().data + print(session["uuid"], info.get("is_ready")) diff --git a/src/octaspace/__init__.py b/src/octaspace/__init__.py index 259fac5..9ac83a2 100644 --- a/src/octaspace/__init__.py +++ b/src/octaspace/__init__.py @@ -28,10 +28,19 @@ def configuration() -> ClientConfig: + """Return the global configuration.""" return _configuration def configure(**overrides: Any) -> ClientConfig: + """Update the global configuration and close the shared client. + + Args: + **overrides: Any ``ClientConfig`` field. + + Returns: + The new global configuration. + """ global _configuration, _shared_client _configuration = _configuration.with_overrides(**overrides) if _shared_client is not None: @@ -41,6 +50,7 @@ def configure(**overrides: Any) -> ClientConfig: def reset_configuration() -> ClientConfig: + """Close the shared client and restore the default configuration.""" global _configuration, _shared_client if _shared_client is not None: _shared_client.close() @@ -50,6 +60,12 @@ def reset_configuration() -> ClientConfig: def client(api_key: str | None = None, **overrides: Any) -> OctaSpaceClient: + """Return the shared client, or a new client when arguments are given. + + Without arguments, returns one client built from the global configuration + and reused on every call. With ``api_key`` or other options, returns a new + client based on the global configuration; close it yourself. + """ global _shared_client if api_key is None and not overrides: if _shared_client is None: @@ -60,6 +76,7 @@ def client(api_key: str | None = None, **overrides: Any) -> OctaSpaceClient: def shutdown_shared_client() -> None: + """Close the shared client; the next ``client()`` call creates a new one.""" global _shared_client if _shared_client is not None: _shared_client.close() diff --git a/src/octaspace/client.py b/src/octaspace/client.py index ce7f487..9e6e3fc 100644 --- a/src/octaspace/client.py +++ b/src/octaspace/client.py @@ -18,6 +18,23 @@ class OctaSpaceClient: + """Client of the OctaSpace API. + + Args: + api_key: The API token of your account (Account → Credentials → + API Token). Without it, only the calls that need no token work. + transport: A custom transport, for example + ``octaspace._transport.mock_transport.MockTransport`` in tests. + config: Base configuration; ``api_key`` and ``**overrides`` take + precedence over it. + **overrides: Any ``ClientConfig`` field, such as ``read_timeout``. + + Raises: + ConfigurationError: An option is unknown or invalid. + + Use the client as a context manager, or call ``close()`` when done. + """ + def __init__( self, api_key: str | None = None, @@ -38,15 +55,19 @@ def __init__( @property def config(self) -> ClientConfig: + """The configuration of this client.""" return self._config def close(self) -> None: + """Close the client's connections.""" self._transport.close() def shutdown(self) -> None: + """Close the client's connections; same as ``close()``.""" self.close() def transport_stats(self) -> dict[str, Any]: + """Return the connection mode, pool limits and API hosts in use.""" return self._transport.transport_stats() def __enter__(self) -> OctaSpaceClient: diff --git a/src/octaspace/config.py b/src/octaspace/config.py index 01896aa..6efde70 100644 --- a/src/octaspace/config.py +++ b/src/octaspace/config.py @@ -19,6 +19,16 @@ def _env_bool(value: str | None, default: bool) -> bool: @dataclass(slots=True) class ClientConfig: + """Settings of an ``OctaSpaceClient``. + + Every field can be passed to ``OctaSpaceClient`` as a keyword argument. + Timeouts and intervals are in seconds. See the client configuration guide + for the meaning and default of each field. + + Raises: + ValueError: A field has an invalid value. + """ + api_key: str | None = None base_url: str = "https://api.octa.space" base_urls: list[str] | None = None @@ -63,15 +73,18 @@ def __post_init__(self) -> None: @property def urls(self) -> list[str]: + """The API hosts in use: ``base_urls`` if set, otherwise ``[base_url]``.""" return list(self.base_urls) if self.base_urls is not None else [self.base_url] @property def resolved_user_agent(self) -> str: + """The ``User-Agent`` header the client sends.""" if self.user_agent: return self.user_agent return f"octaspace-python/{__version__}" def build_timeout(self) -> httpx.Timeout: + """Return the timeouts as an ``httpx.Timeout``.""" return httpx.Timeout( connect=self.open_timeout, read=self.read_timeout, @@ -80,6 +93,7 @@ def build_timeout(self) -> httpx.Timeout: ) def build_limits(self) -> httpx.Limits: + """Return the connection pool limits as an ``httpx.Limits``.""" keepalive_connections = self.pool_size if self.keep_alive else 0 keepalive_expiry = self.idle_timeout if self.keep_alive else None return httpx.Limits( @@ -89,6 +103,11 @@ def build_limits(self) -> httpx.Limits: ) def with_overrides(self, **overrides: Any) -> ClientConfig: + """Return a copy with the given fields replaced. + + Raises: + ValueError: A field is unknown or invalid. + """ try: return replace(self, **overrides) except TypeError as exc: @@ -96,6 +115,15 @@ def with_overrides(self, **overrides: Any) -> ClientConfig: @classmethod def from_env(cls, **overrides: Any) -> ClientConfig: + """Build a configuration from ``OCTA_*`` environment variables. + + Reads ``OCTA_API_KEY``, ``OCTA_BASE_URL``, ``OCTA_BASE_URLS`` + (comma-separated) and one variable per option, for example + ``OCTA_READ_TIMEOUT``. ``OCTA_LOGGER`` is the name of a logger. + + Args: + **overrides: Fields that take precedence over the environment. + """ defaults = cls() base_urls = os.getenv("OCTA_BASE_URLS") logger_name = os.getenv("OCTA_LOGGER") diff --git a/src/octaspace/errors.py b/src/octaspace/errors.py index a948c24..4391dc6 100644 --- a/src/octaspace/errors.py +++ b/src/octaspace/errors.py @@ -26,6 +26,14 @@ class APITimeoutError(NetworkError): class APIError(OctaSpaceError): + """Raised for an error response from the API. + + Attributes: + status_code: The HTTP status. + request_id: The ``X-Request-Id`` header of the response, or ``None``. + response: The full ``APIResponse``. + """ + def __init__(self, message: str | None = None, *, response: APIResponse) -> None: self.response = response self.status_code = response.status_code @@ -50,6 +58,12 @@ class ValidationError(APIError): class RateLimitError(APIError): + """Raised for HTTP 429 responses. + + Attributes: + retry_after: Seconds the API asks you to wait, or ``None``. + """ + def __init__(self, message: str | None = None, *, response: APIResponse) -> None: self.retry_after = response.retry_after super().__init__(message, response=response) @@ -94,6 +108,7 @@ def _message_from_response(response: APIResponse) -> str: def from_response(response: APIResponse) -> APIError: + """Return the error class instance that matches the response status.""" error_type = STATUS_ERRORS.get(response.status_code) if error_type is None: error_type = ServerError if response.is_server_error else APIError @@ -101,10 +116,12 @@ def from_response(response: APIResponse) -> APIError: def raise_for_response(response: APIResponse) -> None: + """Raise the error that matches the response status.""" raise from_response(response) def error_context(error: Exception) -> dict[str, Any]: + """Return the status, request ID and message of an error as a dict.""" if isinstance(error, APIError): return { "status_code": error.status_code, diff --git a/src/octaspace/hooks.py b/src/octaspace/hooks.py index 7606596..520d914 100644 --- a/src/octaspace/hooks.py +++ b/src/octaspace/hooks.py @@ -13,12 +13,21 @@ @dataclass(frozen=True, slots=True) class RequestContext: + """A request as passed to the ``on_request`` hook. + + Attributes: + method: The HTTP method in lower case. + path: The URL path. + params: The query parameters. + """ + method: str path: str params: dict[str, Any] @classmethod def from_httpx(cls, request: httpx.Request) -> RequestContext: + """Build a ``RequestContext`` from an ``httpx.Request``.""" params = dict(parse_qsl(request.url.query.decode("utf-8"), keep_blank_values=True)) return cls(method=request.method.lower(), path=request.url.path, params=params) @@ -28,6 +37,8 @@ def from_httpx(cls, request: httpx.Request) -> RequestContext: class HookDispatcher: + """Runs the ``on_request`` / ``on_response`` hooks and the request log.""" + def __init__( self, on_request: RequestHook | None = None, diff --git a/src/octaspace/models/account.py b/src/octaspace/models/account.py index 3514a77..7edd902 100644 --- a/src/octaspace/models/account.py +++ b/src/octaspace/models/account.py @@ -6,6 +6,8 @@ @dataclass(frozen=True, slots=True) class Account: + """An account profile, built from ``accounts.profile().data``.""" + account_uuid: str email: str avatar: str | None @@ -23,6 +25,8 @@ def from_dict(cls, data: dict[str, Any]) -> Account: @dataclass(frozen=True, slots=True) class Balance: + """An account balance, built from ``accounts.balance().data``.""" + amount: float currency: str diff --git a/src/octaspace/models/node.py b/src/octaspace/models/node.py index 7a93304..1f89dcf 100644 --- a/src/octaspace/models/node.py +++ b/src/octaspace/models/node.py @@ -6,6 +6,8 @@ @dataclass(frozen=True, slots=True) class Node: + """A node, built from a node of ``nodes.list()`` or ``nodes.find()``.""" + id: int ip: str state: str @@ -26,8 +28,10 @@ def from_dict(cls, data: dict[str, Any]) -> Node: @property def online(self) -> bool: + """Whether the node state is ``online``.""" return self.state == "online" @property def offline(self) -> bool: + """Whether the node is not online.""" return not self.online diff --git a/src/octaspace/models/session.py b/src/octaspace/models/session.py index eced2ed..85a9787 100644 --- a/src/octaspace/models/session.py +++ b/src/octaspace/models/session.py @@ -6,6 +6,8 @@ @dataclass(frozen=True, slots=True) class Session: + """A session, built from an item of ``sessions.list().data``.""" + uuid: str service: str app_name: str | None diff --git a/src/octaspace/resources/accounts.py b/src/octaspace/resources/accounts.py index 84bd222..141f18f 100644 --- a/src/octaspace/resources/accounts.py +++ b/src/octaspace/resources/accounts.py @@ -5,11 +5,27 @@ class AccountsResource(Resource): + """Your OctaSpace account: profile, balance and deposit wallet.""" + def profile(self) -> APIResponse: + """Return the account profile. + + ``GET /accounts``. The data includes ``account_uuid``, ``email``, ``uid``, + ``wallet`` and ``balance`` (in Wei). + """ return self.get("/accounts") def balance(self) -> APIResponse: + """Return the account balance. + + ``GET /accounts/balance``. ``data["balance"]`` is an integer in Wei; + 1 OCTA is 10**18 Wei. + """ return self.get("/accounts/balance") def generate_wallet(self) -> APIResponse: + """Generate the deposit wallet of the account. + + ``POST /accounts``. ``data["address"]`` is the wallet address. + """ return self.post("/accounts") diff --git a/src/octaspace/resources/apps.py b/src/octaspace/resources/apps.py index 7f11e77..075170b 100644 --- a/src/octaspace/resources/apps.py +++ b/src/octaspace/resources/apps.py @@ -7,5 +7,14 @@ class AppsResource(Resource): + """Applications that can be started on rented machines.""" + def list(self, **params: Any) -> APIResponse: + """List the applications. + + ``GET /apps``. Works without an API token. + + Args: + **params: Query parameters sent as they are. + """ return self.get("/apps", params=params or None) diff --git a/src/octaspace/resources/base.py b/src/octaspace/resources/base.py index 46d667c..5d56e70 100644 --- a/src/octaspace/resources/base.py +++ b/src/octaspace/resources/base.py @@ -8,6 +8,8 @@ class Resource: + """Base class of the resources; sends requests through the client's transport.""" + def __init__(self, transport: BaseTransport) -> None: self._transport = transport diff --git a/src/octaspace/resources/idle_jobs.py b/src/octaspace/resources/idle_jobs.py index ad56ccc..225d4f9 100644 --- a/src/octaspace/resources/idle_jobs.py +++ b/src/octaspace/resources/idle_jobs.py @@ -5,8 +5,29 @@ class IdleJobsResource(Resource): + """Jobs that your nodes run while nobody rents them.""" + def find(self, *, node_id: int | str, job_id: int | str) -> APIResponse: + """Return an idle job. + + ``GET /idle_jobs/{node_id}/{job_id}``. + + Args: + node_id: The node that runs the job. + job_id: The job. + """ return self.get(f"/idle_jobs/{self.encode(node_id)}/{self.encode(job_id)}") def logs(self, *, node_id: int | str, job_id: int | str) -> APIResponse: + """Return the logs of an idle job. + + ``GET /idle_jobs/{node_id}/{job_id}/logs``. + + Args: + node_id: The node that runs the job. + job_id: The job. + + Raises: + NotFoundError: The job does not exist. + """ return self.get(f"/idle_jobs/{self.encode(node_id)}/{self.encode(job_id)}/logs") diff --git a/src/octaspace/resources/network.py b/src/octaspace/resources/network.py index bd72b07..35ce3eb 100644 --- a/src/octaspace/resources/network.py +++ b/src/octaspace/resources/network.py @@ -5,5 +5,11 @@ class NetworkResource(Resource): + """Statistics of the OctaSpace network.""" + def info(self) -> APIResponse: + """Return network, blockchain and GPU market statistics. + + ``GET /network``. Works without an API token. + """ return self.get("/network") diff --git a/src/octaspace/resources/nodes.py b/src/octaspace/resources/nodes.py index 6f19ce6..ac41344 100644 --- a/src/octaspace/resources/nodes.py +++ b/src/octaspace/resources/nodes.py @@ -7,20 +7,60 @@ class NodesResource(Resource): + """The nodes you host.""" + def list(self, **params: Any) -> APIResponse: + """List your nodes. + + ``GET /nodes``. + + Args: + **params: Query parameters sent as they are. + """ return self.get("/nodes", params=params or None) def find(self, node_id: int | str) -> APIResponse: + """Return one of your nodes. + + ``GET /nodes/{node_id}``. + + Raises: + NotFoundError: The node does not exist or is not yours. + """ return self.get(f"/nodes/{self.encode(node_id)}") def download_ident(self, node_id: int | str) -> APIResponse: + """Download the identity file of a node. + + ``GET /nodes/{node_id}/ident``. Read the file from ``.body``. + """ return self.get(f"/nodes/{self.encode(node_id)}/ident") def download_logs(self, node_id: int | str) -> APIResponse: + """Download the logs of a node. + + ``GET /nodes/{node_id}/logs``. Read the logs from ``.body``. + """ return self.get(f"/nodes/{self.encode(node_id)}/logs") def update_prices(self, node_id: int | str, **prices: Any) -> APIResponse: + """Set the prices of a node. + + ``PATCH /nodes/{node_id}/prices``. Never retried. + + Args: + node_id: Your node. + **prices: One or more of ``base``, ``storage`` and ``traffic``, each + an integer from 0 to 10000. + + Raises: + APIError: Status 400 for an unknown field or a value out of range. + """ return self.patch(f"/nodes/{self.encode(node_id)}/prices", body=prices) def reboot(self, node_id: int | str) -> APIResponse: + """Reboot a node. + + ``GET /nodes/{node_id}/reboot``. Retried like any GET request. + """ return self.get(f"/nodes/{self.encode(node_id)}/reboot") diff --git a/src/octaspace/resources/services/__init__.py b/src/octaspace/resources/services/__init__.py index 5d8cfcd..5d55982 100644 --- a/src/octaspace/resources/services/__init__.py +++ b/src/octaspace/resources/services/__init__.py @@ -9,6 +9,14 @@ class ServicesResource(Resource): + """Services you rent: machines, VPN gateways, render nodes and their sessions. + + Attributes: + mr: Machine rental. + vpn: VPN gateways. + render: Render nodes. + """ + def __init__(self, transport: BaseTransport) -> None: super().__init__(transport) self.mr = MachineRentalResource(transport) @@ -16,6 +24,12 @@ def __init__(self, transport: BaseTransport) -> None: self.render = RenderResource(transport) def session(self, uuid: str) -> SessionProxy: + """Return the operations of one session. + + Args: + uuid: The session UUID, as returned by ``create()`` or + ``sessions.list()``. + """ return SessionProxy(self._transport, uuid) diff --git a/src/octaspace/resources/services/machine_rental.py b/src/octaspace/resources/services/machine_rental.py index 26c0a63..e493729 100644 --- a/src/octaspace/resources/services/machine_rental.py +++ b/src/octaspace/resources/services/machine_rental.py @@ -23,7 +23,13 @@ class MachineRentalCreatePayload(_MachineRentalRequiredPayload, total=False): class MachineRentalResource(Resource): + """Machines for rent, to run your own container.""" + def list(self, **params: Any) -> APIResponse: + """List the machines available for rent. + + ``GET /services/mr``. Works without an API token. + """ return self.get("/services/mr", params=params or None) def create( @@ -39,6 +45,28 @@ def create( start_command: str = "", entrypoint: str = "", ) -> APIResponse: + """Start a machine rental session. + + ``POST /services/mr``. Never retried. + + Args: + node_id: The machine to rent. + disk_size: Disk to allocate, in GB. + image: The container image, for example ``"ubuntu:24.04"``. + app: UUID of an application from ``apps.list()``. + envs: Environment variables of the container. + ports: TCP/UDP ports to expose. + http_ports: HTTP ports to expose as HTTPS links. + start_command: Shell commands to run after the container starts. + entrypoint: The container entrypoint. + + Returns: + The response; ``data["uuid"]`` is the session UUID. + + Raises: + APIError: Status 400 when the session cannot be started, for + example for an insufficient balance. + """ payload: MachineRentalCreatePayload = { "node_id": node_id, "disk_size": disk_size, @@ -50,7 +78,6 @@ def create( "start_command": start_command, "entrypoint": entrypoint, } - # The OctaSpace API expects MR creation as a single-item array with id=0. - # This mirrors the Ruby gem and the live payload preset used by the JS SDK. + # The endpoint takes a list of sessions to start; this starts one. item = {"id": 0, **payload} return self.post("/services/mr", body=[item]) diff --git a/src/octaspace/resources/services/render.py b/src/octaspace/resources/services/render.py index 04f2025..79580de 100644 --- a/src/octaspace/resources/services/render.py +++ b/src/octaspace/resources/services/render.py @@ -7,8 +7,25 @@ class RenderResource(Resource): + """Render nodes for rent.""" + def list(self, **params: Any) -> APIResponse: + """List the render nodes available for rent. + + ``GET /services/render``. + """ return self.get("/services/render", params=params or None) def create(self, **attrs: Any) -> APIResponse: + """Start a render session. + + ``POST /services/render``. Never retried. + + Args: + **attrs: The request body: ``node_id`` and ``disk_size`` (GB), + optionally ``envs`` and ``multi-gpu-worker``. + + Returns: + The response; ``data["uuid"]`` is the session UUID. + """ return self.post("/services/render", body=attrs) diff --git a/src/octaspace/resources/services/session_proxy.py b/src/octaspace/resources/services/session_proxy.py index 17e7a9e..8edb7ca 100644 --- a/src/octaspace/resources/services/session_proxy.py +++ b/src/octaspace/resources/services/session_proxy.py @@ -8,17 +8,40 @@ class SessionProxy(Resource): + """Operations of one session, returned by ``client.services.session(uuid)``.""" + def __init__(self, transport: BaseTransport, uuid: str) -> None: super().__init__(transport) self._uuid = self.encode(uuid) def info(self) -> APIResponse: + """Return the session details. + + ``GET /services/{uuid}/info``. The fields depend on the kind of + session, for example ``is_ready``, ``urls``, ``ssh_direct`` and + ``vpn_config``. + + Raises: + NotFoundError: The session does not exist. + """ return self.get(f"/services/{self._uuid}/info") def logs(self) -> APIResponse: + """Return the session logs. + + ``GET /services/{uuid}/logs``. ``data["container"]`` is the container + output, ``data["system"]`` the platform's events. + """ return self.get(f"/services/{self._uuid}/logs") def stop(self, **params: Any) -> APIResponse: + """Stop the session. + + ``POST /services/{uuid}/stop``. Never retried. + + Args: + **params: Optional ``score``, your integer rating of the session. + """ return self.post( f"/services/{self._uuid}/stop", body=params or None, diff --git a/src/octaspace/resources/services/vpn.py b/src/octaspace/resources/services/vpn.py index 1e81ebe..e65ad49 100644 --- a/src/octaspace/resources/services/vpn.py +++ b/src/octaspace/resources/services/vpn.py @@ -7,8 +7,25 @@ class VpnResource(Resource): + """VPN gateways for rent.""" + def list(self, **params: Any) -> APIResponse: + """List the VPN gateways available for rent. + + ``GET /services/vpn``. + """ return self.get("/services/vpn", params=params or None) def create(self, **attrs: Any) -> APIResponse: + """Start a VPN session. + + ``POST /services/vpn``. Never retried. + + Args: + **attrs: The request body: ``node_id`` (required), ``subkind`` (the + VPN protocol, ``"wg"`` by default) and ``protocol``. + + Returns: + The response; ``data["uuid"]`` is the session UUID. + """ return self.post("/services/vpn", body=attrs) diff --git a/src/octaspace/resources/sessions.py b/src/octaspace/resources/sessions.py index 37cb01c..a51038d 100644 --- a/src/octaspace/resources/sessions.py +++ b/src/octaspace/resources/sessions.py @@ -7,5 +7,15 @@ class SessionsResource(Resource): + """Your sessions of every kind.""" + def list(self, **params: Any) -> APIResponse: + """List your sessions. + + ``GET /sessions``. + + Args: + **params: Query parameters; ``recent=True`` returns recent sessions + instead of the active ones. + """ return self.get("/sessions", params=params or None) diff --git a/src/octaspace/response.py b/src/octaspace/response.py index 6354c74..beed87f 100644 --- a/src/octaspace/response.py +++ b/src/octaspace/response.py @@ -8,6 +8,16 @@ @dataclass(frozen=True, slots=True) class APIResponse: + """A response of the API. + + Attributes: + status_code: The HTTP status. + headers: The response headers. + body: The raw body. + data: The parsed JSON body, or ``None`` if the body is empty or not JSON. + raw_response: The underlying ``httpx.Response``. + """ + status_code: int headers: httpx.Headers body: bytes @@ -16,6 +26,7 @@ class APIResponse: @classmethod def from_httpx(cls, response: httpx.Response) -> APIResponse: + """Build an ``APIResponse`` from an ``httpx.Response``.""" body = response.content data: Any = None content_type = response.headers.get("content-type", "") @@ -34,11 +45,13 @@ def from_httpx(cls, response: httpx.Response) -> APIResponse: @property def request_id(self) -> str | None: + """The ``X-Request-Id`` header, or ``None``.""" value = self.headers.get("x-request-id") return str(value) if value is not None else None @property def retry_after(self) -> int | None: + """The ``Retry-After`` header in seconds, or ``None``.""" value = self.headers.get("retry-after") if value is None: return None @@ -49,21 +62,26 @@ def retry_after(self) -> int | None: @property def is_success(self) -> bool: + """Whether the status is 2xx.""" return 200 <= self.status_code <= 299 @property def is_client_error(self) -> bool: + """Whether the status is 4xx.""" return 400 <= self.status_code <= 499 @property def is_server_error(self) -> bool: + """Whether the status is 5xx.""" return 500 <= self.status_code <= 599 @property def is_error(self) -> bool: + """Whether the status is 4xx or 5xx.""" return self.is_client_error or self.is_server_error def raise_for_status(self) -> None: + """Raise the matching ``APIError`` if the status is 4xx or 5xx.""" if self.is_error: from .errors import raise_for_response From c0f8a9bfe0830fcb5264e6e6b82c626f13de0c90 Mon Sep 17 00:00:00 2001 From: secretpray Date: Sun, 8 Nov 2026 13:43:43 +0200 Subject: [PATCH 4/5] ci: test on Python 3.11-3.15 and update actions --- .github/workflows/ci.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 35a1038..a583e5f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,17 +11,19 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12", "3.13"] + python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - - uses: astral-sh/setup-uv@v5 + - uses: astral-sh/setup-uv@v10 - name: Install run: uv sync --group dev - name: Lint run: uv run ruff check . + - name: Format + run: uv run ruff format --check . - name: Typecheck run: uv run mypy src - name: Test From 92b62b1a02bc3b86e6e5e40f1fb78f47121171cc Mon Sep 17 00:00:00 2001 From: secretpray Date: Sun, 8 Nov 2026 19:15:40 +0200 Subject: [PATCH 5/5] ci: pin setup-uv to v10.3.3 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a583e5f..3e25a27 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - - uses: astral-sh/setup-uv@v10 + - uses: astral-sh/setup-uv@v10.3.3 - name: Install run: uv sync --group dev - name: Lint