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/.github/workflows/ci.yml b/.github/workflows/ci.yml index 35a1038..3e25a27 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.3.3 - 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 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/.python-version b/.python-version index c8cfe39..2c07333 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.10 +3.11 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/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/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/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/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/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..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 = [ @@ -35,29 +36,19 @@ 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", + "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 = "playground" }, { include-group = "test" }, { include-group = "lint" }, { include-group = "typecheck" }, @@ -66,11 +57,22 @@ dev = [ [tool.hatch.version] path = "src/octaspace/__about__.py" +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/README.md", + "/CHANGELOG.md", + "/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"] @@ -79,10 +81,9 @@ 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] -python_version = "3.10" +python_version = "3.11" strict = true packages = ["octaspace"] 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/__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/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 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..a21e6a8 100644 --- a/uv.lock +++ b/uv.lock @@ -1,58 +1,95 @@ 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" }, +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'" }, -] -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" } -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" }, + { 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" }, +] + +[[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/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" }, -] - -[[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" }, + { 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]] @@ -66,148 +103,128 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.5" -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" } -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" }, -] - -[[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" }, +version = "7.17.2" +source = { registry = "https://pypi.org/simple" } +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/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]] @@ -249,269 +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" }, -] - -[[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" }, + { 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" -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" } -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" }, -] - -[[package]] -name = "markupsafe" -version = "3.0.3" +version = "0.16.0" 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" }, +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/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" } -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" }, +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/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]] @@ -523,25 +476,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,213 +499,70 @@ 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" }, + { 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.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" } -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" } +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/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" }, + { 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]] @@ -770,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" }, ] @@ -779,15 +579,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" @@ -802,127 +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 = "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" -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" } -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" }, +version = "0.17.4" +source = { registry = "https://pypi.org/simple" } +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/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" } -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" } +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/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" }, + { 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" }, ]