From 9d7c216c6ebd0bffee9342ba67e5976c85d3a13e Mon Sep 17 00:00:00 2001 From: Shudipto Trafder Date: Thu, 22 Oct 2026 14:43:46 +0600 Subject: [PATCH 1/3] Refactor CLI commands and update WebSocket configurations - Changed CLI commands from `10xgraph api` and `10xgraph play` to `10xgraph dev` for local development. - Updated documentation and references to reflect the new command structure. - Enhanced WebSocket configuration in `10xgraph.json` to include an `enabled` flag for better control over WebSocket routes. - Implemented logic to conditionally include WebSocket routes based on the configuration. - Added tests to ensure proper functionality of the new command structure and WebSocket behavior. - Introduced a slim image script to remove CLI-only packages from production images. --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/workflows/ci.yaml | 2 +- CHANGELOG.md | 58 ++++++++ CLAUDE.md | 25 ++-- README.md | 31 ++-- examples/security/README.md | 2 +- pyproject.toml | 17 ++- tenxgraph_api/_slim_image.py | 93 ++++++++++++ tenxgraph_api/cli/commands/api.py | 2 +- tenxgraph_api/cli/commands/demo.py | 7 +- tenxgraph_api/cli/commands/init.py | 136 ++++++++++++++++-- tenxgraph_api/cli/config_editor/schema.py | 22 ++- tenxgraph_api/cli/core/animations.py | 4 - tenxgraph_api/cli/main.py | 129 ++--------------- tenxgraph_api/cli/templates/defaults.py | 19 ++- tenxgraph_api/cli/templates/dev/.env.example | 2 +- .../cli/templates/dev/graph/agent.py | 4 +- tenxgraph_api/cli/templates/prod/.env.example | 2 +- .../prod/evals/user_simulator_eval.py | 3 +- .../cli/templates/prod/graph/agent.py | 9 +- .../cli/templates/prod/pyproject.toml | 5 +- .../cli/templates/prod/tests/conftest.py | 2 +- .../templates/prod/tests/test_graph_nodes.py | 51 ++----- .../cli/templates/skills/10xgraph/SKILL.md | 4 +- .../10xgraph/references/architecture.md | 2 +- .../10xgraph/references/cli-commands.md | 22 ++- .../10xgraph/references/production-runtime.md | 5 +- .../skills/10xgraph/references/realtime.md | 2 +- .../skills/copilot/10xgraph.instructions.md | 2 +- .../src/app/core/config/graph_config.py | 11 +- .../app/core/middleware/rate_limit/factory.py | 7 +- tenxgraph_api/src/app/main.py | 11 +- tenxgraph_api/src/app/routers/setup_router.py | 46 +++++- .../src/app/utils/callable_helper.py | 2 +- tests/cli/test_cli_demo.py | 6 +- tests/cli/test_cli_main.py | 25 ++-- tests/cli/test_init_non_interactive.py | 102 +++++++++++++ tests/unit_tests/test_cli_animations.py | 18 +-- tests/unit_tests/test_cli_output.py | 16 +-- tests/unit_tests/test_env_file_load_order.py | 2 +- tests/unit_tests/test_production_surface.py | 2 + tests/unit_tests/test_rate_limit.py | 14 ++ tests/unit_tests/test_slim_image.py | 67 +++++++++ tests/unit_tests/test_websocket_toggle.py | 85 +++++++++++ 44 files changed, 796 insertions(+), 282 deletions(-) create mode 100644 tenxgraph_api/_slim_image.py create mode 100644 tests/unit_tests/test_slim_image.py create mode 100644 tests/unit_tests/test_websocket_toggle.py diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6958391..9cc7c3a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -36,7 +36,7 @@ body: description: A minimal sequence someone else can run. Include the exact commands. placeholder: | 1. 10xgraph init - 2. 10xgraph api + 2. 10xgraph dev 3. curl -X POST localhost:8000/v1/graph/invoke ... validations: required: true diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 432a6d4..7dc7c3a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -48,7 +48,7 @@ jobs: fail-fast: false matrix: # Must cover every version advertised in pyproject.toml classifiers. - python-version: ["3.12", "3.13"] + python-version: ["3.12", "3.13", "3.14", "3.15"] services: redis: diff --git a/CHANGELOG.md b/CHANGELOG.md index c7b5a83..c5afe84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,64 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Breaking + +- **`10xgraph api` and `10xgraph play` are removed; `10xgraph dev` is the only server command.** + All three ran the same local Uvicorn dev server. Migration: `10xgraph api` -> + `10xgraph dev --no-open`, `10xgraph play` -> `10xgraph dev`. Options are unchanged + (`--config`, `--host`, `--port`, `--reload/--no-reload`). `10xgraph demo --style` no longer + accepts the `play` / `api` aliases. Production is unaffected: the image from `10xgraph build` + runs Gunicorn directly. + +### Added + +- **Python 3.14 and 3.15 are officially supported.** The test suite runs on 3.12, 3.13, 3.14 + and 3.15 in CI, and the package classifiers list all four. +- **`websocket.enabled` in `10xgraph.json`** turns the WebSocket routes on or off. `false` leaves + out `/v1/graph/ws` and `/v1/graph/live`, so the server needs no WebSocket library. `true` + without one refuses to boot with the install hint. Unset keeps them mounted, as before, but + now logs a warning when no library is installed (previously a non-Google project silently + answered 404 on both routes, because only `google-genai` brought in `websockets`). +- **`10xgraph-api[websocket]` extra** (`websockets`) for those routes. +- **`10xgraph init` asks whether to enable WebSocket streaming and realtime audio** + (`--websocket/--no-websocket`, default off). Yes adds the `websocket` extra to a production + `pyproject.toml` and writes `"websocket": {"enabled": true}`; no writes `false`. +- **`10xgraph init` asks for the AI provider** (Google Gemini, OpenAI or Anthropic Claude; + `--provider` for non-interactive runs, default `google`). The scaffold's model, provider, + `.env.example` API key, test conftest and eval simulator model follow the answer. +- **`10xgraph-api[images]` extra.** Pillow is optional in core `10xgraph`; this extra installs it + for image resize, EXIF orientation and format conversion. + +### Changed + +- Replaced `asyncio.iscoroutinefunction`, which is removed in Python 3.16, with + `inspect.iscoroutinefunction`. No behaviour change. +- **Production scaffolds list only the dependencies the answers need.** `pyproject.toml` now + depends on `10xgraph[]` and `10xgraph-api[jwt,redis]` as chosen (no extras when + neither is picked), and drops the hardcoded `google-genai`. The generated Dockerfile installs + this list, so JWT and Redis work in the image without a separate `requirements.txt`. +- **The generated Dockerfile removes pip and the CLI's own libraries from the image.** typer, + rich, questionary and what only they pull in (pygments, markdown-it-py, mdurl, prompt-toolkit, + wcwidth, shellingham) are uninstalled in the builder stage by `tenxgraph_api/_slim_image.py`, + unless another installed package still needs them. With pip, about 38 MB smaller. +- Dropped unused dev dependencies (`mkdocs-gen-files`, `mkdocstrings`, `mypy-extensions`, + `lib`, `markdown-it-py`). + +### Fixed + +- **`10xgraph init` generated a `pyproject.toml` claiming Python 3.10.** The template said + `requires-python = ">=3.10"`, but `10xgraph` needs 3.12, so installing a generated project + on 3.10 or 3.11 failed. It now says `>=3.12`. +- **Redis rate limiting from a URL crashed at startup** with `NameError: Redis`: the class was + imported only for type checking but used at runtime to bind the client. +- **A production scaffold with the Redis rate limiter did not start:** `10xgraph.json` had no + Redis URL. It now reads `"redis": {"url": "${REDIS_URL}"}` from `.env`. +- **A production scaffold did not start at all:** `10xgraph.json` points at + `graph.agent:container`, which the template never defined. `graph/agent.py` now exports it. +- **The production scaffold's `tests/test_graph_nodes.py` failed on collection** (it imported + `graph.nodes` and `graph.utils`, which the template does not create). It now tests the agent + in `graph/agent.py`. + ## [0.8.0] - 2026-10-16 Security and scalability pass over the API, following an external review. Every item below diff --git a/CLAUDE.md b/CLAUDE.md index 416aa62..0ecffbc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,7 +32,7 @@ output schema id, `AF-*` CLI error codes. It turns a 10xGraph `CompiledGraph` into a production FastAPI service, plus a Typer CLI to scaffold, run, build, test, and evaluate that service. You write a graph, point `10xgraph.json` -at it, and `10xgraph api` serves it over REST + WebSocket with auth, rate limiting, media +at it, and `10xgraph dev` serves it over REST + WebSocket with auth, rate limiting, media handling, checkpointer/thread management, and a memory store API. ## Package layout @@ -51,17 +51,15 @@ Public exports from the package root (`from tenxgraph_api import ...`): `BaseAut | Command | Purpose | Notable options | |---|---|---| -| `10xgraph api` | Start the API server | `--config/-c` (default `10xgraph.json`), `--host/-H`, `--port/-p` (8000), `--reload/--no-reload`, `-v/-q` | -| `10xgraph play` | Start the server and open the hosted playground | same as `api` | -| `10xgraph dev` | Goal-oriented local dev server: same runner as `api`, opens the playground by default. `api`/`play` stay for compatibility | same as `api`, plus `--open/--no-open` | -| `10xgraph init` | Interactively scaffold a project (guided prompts pick dev vs production, auth, rate limit) | `--path/-p`, `--force/-f`, `--name`, `--template` (quick-start\|production), `--auth` (none\|jwt\|custom), `--rate-limit` (none\|memory\|redis), `--yes/-y`, `--non-interactive`, `--dry-run`. There is **no `--prod` flag** | -| `10xgraph build` | Generate a `Dockerfile` (and optionally `docker-compose.yml` / `k8s.yaml`) | `--output/-o`, `--force/-f`, `--python-version` (3.13), `--port`, `--docker-compose/--no-docker-compose`, `--k8s/--no-k8s`, `--service-name` | +| `10xgraph dev` | The only server command: local Uvicorn dev server (reload on) via `commands/api.py::APICommand`, opens the hosted playground when ready. `api` and `play` were removed (use `dev --no-open` / `dev`); production runs Gunicorn in the `10xgraph build` image | `--config/-c` (default `10xgraph.json`), `--host/-H`, `--port/-p` (8000), `--reload/--no-reload`, `--open/--no-open`, `-v/-q` | +| `10xgraph init` | Interactively scaffold a project (guided prompts pick dev vs production, auth, rate limit) | `--path/-p`, `--force/-f`, `--name`, `--template` (quick-start\|production), `--auth` (none\|jwt\|custom), `--rate-limit` (none\|memory\|redis), `--provider` (google\|openai\|anthropic), `--websocket/--no-websocket` (default off; writes `websocket.enabled`). Prod `pyproject.toml` depends on `10xgraph[]` and `10xgraph-api[jwt,redis,websocket]` as chosen, `--yes/-y`, `--non-interactive`, `--dry-run`. There is **no `--prod` flag** | +| `10xgraph build` | Generate a `Dockerfile` (and optionally `docker-compose.yml` / `k8s.yaml`). The builder stage strips CLI assets, runs `tenxgraph_api/_slim_image.py` (uninstalls typer/rich/questionary and what only they need, unless another package needs them) and removes pip, so the image carries no CLI code paths | `--output/-o`, `--force/-f`, `--python-version` (3.13), `--port`, `--docker-compose/--no-docker-compose`, `--k8s/--no-k8s`, `--service-name` | | `10xgraph eval` | Run agent evaluations; discovers `*_eval.py`/`eval_*.py`, runs cases (optionally `--parallel`), writes HTML+JSON to `eval_reports/` | `--output/-o`, `--no-report`, `--threshold/-t`, `--open`, `--parallel/-p`, `--max-concurrency/-c` | | `10xgraph test` | Run project tests via pytest (args after `--` forwarded verbatim) | `--coverage/-C`, `--html`, `-k`, path arg | | `10xgraph skills` | Install the bundled 10xGraph skill (Agent Skills spec) for Codex/Claude/GitHub, or validate skills | `--agent/-a`, `--path/-p`, `--force/-f`, `--all`, `--list/-l`, `--validate PATH` | | `10xgraph version` | Show CLI + core framework version | both resolve from installed distribution metadata | | `10xgraph audit` | Read-only audit of the interpreter, installed CLI/core packages, evaluation-API compatibility, `10xgraph.json`, and the default port. Exits `1` on failure, `0` on warnings only, so it works as a CI gate | `-v/--verbose`, `-q/--quiet` | -| `10xgraph demo` | Preview the animation/timeline/progress themes with no side effects (`Diagnostics` help panel) | `--style` (all\|typing\|network\|init\|build\|eval; `play`/`api` alias to typing/network) | +| `10xgraph demo` | Preview the animation/timeline/progress themes with no side effects (`Diagnostics` help panel) | `--style` (all\|typing\|network\|init\|build\|eval) | | `10xgraph config` | Browser editor for `10xgraph.json` (`Manage` panel). Loopback-only stdlib HTTP server in `cli/config_editor/` (`schema.py` lists every key, `validation.py` reuses the `graph_config` parsers, `store.py` does conflict-checked atomic writes with a `.bak`). The page is a Preact + Tailwind app whose source lives in `config-editor-ui/`; `npm run build` there writes the committed `static/app.js` and `static/app.css`, so rebuild after editing `config-editor-ui/src` | `--config/-c`, `--port/-p` (0 = any free port), `--open/--no-open` | Defaults (from `cli/constants.py`): `DEFAULT_HOST="127.0.0.1"`, `DEFAULT_PORT=8000`, @@ -97,7 +95,7 @@ Parsed by `tenxgraph_api/src/app/core/config/graph_config.py`. Supported keys: | `redis` | Redis URL string | | `rate_limit` | Object (see below) | | `ag_ui` | `{"enabled": bool}`, default off. Mounts `POST /v1/ag-ui` (AG-UI protocol, for CopilotKit and other AG-UI clients). Needs the `ag-ui` extra | -| `websocket` | `max_connections` (1000), `max_connections_per_user` (10), `realtime_models` (models a `/v1/graph/live` client may pick), `realtime_client_overrides` (init options a client may set; default `voice`, `modalities`, `vad`; add `system_prompt` only for trusted callers) | +| `websocket` | `enabled` (`false` leaves out `/v1/graph/ws` and `/v1/graph/live`; `true` without `websockets`/`wsproto` refuses to boot; unset keeps them and warns if no library; `routers/setup_router.py::_graph_router`), `max_connections` (1000), `max_connections_per_user` (10), `realtime_models` (models a `/v1/graph/live` client may pick), `realtime_client_overrides` (init options a client may set; default `voice`, `modalities`, `vad`; add `system_prompt` only for trusted callers) | | `runs` | `RunsConfig`: `thread_ids` (`client` default, `server` = only existing ids may be named, enforced by the ownership/rbac backends), `config_keys` / `state_keys` (allow-lists for request config and `initial_state`/PUT state; 422 on other keys; `thread_id`, `run_id` always allowed), `max_concurrent_per_user` (HTTP invoke/stream/AG-UI runs in flight, default 10, 429 over it; `routers/graph/run_limits.py`), `timeout` (seconds; invoke 504, streams end with an error chunk) | | `token_revocation` | `"redis"` / `"memory"` / `"module:attr"` -> `core/auth/revocation.py::TokenRevocationBackend`, checked by `RequirePermission` (401) and before every WS run; backend errors fail closed (503) | | `graph_introspection` | `"open"` or `"scoped"` (default scoped in production): `GET /v1/graph`, `/v1/graph/tools`, `/v1/graph:StateSchema` need an explicit `graph:read` scope (`RequirePermission(..., introspection=True)`) | @@ -199,7 +197,7 @@ Production error responses hide only 5xx details; raise `request_config.ClientIn ## Optional extras (`pyproject.toml`) `sentry`, `firebase`, `snowflakekit`, `redis`, `jwt`, `ag-ui` (AG-UI endpoint), `media` (document text extraction via -`textxtract`), `gcloud` (Cloud Logging), `otel` (includes FastAPI instrumentation + OTLP exporter). +`textxtract`), `images` (Pillow via `10xgraph[images]`, for image resize / EXIF / format conversion), `websocket` (`websockets`, needed to serve `/v1/graph/ws` and `/v1/graph/live`), `gcloud` (Cloud Logging), `otel` (includes FastAPI instrumentation + OTLP exporter). ## Development workflow @@ -207,8 +205,8 @@ Production error responses hide only 5xx details; raise `request_config.ClientIn # from this folder (10xgraph-api/); a .venv is present .venv/bin/python -m pytest # tests in tests/ 10xgraph init # scaffold (interactive) -10xgraph api --reload # dev server on 127.0.0.1:8000 -10xgraph play # server + hosted playground +10xgraph dev # dev server on 127.0.0.1:8000 + hosted playground +10xgraph dev --no-open # dev server only 10xgraph build --docker-compose # Dockerfile + compose ruff check . && ruff format . ``` @@ -232,6 +230,5 @@ ruff check . && ruff format . files from git history if that surface is actually built. - The local git remote is still `Iamsdt/pyagenity-api.git`; GitHub redirects it to `10xGraph/10xgraph-api`, so pushes work. Repoint it when convenient. -- The workspace-root `CLAUDE.md` lists only `init/api/play/build` and an older `10xgraph.json` - shape; the real CLI has `eval/test/skills/version` too and the config supports `rate_limit`, - `thread_name_generator`, and `authorization`. +- The docs site (`agentflow-docs/`) still uses `10xgraph api` / `10xgraph play` in many pages; + both commands were removed in favour of `10xgraph dev` (`dev --no-open` for server only). diff --git a/README.md b/README.md index ffa9c0b..04e7941 100644 --- a/README.md +++ b/README.md @@ -89,11 +89,17 @@ Optional extras; install only what you configure: pip install "10xgraph-api[redis]" # Redis rate-limit / cache backend pip install "10xgraph-api[jwt]" # JWT authentication pip install "10xgraph-api[media]" # Document text extraction (multimodal) +pip install "10xgraph-api[images]" # Image resize / EXIF / format conversion (Pillow) +pip install "10xgraph-api[ag-ui]" # AG-UI endpoint +pip install "10xgraph-api[websocket]" # WebSocket streaming + realtime audio pip install "10xgraph-api[otel]" # OpenTelemetry tracing pip install "10xgraph-api[snowflakekit]" # Snowflake ID generation +pip install "10xgraph-api[sentry]" # Sentry error reporting +pip install "10xgraph-api[firebase]" # Firebase Admin SDK +pip install "10xgraph-api[gcloud]" # Google Cloud Logging ``` -Requires Python 3.12 or newer. Installs the core `10xgraph` framework as a dependency. +Requires Python 3.12 or newer. Tested on 3.12, 3.13, 3.14 and 3.15. Installs the core `10xgraph` framework as a dependency. --- @@ -113,9 +119,6 @@ Requires Python 3.12 or newer. Installs the core `10xgraph` framework as a depen 10xgraph build --docker-compose ``` -`10xgraph api` (server only) and `10xgraph play` (server + playground) remain -available; `dev` is the goal-oriented wrapper around them. - --- ## CLI commands @@ -133,9 +136,17 @@ Initialize a new project with configuration and a sample graph. 10xgraph init --path ./my-app --name MyAgent --template quick-start \ --non-interactive # reproducible CI/agent workflow 10xgraph init --path ./my-app --template production --auth jwt \ - --rate-limit redis --yes --dry-run + --rate-limit redis --provider openai --yes --dry-run ``` +`init` asks which AI provider to use (`--provider google|openai|anthropic`, default `google`) +and writes the matching model and API key variable. It also asks whether to enable WebSocket +streaming and realtime audio (`--websocket`, default off); off leaves `/v1/graph/ws` and +`/v1/graph/live` unmounted and installs no WebSocket library. A production project's `pyproject.toml` +lists only what your answers need, for example `10xgraph[openai]` and +`10xgraph-api[jwt,redis]`, and `10xgraph build` installs exactly that list, so no separate +`requirements.txt` is needed. + ### `10xgraph dev` Start the development API server and open the hosted playground when it is ready. @@ -147,16 +158,18 @@ Start the development API server and open the hosted playground when it is ready 10xgraph dev --no-open --no-reload # API only, without auto-reload ``` -`10xgraph api` and `10xgraph play` remain available as compatibility commands. +`dev` is the only server command: the old `api` and `play` commands were removed +(use `10xgraph dev --no-open` and `10xgraph dev`). It runs Uvicorn with reload for local +work; production runs Gunicorn in the image from `10xgraph build`. ### Adaptive and structured output ```bash -10xgraph play # branded intro, then output in your scrollback +10xgraph dev # branded intro, then output in your scrollback 10xgraph demo # preview every animation theme safely 10xgraph demo --style build # one theme: typing, network, init, build, or eval -10xgraph --fullscreen play # opt-in pinned header/footer surface -10xgraph --no-animation play # accessible/static workflow +10xgraph --fullscreen dev # opt-in pinned header/footer surface +10xgraph --no-animation dev # accessible/static workflow 10xgraph --format plain --no-color audit 10xgraph --format jsonl eval --parallel 10xgraph --quiet build diff --git a/examples/security/README.md b/examples/security/README.md index 2affb77..abaa4ee 100644 --- a/examples/security/README.md +++ b/examples/security/README.md @@ -42,7 +42,7 @@ export JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30 **Step 3:** Start the application ```bash -10xgraph api +10xgraph dev --no-open ``` ### 2. Custom Authentication diff --git a/pyproject.toml b/pyproject.toml index dd8ac93..6f9438a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,8 @@ classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", ] @@ -100,6 +102,11 @@ jwt = [ media = [ "textxtract[pdf,docx,html,xml,md]>=0.2.0", ] +# Pillow for image resize / EXIF / format conversion in core's media processor. +# Without it, uploaded images pass through unchanged. +images = [ + "10xgraph[images]>=0.10.0,<2.0", +] gcloud = [ "google-cloud-logging", ] @@ -112,6 +119,11 @@ otel = [ ag-ui = [ "ag-ui-protocol>=1.0,<2", ] +# WebSocket streaming (/v1/graph/ws) and realtime audio (/v1/graph/live). Uvicorn needs a +# WebSocket library to serve them; set "websocket": {"enabled": false} to run without one. +websocket = [ + "websockets>=13.0,<17", +] [project.scripts] 10xgraph = "tenxgraph_api.cli.main:main" @@ -371,12 +383,7 @@ dev = [ "pre-commit>=3.8.0", "mypy>=1.11.0", "ruff==0.5.2", - "mkdocs-gen-files==0.5.0", - "mkdocstrings==0.25.2", - "mypy-extensions==1.0.0", "httpx==0.27.0", - "lib==4.0.0", - "markdown-it-py==3.0.0", "requests==2.32.3", "redis>=5.0.7", "PyJWT", diff --git a/tenxgraph_api/_slim_image.py b/tenxgraph_api/_slim_image.py new file mode 100644 index 0000000..8d9d2e7 --- /dev/null +++ b/tenxgraph_api/_slim_image.py @@ -0,0 +1,93 @@ +"""Remove CLI-only packages from a production image. Run by the generated Dockerfile. + +The server never imports the CLI libraries (typer, rich, questionary and what they pull in), +so the image does not need them. A package is removed only when nothing else installed still +requires it, so a project dependency that uses rich (for example) keeps it and everything it +needs. Standard library only: it runs in the Docker builder stage. + + python -I -m tenxgraph_api._slim_image +""" + +from __future__ import annotations + +import re +import subprocess +import sys +import sysconfig +from importlib import metadata +from pathlib import Path + + +CLI_PACKAGE = "10xgraph-api" +# The CLI's own dependencies and everything only they pull in. +CLI_ONLY = frozenset( + { + "typer", + "shellingham", + "rich", + "pygments", + "markdown-it-py", + "mdurl", + "questionary", + "prompt-toolkit", + "wcwidth", + } +) +CONSOLE_SCRIPTS = ("10xgraph", "agentflow") + + +def _norm(name: str) -> str: + return re.sub(r"[-_.]+", "-", name).lower() + + +def _requirements(dist: metadata.Distribution) -> set[str]: + """Names this distribution needs at install time (requirements behind an extra skipped).""" + names = set() + for req in dist.requires or (): + _, _, marker = req.partition(";") + if "extra" in marker: + continue + names.add(_norm(re.split(r"[\s;<>=!~\[(]", req.strip(), maxsplit=1)[0])) + return names + + +def removable(dists: dict[str, metadata.Distribution]) -> list[str]: + """CLI-only packages that no other installed package (directly or through another) needs.""" + needed: set[str] = set() + for name, dist in dists.items(): + if name not in CLI_ONLY and name != CLI_PACKAGE: + needed |= _requirements(dist) + + kept: set[str] = set() + while True: + newly_kept = {name for name in CLI_ONLY & needed if name in dists} - kept + if not newly_kept: + break + kept |= newly_kept + for name in newly_kept: + needed |= _requirements(dists[name]) + + return sorted(name for name in CLI_ONLY if name in dists and name not in kept) + + +def main() -> int: + dists = {_norm(d.metadata["Name"]): d for d in metadata.distributions()} + remove = removable(dists) + if not remove: + sys.stdout.write("slim image: no CLI-only packages to remove\n") + return 0 + + # Fixed package names from CLI_ONLY, never user input. + subprocess.check_call([sys.executable, "-m", "pip", "uninstall", "-y", "-q", *remove]) # noqa: S603 + sys.stdout.write(f"slim image: removed {', '.join(remove)}\n") + + if "typer" in remove: + # The 10xgraph command cannot run without typer; drop its launchers too. + scripts = Path(sysconfig.get_path("scripts")) + for name in CONSOLE_SCRIPTS: + (scripts / name).unlink(missing_ok=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tenxgraph_api/cli/commands/api.py b/tenxgraph_api/cli/commands/api.py index 532042b..1ff9685 100644 --- a/tenxgraph_api/cli/commands/api.py +++ b/tenxgraph_api/cli/commands/api.py @@ -72,7 +72,7 @@ def execute( """ try: self.output.command_header( - "play" if open_playground else "api", + "dev", "Starting development server via Uvicorn. Not for production use.", hint="Ctrl+C to stop the development server", ) diff --git a/tenxgraph_api/cli/commands/demo.py b/tenxgraph_api/cli/commands/demo.py index 0a15148..7d278c7 100644 --- a/tenxgraph_api/cli/commands/demo.py +++ b/tenxgraph_api/cli/commands/demo.py @@ -13,7 +13,6 @@ class DemoCommand(BaseCommand): """Preview command-specific animations without external side effects.""" _STYLES = ("typing", "network", "init", "build", "eval") - _ALIASES = {"play": "typing", "api": "network"} _SUBTITLES = { "typing": "Full-screen 10xGraph identity and workspace transition", @@ -24,14 +23,14 @@ class DemoCommand(BaseCommand): } _COMMAND_NAMES = { "typing": "typing", - "network": "api", + "network": "dev", "init": "init", "build": "build", "eval": "eval", } def execute(self, style: str = "all", **kwargs: Any) -> int: - normalized = self._ALIASES.get(style.strip().lower(), style.strip().lower()) + normalized = style.strip().lower() if normalized != "all" and normalized not in self._STYLES: return self.handle_error( ValidationError( @@ -57,7 +56,7 @@ def execute(self, style: str = "all", **kwargs: Any) -> int: "Fallback": "automatic for CI, pipes, and JSON", }, next_steps=[ - "Run `10xgraph play` to see the network theme in a real workflow.", + "Run `10xgraph dev` to see the network theme in a real workflow.", "Use `10xgraph --no-animation COMMAND` for static accessible output.", "Use `10xgraph --fullscreen COMMAND` to hold a dedicated screen.", ], diff --git a/tenxgraph_api/cli/commands/init.py b/tenxgraph_api/cli/commands/init.py index e47ac4d..4dca689 100644 --- a/tenxgraph_api/cli/commands/init.py +++ b/tenxgraph_api/cli/commands/init.py @@ -19,6 +19,33 @@ # Directories inside prod/ that are only included based on user choices _AUTH_DIR = "auth" +# AI provider choices: the core extra that installs the SDK, the model the scaffold +# starts with, the model the eval user simulator uses, and the API key variable. +_PROVIDERS: dict[str, dict[str, str]] = { + "google": { + "label": "Google Gemini", + "extra": "google-genai", + "model": "gemini-2.5-flash", + "sim_model": "gemini/gemini-2.5-flash", + "api_key_env": "GOOGLE_API_KEY", + }, + "openai": { + "label": "OpenAI", + "extra": "openai", + "model": "gpt-4o-mini", + "sim_model": "openai/gpt-4o-mini", + "api_key_env": "OPENAI_API_KEY", + }, + "anthropic": { + "label": "Anthropic Claude", + "extra": "anthropic", + "model": "claude-sonnet-5-5", + "sim_model": "anthropic/claude-sonnet-5-5", + "api_key_env": "ANTHROPIC_API_KEY", + }, +} +_DEFAULT_PROVIDER = "google" + def _slugify(name: str) -> str: """Convert 'WeatherBot' → 'weather-bot'.""" @@ -44,10 +71,38 @@ def _strip_env_blocks(content: str, *, keep_redis: bool, keep_jwt: bool) -> str: return content +def _template_vars(context: dict[str, Any]) -> dict[str, Any]: + """Derive the template variables that depend on the user's answers. + + The production pyproject.toml lists only what the answers need: the provider SDK + through the core extra, PyJWT for JWT auth, redis for the Redis rate limiter, and + websockets for WebSocket streaming / realtime. The generated Dockerfile installs + exactly this list. + """ + provider = _PROVIDERS[context.get("provider", _DEFAULT_PROVIDER)] + api_extras = [] + if context.get("auth") == "jwt": + api_extras.append("jwt") + if context.get("rate_limit") == "redis": + api_extras.append("redis") + if context.get("websocket"): + api_extras.append("websocket") + return { + "llm_provider": context.get("provider", _DEFAULT_PROVIDER), + "llm_model": provider["model"], + "llm_sim_model": provider["sim_model"], + "llm_api_key_env": provider["api_key_env"], + "core_requirement": f"10xgraph[{provider['extra']}]", + "api_requirement": ( + f"10xgraph-api[{','.join(api_extras)}]" if api_extras else "10xgraph-api" + ), + } + + class InitCommand(BaseCommand): """Command to initialize a new agent project interactively.""" - def execute( + def execute( # noqa: PLR0913 self, path: str = ".", force: bool = False, @@ -55,6 +110,8 @@ def execute( template: str | None = None, auth: str | None = None, rate_limit: str | None = None, + provider: str | None = None, + websocket: bool = False, yes: bool = False, non_interactive: bool = False, dry_run: bool = False, @@ -73,6 +130,8 @@ def execute( template=template, auth=auth, rate_limit=rate_limit, + provider=provider, + websocket=websocket, ) else: context = self._prompt_user() @@ -141,6 +200,15 @@ def execute( step.detail(str(config_path)) agent_name = context["agent_name"] + provider = _PROVIDERS[context.get("provider", _DEFAULT_PROVIDER)] + # Production lists these in pyproject.toml; quick-start has no pyproject, so it + # needs the install step. + install_step = ( + [] + if is_prod + else [f'pip install "{_template_vars(context)["core_requirement"]}"'] + + (['pip install "10xgraph-api[websocket]"'] if context.get("websocket") else []) + ) self.output.completion_screen( "Project ready", f'"{agent_name}" was created successfully', @@ -148,11 +216,14 @@ def execute( "Location": base_path.resolve(), "Template": "production" if is_prod else "quick-start", "Auth": context["auth"], + "AI provider": provider["label"], + "WebSocket": "on" if context.get("websocket") else "off", }, next_steps=[ f"cd {base_path}", - "Copy .env.example to .env and add your model API key.", - "10xgraph play", + *install_step, + f"Copy .env.example to .env and set {provider['api_key_env']}.", + "10xgraph dev", ], ) @@ -171,6 +242,8 @@ def _non_interactive_context( template: str | None, auth: str | None, rate_limit: str | None, + provider: str | None = None, + websocket: bool = False, ) -> dict[str, Any]: """Resolve a complete, reproducible scaffold recipe without prompting.""" resolved_template = (template or "quick-start").strip().lower().replace("_", "-") @@ -204,6 +277,13 @@ def _non_interactive_context( field="rate_limit", ) + resolved_provider = (provider or _DEFAULT_PROVIDER).strip().lower() + if resolved_provider not in _PROVIDERS: + raise ValidationError( + f"Invalid AI provider '{resolved_provider}'. Choose {', '.join(_PROVIDERS)}.", + field="provider", + ) + if resolved_template == "quick-start" and ( resolved_auth != "none" or resolved_rate_limit != "none" ): @@ -216,6 +296,8 @@ def _non_interactive_context( "agent_name": resolved_name, "agent_name_slug": slug, "setup_type": "production" if resolved_template == "production" else "quick_start", + "provider": resolved_provider, + "websocket": websocket, "auth": resolved_auth, "rate_limit": resolved_rate_limit, "rl_requests": 100, @@ -228,7 +310,7 @@ def _non_interactive_context( # Prompts # ------------------------------------------------------------------ - def _prompt_user(self) -> dict | None: # noqa: PLR0911 + def _prompt_user(self) -> dict | None: # noqa: PLR0911, PLR0912 prompts = self.output.prompts() prompts.require_interactive( field="template", @@ -261,11 +343,33 @@ def _prompt_user(self) -> dict | None: # noqa: PLR0911 if template is None: return None + provider = prompts.select( + "Which AI provider?", + [ + Choice("google", "Google Gemini", "Installs google-genai · GOOGLE_API_KEY"), + Choice("openai", "OpenAI", "Installs openai · OPENAI_API_KEY"), + Choice("anthropic", "Anthropic Claude", "Installs anthropic · ANTHROPIC_API_KEY"), + ], + default=_DEFAULT_PROVIDER, + ) + if provider is None: + return None + + websocket = prompts.confirm( + "Enable WebSocket streaming and realtime audio? " + "(/v1/graph/ws, /v1/graph/live; installs websockets)", + default=False, + ) + if websocket is None: + return None + is_prod = template == "production" context: dict[str, Any] = { "agent_name": agent_name, "agent_name_slug": _slugify(agent_name), "setup_type": template, + "provider": provider, + "websocket": websocket, "auth": "none", "rate_limit": "none", } @@ -359,6 +463,8 @@ def _print_summary(self, context: dict) -> None: ("Agent name", context["agent_name"]), ("Package name", context["agent_name_slug"]), ("Setup", setup_label), + ("AI provider", _PROVIDERS[context.get("provider", _DEFAULT_PROVIDER)]["label"]), + ("WebSocket", "On" if context.get("websocket") else "Off"), ] if is_prod: auth = context["auth"] @@ -392,8 +498,15 @@ def _print_next_steps(self, context: dict, is_prod: bool) -> None: if is_prod: steps.append(("pre-commit install", "Set up Git hooks (ruff, bandit, etc.)")) - steps.append(("pip install google-genai", "Add the AI provider library")) - steps.append(("cp .env.example .env", "Copy env template, then fill in your API keys")) + provider = _PROVIDERS[context.get("provider", _DEFAULT_PROVIDER)] + if not is_prod: + # Production lists the provider SDK in pyproject.toml already. + steps.append( + (f'pip install "10xgraph[{provider["extra"]}]"', "Add the AI provider library") + ) + steps.append( + ("cp .env.example .env", f"Copy env template, then set {provider['api_key_env']}") + ) if context["auth"] == "jwt": steps.append(("# Set JWT_SECRET_KEY in .env", "Required for JWT auth to work")) @@ -401,7 +514,7 @@ def _print_next_steps(self, context: dict, is_prod: bool) -> None: if context["rate_limit"] == "redis": steps.append(("# Set REDIS_URL in .env", "Required for Redis rate limiting")) - steps.append(("10xgraph play", "Launch your agent")) + steps.append(("10xgraph dev", "Launch your agent")) self.output.print_table( ["#", "Command", "Purpose"], @@ -424,6 +537,9 @@ def _build_config(self, context: dict, is_prod: bool) -> dict: "thread_name_generator": None, # AG-UI endpoint for clients such as CopilotKit. Off until turned on here. "ag_ui": {"enabled": False}, + # WebSocket streaming (/v1/graph/ws) and realtime audio (/v1/graph/live). + # Off: neither route is mounted and no WebSocket library is needed. + "websocket": {"enabled": bool(context.get("websocket"))}, } if not is_prod: @@ -457,6 +573,10 @@ def _build_config(self, context: dict, is_prod: bool) -> dict: "trusted_proxy_headers": context.get("rl_trusted_proxy", False), "exclude_paths": ["/ping", "/docs", "/redoc", "/openapi.json"], } + if rate_limit == "redis": + # Resolved from REDIS_URL in .env; the redis backend refuses to start + # without a URL or a Redis client bound in InjectQ. + config["rate_limit"]["redis"] = {"url": "${REDIS_URL}"} return config @@ -486,7 +606,7 @@ def _render(self, src: Path, context: dict, is_prod: bool) -> str: ) with contextlib.suppress(Exception): - content = Template(content).safe_substitute(context) + content = Template(content).safe_substitute({**context, **_template_vars(context)}) return content diff --git a/tenxgraph_api/cli/config_editor/schema.py b/tenxgraph_api/cli/config_editor/schema.py index fc1553e..f06eddb 100644 --- a/tenxgraph_api/cli/config_editor/schema.py +++ b/tenxgraph_api/cli/config_editor/schema.py @@ -310,12 +310,28 @@ { "id": "websocket", "title": "WebSocket", - "description": "Caps on concurrent realtime and streaming connections, per process.", + "description": ( + "WebSocket streaming and realtime audio, and per-process caps on concurrent " + "connections." + ), "key": "websocket", "optional": True, - "off_summary": "Concurrent WebSocket connections are not capped.", - "default": {"max_connections": 1000, "max_connections_per_user": 10}, + "off_summary": ( + "/v1/graph/ws and /v1/graph/live stay mounted with the default caps " + "(1000 per process, 10 per user)." + ), + "default": {"enabled": True, "max_connections": 1000, "max_connections_per_user": 10}, "fields": [ + { + "path": ["websocket", "enabled"], + "default": True, + "label": "Enabled", + "type": "bool", + "help": ( + "Mounts /v1/graph/ws and /v1/graph/live. Needs the websockets package " + '(pip install "10xgraph-api[websocket]"). Off: neither route exists.' + ), + }, { "path": ["websocket", "max_connections"], "label": "Max connections", diff --git a/tenxgraph_api/cli/core/animations.py b/tenxgraph_api/cli/core/animations.py index ae3fc0d..cc577ed 100644 --- a/tenxgraph_api/cli/core/animations.py +++ b/tenxgraph_api/cli/core/animations.py @@ -82,9 +82,7 @@ # Each command reveals its own pipeline during the intro, so the animation # doubles as a preview of what the command is about to do. _SIGNATURES: dict[str, tuple[str, ...]] = { - "api": ("config", "runtime", "server", "ready"), "dev": ("config", "runtime", "server", "playground"), - "play": ("config", "runtime", "server", "playground"), "init": ("template", "graph", "config", "project"), "build": ("source", "deps", "image", "ship"), "test": ("collect", "run", "assert", "report"), @@ -95,9 +93,7 @@ _DEFAULT_SIGNATURE = ("boot", "load", "ready") _TAGLINES: dict[str, str] = { - "api": "Serving your agent over REST and WebSocket.", "dev": "Local agent runtime with live reload.", - "play": "Local agent runtime, wired to the playground.", "init": "Scaffolding a production-shaped agent project.", "build": "Packaging your agent for deployment.", "test": "Exercising your agent's test suite.", diff --git a/tenxgraph_api/cli/main.py b/tenxgraph_api/cli/main.py index a5d7b6d..5c4a054 100644 --- a/tenxgraph_api/cli/main.py +++ b/tenxgraph_api/cli/main.py @@ -275,120 +275,6 @@ def handle_exception(e: Exception) -> int: return 1 -@app.command() -def api( - config: str = typer.Option( - DEFAULT_CONFIG_FILE, - "--config", - "-c", - help="Path to config file", - ), - host: str = typer.Option( - DEFAULT_HOST, - "--host", - "-H", - help="Host to run the API on (default: 127.0.0.1, localhost only; " - "use 0.0.0.0 to bind all interfaces)", - ), - port: int = typer.Option( - DEFAULT_PORT, - "--port", - "-p", - help="Port to run the API on", - ), - reload: bool = typer.Option( - True, - "--reload/--no-reload", - help="Enable auto-reload for development", - ), - verbose: bool = typer.Option( - False, - "--verbose", - "-v", - help="Enable verbose logging", - ), - quiet: bool = typer.Option( - False, - "--quiet", - "-q", - help="Suppress all output except errors", - ), -) -> None: - """Start the 10xGraph API server.""" - # Setup logging - _configure_command(verbose=verbose, quiet=quiet) - - try: - command = APICommand(output) - exit_code = command.execute( - config=config, - host=host, - port=port, - reload=reload, - ) - sys.exit(exit_code) - except Exception as e: - sys.exit(handle_exception(e)) - - -@app.command() -def play( - config: str = typer.Option( - DEFAULT_CONFIG_FILE, - "--config", - "-c", - help="Path to config file", - ), - host: str = typer.Option( - DEFAULT_HOST, - "--host", - "-H", - help=( - "Host to run the API on for the local playground session " - "(use 127.0.0.1 for localhost only)" - ), - ), - port: int = typer.Option( - DEFAULT_PORT, - "--port", - "-p", - help="Port to run the API on", - ), - reload: bool = typer.Option( - True, - "--reload/--no-reload", - help="Enable auto-reload for development", - ), - verbose: bool = typer.Option( - False, - "--verbose", - "-v", - help="Enable verbose logging", - ), - quiet: bool = typer.Option( - False, - "--quiet", - "-q", - help="Suppress all output except errors", - ), -) -> None: - """Start the API server and open the hosted playground.""" - _configure_command(verbose=verbose, quiet=quiet) - - try: - command = APICommand(output) - exit_code = command.execute( - config=config, - host=host, - port=port, - reload=reload, - open_playground=True, - ) - sys.exit(exit_code) - except Exception as e: - sys.exit(handle_exception(e)) - - @app.command() def dev( config: str = typer.Option( @@ -401,7 +287,8 @@ def dev( DEFAULT_HOST, "--host", "-H", - help="Host interface for the local development server.", + help="Host for the local development server (default: 127.0.0.1, localhost only; " + "use 0.0.0.0 to bind all interfaces).", ), port: int = typer.Option( DEFAULT_PORT, @@ -593,6 +480,16 @@ def init( # noqa: PLR0913 "--rate-limit", help="Rate limiting: none, memory, or redis.", ), + provider: str | None = typer.Option( + None, + "--provider", + help="AI provider: google, openai, or anthropic (default google).", + ), + websocket: bool = typer.Option( + False, + "--websocket/--no-websocket", + help="Enable WebSocket streaming and realtime audio (adds the websocket extra).", + ), yes: bool = typer.Option( False, "--yes", @@ -638,6 +535,8 @@ def init( # noqa: PLR0913 template=template, auth=auth, rate_limit=rate_limit, + provider=provider, + websocket=websocket, yes=yes, non_interactive=non_interactive, dry_run=dry_run, diff --git a/tenxgraph_api/cli/templates/defaults.py b/tenxgraph_api/cli/templates/defaults.py index 94d27f8..cc164a6 100644 --- a/tenxgraph_api/cli/templates/defaults.py +++ b/tenxgraph_api/cli/templates/defaults.py @@ -392,7 +392,7 @@ def should_use_tools(state: AgentState) -> str: description = "10xGraph API application" readme = "README.md" license = {text = "MIT"} -requires-python = ">=3.10" +requires-python = ">=3.12" authors = [ {name = "Your Name", email = "you@example.com"}, ] @@ -517,6 +517,15 @@ def should_use_tools(state: AgentState) -> str: f"[shutil.rmtree(cli / d, ignore_errors=True) for d in {CLI_ONLY_ASSET_DIRS!r} if cli]" ) +# Removes the CLI-only libraries (typer, rich, questionary, ...) unless something else needs +# them; see ``tenxgraph_api/_slim_image.py``. Skipped when the installed 10xgraph-api is older +# than that module, so a pinned older version still builds. +_SLIM_IMAGE = ( + "import importlib.util, runpy; " + "importlib.util.find_spec('tenxgraph_api._slim_image') " + "and runpy.run_module('tenxgraph_api._slim_image', run_name='__main__')" +) + def generate_dockerfile_content( python_version: str, @@ -603,6 +612,14 @@ def generate_dockerfile_content( "# its dependencies stay in the image.", f'RUN python -I -c "{_STRIP_CLI_ASSETS}"', "", + "# Remove the CLI's own libraries (typer, rich, questionary and what only they pull", + "# in). A package that one of your dependencies still needs is kept.", + f'RUN python -I -c "{_SLIM_IMAGE}"', + "", + "# The server never runs pip, so drop it from the virtualenv the runtime copies", + "# (about 13 MB). The base image keeps its own pip for debugging.", + "RUN pip uninstall -y pip", + "", "# ---- Stage 2: runtime image ----", f"FROM python:{python_version}-slim", "", diff --git a/tenxgraph_api/cli/templates/dev/.env.example b/tenxgraph_api/cli/templates/dev/.env.example index 3991070..80978f1 100644 --- a/tenxgraph_api/cli/templates/dev/.env.example +++ b/tenxgraph_api/cli/templates/dev/.env.example @@ -1 +1 @@ -GOOGLE_API_KEY="" \ No newline at end of file +$llm_api_key_env="" \ No newline at end of file diff --git a/tenxgraph_api/cli/templates/dev/graph/agent.py b/tenxgraph_api/cli/templates/dev/graph/agent.py index 7677c51..46250a4 100644 --- a/tenxgraph_api/cli/templates/dev/graph/agent.py +++ b/tenxgraph_api/cli/templates/dev/graph/agent.py @@ -37,8 +37,8 @@ def get_weather( react_agent = ReactAgent( - model="google/gemini-2.5-flash", - provider="google", + model="$llm_model", + provider="$llm_provider", system_prompt=[ { "role": "system", diff --git a/tenxgraph_api/cli/templates/prod/.env.example b/tenxgraph_api/cli/templates/prod/.env.example index 4123ec9..7e6ba45 100644 --- a/tenxgraph_api/cli/templates/prod/.env.example +++ b/tenxgraph_api/cli/templates/prod/.env.example @@ -74,4 +74,4 @@ JWT_ALGORITHM="HS256" ################################# ###### API Keys ################# ################################# -GOOGLE_API_KEY="" +$llm_api_key_env="" diff --git a/tenxgraph_api/cli/templates/prod/evals/user_simulator_eval.py b/tenxgraph_api/cli/templates/prod/evals/user_simulator_eval.py index 130bcea..9487f60 100644 --- a/tenxgraph_api/cli/templates/prod/evals/user_simulator_eval.py +++ b/tenxgraph_api/cli/templates/prod/evals/user_simulator_eval.py @@ -27,9 +27,10 @@ # --------------------------------------------------------------------------- # Optional: override the simulator model and settings for this file. # If omitted, the CLI uses UserSimulatorConfig defaults (gemini-2.5-flash). +# The provider prefix picks the SDK ("gemini/", "openai/", "anthropic/"). # --------------------------------------------------------------------------- SIMULATOR_CONFIG = UserSimulatorConfig( - model="gemini/gemini-2.5-flash", + model="$llm_sim_model", max_invocations=8, temperature=0.7, ) diff --git a/tenxgraph_api/cli/templates/prod/graph/agent.py b/tenxgraph_api/cli/templates/prod/graph/agent.py index 776f8ba..505108a 100644 --- a/tenxgraph_api/cli/templates/prod/graph/agent.py +++ b/tenxgraph_api/cli/templates/prod/graph/agent.py @@ -4,6 +4,7 @@ from tenxgraph.prebuilt.agent import ReactAgent from tenxgraph.storage.checkpointer import InMemoryCheckpointer from dotenv import load_dotenv +from injectq import InjectQ from graph.state import WeatherState from graph.tools.weather_tool import get_weather @@ -14,6 +15,9 @@ checkpointer = InMemoryCheckpointer() +# Dependency injection container; 10xgraph.json points at it ("injectq": "graph.agent:container"). +container = InjectQ.get_instance() + context_manager = MessageContextManager( max_messages=20, # last 20 user messages will be kept in context remove_tool_msgs=True, @@ -21,8 +25,8 @@ react_agent = ReactAgent( state=WeatherState(), - model="google/gemini-2.5-flash", - provider="google", + model="$llm_model", + provider="$llm_provider", system_prompt=[ { "role": "system", @@ -42,6 +46,7 @@ trim_context=True, tools=[get_weather], context_manager=context_manager, + container=container, ) app = react_agent.compile( diff --git a/tenxgraph_api/cli/templates/prod/pyproject.toml b/tenxgraph_api/cli/templates/prod/pyproject.toml index c4ddc58..64ee52e 100644 --- a/tenxgraph_api/cli/templates/prod/pyproject.toml +++ b/tenxgraph_api/cli/templates/prod/pyproject.toml @@ -11,9 +11,8 @@ license = {text = "MIT"} requires-python = ">=3.12" authors = [] dependencies = [ - "10xgraph", - "10xgraph-api", - "google-genai", + "$core_requirement", + "$api_requirement", ] [project.optional-dependencies] diff --git a/tenxgraph_api/cli/templates/prod/tests/conftest.py b/tenxgraph_api/cli/templates/prod/tests/conftest.py index 1d27c3f..72ba706 100644 --- a/tenxgraph_api/cli/templates/prod/tests/conftest.py +++ b/tenxgraph_api/cli/templates/prod/tests/conftest.py @@ -3,4 +3,4 @@ import os -os.environ.setdefault("GEMINI_API_KEY", "test-key") +os.environ.setdefault("$llm_api_key_env", "test-key") diff --git a/tenxgraph_api/cli/templates/prod/tests/test_graph_nodes.py b/tenxgraph_api/cli/templates/prod/tests/test_graph_nodes.py index 31a3136..5de906c 100644 --- a/tenxgraph_api/cli/templates/prod/tests/test_graph_nodes.py +++ b/tenxgraph_api/cli/templates/prod/tests/test_graph_nodes.py @@ -1,19 +1,13 @@ from __future__ import annotations -from types import SimpleNamespace - -from tenxgraph.core.state.message import Message -from tenxgraph.utils.constants import END - -from graph.nodes.main_node import agent -from graph.utils.tool_decision import should_use_tools +from graph.agent import app, container, react_agent def test_main_agent_is_configured_for_weather_assistant() -> None: - assert agent.model == "gemini-3-flash-preview" - assert agent.provider == "google" + agent = react_agent._agent + assert agent.model == "$llm_model" + assert agent.provider == "$llm_provider" assert agent.trim_context is True - assert agent.tool_node == "TOOL" system_prompt_text = " ".join( block["content"] for block in agent.system_prompt if "content" in block @@ -21,37 +15,10 @@ def test_main_agent_is_configured_for_weather_assistant() -> None: assert "helpful assistant" in system_prompt_text.lower() -def test_should_end_when_no_context_exists() -> None: - assert should_use_tools(SimpleNamespace(context=[])) == END - - -def test_should_route_assistant_tool_calls_to_tool_node() -> None: - state = SimpleNamespace( - context=[ - Message( - role="assistant", - content=[], - tools_calls=[ - { - "id": "call_1", - "name": "get_weather", - "args": {"location": "London"}, - } - ], - ) - ] - ) - - assert should_use_tools(state) == "TOOL" - - -def test_should_route_tool_result_back_to_main_agent() -> None: - state = SimpleNamespace(context=[Message(role="tool", content=[])]) - - assert should_use_tools(state) == "MAIN" - +def test_graph_compiles() -> None: + assert app is not None -def test_should_end_after_regular_assistant_response() -> None: - state = SimpleNamespace(context=[Message(role="assistant", content=[])]) - assert should_use_tools(state) == END +def test_container_is_exported_for_10xgraph_json() -> None: + # 10xgraph.json loads "graph.agent:container"; the server refuses to start without it. + assert container is not None diff --git a/tenxgraph_api/cli/templates/skills/10xgraph/SKILL.md b/tenxgraph_api/cli/templates/skills/10xgraph/SKILL.md index a18fe08..88f01af 100644 --- a/tenxgraph_api/cli/templates/skills/10xgraph/SKILL.md +++ b/tenxgraph_api/cli/templates/skills/10xgraph/SKILL.md @@ -4,7 +4,7 @@ description: >- Expert guidance for building, debugging, and extending applications with 10xGraph (10xgraph). Use when code imports from tenxgraph or the legacy agentflow name (e.g. `from tenxgraph import`, `StateGraph`, `Agent`, `ToolNode`, `AgentState`); when the user references `10xgraph.json` - or CLI commands (`10xgraph init`, `10xgraph api`, `10xgraph play`, `10xgraph build`, + or CLI commands (`10xgraph init`, `10xgraph dev`, `10xgraph build`, `10xgraph skills`); or when the user is building graph-based multi-agent workflows, tools, memory, checkpointing, or streaming with this framework. Skip generic Python or multi-agent questions that do not reference 10xGraph, and other frameworks (LangGraph, CrewAI, AutoGen) @@ -25,7 +25,7 @@ Treat https://10xgraph.com/ as the first source of truth for public package name - PyPI API/CLI SDK: `10xgraph-api` (`pip install 10xgraph-api`), source at https://github.com/10xGraph/10xgraph-api/tree/main/tenxgraph_api - npm TypeScript SDK: `10xgraph-client` (`npm install 10xgraph-client`), source at https://github.com/10xGraph/10xgraph-client/tree/master/src - Main docs: https://10xgraph.com/ - - Playground/UI: `10xgraph play` command after installed cli + - Playground/UI: `10xgraph dev` command after installed cli 2. Read the matching reference file before changing behavior. Paths are relative to this skill's directory: diff --git a/tenxgraph_api/cli/templates/skills/10xgraph/references/architecture.md b/tenxgraph_api/cli/templates/skills/10xgraph/references/architecture.md index 8992ef9..f09e7e6 100644 --- a/tenxgraph_api/cli/templates/skills/10xgraph/references/architecture.md +++ b/tenxgraph_api/cli/templates/skills/10xgraph/references/architecture.md @@ -33,7 +33,7 @@ Use this when deciding where a change belongs or explaining how 10xGraph package ### `10xgraph-api` — API and CLI -Owns `10xgraph api`, `10xgraph play`, `10xgraph init`, `10xgraph build`, REST routers, auth/middleware, config loading, and graph service execution. +Owns `10xgraph dev`, `10xgraph init`, `10xgraph build`, REST routers, auth/middleware, config loading, and graph service execution. ### `10xgraph-client` — TypeScript HTTP client diff --git a/tenxgraph_api/cli/templates/skills/10xgraph/references/cli-commands.md b/tenxgraph_api/cli/templates/skills/10xgraph/references/cli-commands.md index c307436..8b3a5f0 100644 --- a/tenxgraph_api/cli/templates/skills/10xgraph/references/cli-commands.md +++ b/tenxgraph_api/cli/templates/skills/10xgraph/references/cli-commands.md @@ -21,21 +21,17 @@ agentflow = tenxgraph_api.cli.main:legacy_main # deprecated alias, removed in `10xgraph init` -- Scaffolds `10xgraph.json`, `graph/__init__.py`, and `graph/react.py`, and suggests `10xgraph skills` as a next step. -- Options include `--path/-p`, `--force/-f`, `--prod`, `--verbose/-v`, and `--quiet/-q`. -- `--prod` also adds production project files such as `pyproject.toml` and `.pre-commit-config.yaml`. +- Scaffolds `10xgraph.json`, `graph/agent.py` and `.env.example`, and suggests `10xgraph skills` as a next step. +- Asks for the template (quick-start or production), the AI provider (Google, OpenAI, Anthropic) and whether to enable WebSocket streaming / realtime audio; production also asks for auth and rate limiting. +- Non-interactive options: `--path/-p`, `--force/-f`, `--name`, `--template` (quick-start|production), `--provider` (google|openai|anthropic), `--websocket/--no-websocket` (default off), `--auth` (none|jwt|custom), `--rate-limit` (none|memory|redis), `--yes/-y`, `--non-interactive`, `--dry-run`. +- Production adds `pyproject.toml` (dependencies follow the answers, e.g. `10xgraph[openai]`, `10xgraph-api[jwt,redis,websocket]`), `.pre-commit-config.yaml`, `tests/` and `evals/`. -`10xgraph api` +`10xgraph dev` -- Starts the FastAPI server for a compiled graph. -- Options include `--config/-c`, `--host/-H`, `--port/-p`, `--reload/--no-reload`, `--verbose/-v`, and `--quiet/-q`. -- Defaults are config `10xgraph.json`, host `127.0.0.1`, port `8000`, reload enabled. - -`10xgraph play` - -- Starts the API server and opens/prints the hosted playground URL. -- Accepts the same server options as `api`. -- Uses host/port to build the playground backend URL. +- Starts the local development server (Uvicorn, reload on) for a compiled graph and opens the hosted playground when it is ready. +- Options include `--config/-c`, `--host/-H`, `--port/-p`, `--reload/--no-reload`, `--open/--no-open`, `--verbose/-v`, and `--quiet/-q`. +- Defaults are config `10xgraph.json`, host `127.0.0.1`, port `8000`, reload on, playground opened. `--no-open` serves only. +- There is no `10xgraph api` or `10xgraph play`; both were replaced by `dev`. Production runs Gunicorn in the image from `10xgraph build`. `10xgraph build` diff --git a/tenxgraph_api/cli/templates/skills/10xgraph/references/production-runtime.md b/tenxgraph_api/cli/templates/skills/10xgraph/references/production-runtime.md index 385de27..2d72d5b 100644 --- a/tenxgraph_api/cli/templates/skills/10xgraph/references/production-runtime.md +++ b/tenxgraph_api/cli/templates/skills/10xgraph/references/production-runtime.md @@ -4,7 +4,7 @@ Use this when changing API serving, deployment, auth/middleware, worker scaling, ## API Server -`10xgraph api` starts a Uvicorn ASGI server with FastAPI. The server loads the configured compiled graph once at startup and reuses it for requests. +`10xgraph dev` starts a Uvicorn ASGI server (local development, with reload) with FastAPI. The server loads the configured compiled graph once at startup and reuses it for requests. Runtime components: @@ -42,8 +42,7 @@ Production should set `MODE=production`, configure CORS origins, and use real au ## CLI and Docker -- `10xgraph api`: serve graph over HTTP. -- `10xgraph play`: serve graph and open/use playground flow. +- `10xgraph dev`: serve graph over HTTP and open the playground (`--no-open` to serve only). - `10xgraph init`: scaffold config and graph. - `10xgraph build --docker-compose`: generate Docker deployment files. diff --git a/tenxgraph_api/cli/templates/skills/10xgraph/references/realtime.md b/tenxgraph_api/cli/templates/skills/10xgraph/references/realtime.md index 836c7c5..d3ba1d0 100644 --- a/tenxgraph_api/cli/templates/skills/10xgraph/references/realtime.md +++ b/tenxgraph_api/cli/templates/skills/10xgraph/references/realtime.md @@ -140,7 +140,7 @@ per-turn observability stand-in. ## API server WebSocket bridge (`/v1/graph/live`) -`10xgraph api` exposes `ws:///v1/graph/live` when the configured graph is rooted at a +`10xgraph dev` exposes `ws:///v1/graph/live` when the configured graph is rooted at a `LiveAgent`. - First frame: a JSON control frame (e.g. `{"model": "...", "thread_id": "abc", "voice": "Puck"}`); diff --git a/tenxgraph_api/cli/templates/skills/copilot/10xgraph.instructions.md b/tenxgraph_api/cli/templates/skills/copilot/10xgraph.instructions.md index d403fbc..cc5e8ba 100644 --- a/tenxgraph_api/cli/templates/skills/copilot/10xgraph.instructions.md +++ b/tenxgraph_api/cli/templates/skills/copilot/10xgraph.instructions.md @@ -14,7 +14,7 @@ When generating, refactoring, or debugging code in this repo, prefer 10xGraph's - Python API/CLI SDK: `10xgraph-api` — `pip install 10xgraph-api` — source under `tenxgraph_api/` in https://github.com/10xGraph/10xgraph-api - TypeScript SDK: `10xgraph-client` — `npm install 10xgraph-client` — source at https://github.com/10xGraph/10xgraph-client/tree/master/src - Docs: https://10xgraph.com/ (source of truth for public API names) -- Playground: `10xgraph play` (after the CLI is installed) +- Playground: `10xgraph dev` (after the CLI is installed) Never use repository folder names (e.g. `10xgraph-api`) in install commands or user-facing docs — use the published package names above. diff --git a/tenxgraph_api/src/app/core/config/graph_config.py b/tenxgraph_api/src/app/core/config/graph_config.py index 0c14d7c..7d6774e 100644 --- a/tenxgraph_api/src/app/core/config/graph_config.py +++ b/tenxgraph_api/src/app/core/config/graph_config.py @@ -299,11 +299,12 @@ def from_dict(cls, data: dict) -> "RateLimitConfig": @dataclass class WebSocketConfig: - """WebSocket connection limits parsed from 10xgraph.json. + """WebSocket settings and connection limits parsed from 10xgraph.json. Example:: "websocket": { + "enabled": true, "max_connections": 1000, "max_connections_per_user": 10, "realtime_models": ["gemini-2.5-flash-live"], @@ -329,9 +330,15 @@ class WebSocketConfig: presentation options (voice, modalities, vad) only. ``system_prompt`` replaces the agent's instructions, so a client could rewrite its rules; list it only when every caller is trusted to do that. ``model`` and ``tools_tags`` have their own rules and are not listed. + + ``enabled`` mounts ``/v1/graph/ws`` and ``/v1/graph/live``. ``false`` leaves both out, so the + server needs no WebSocket library. Unset (``None``) keeps them mounted, as before this key + existed. Serving them needs ``websockets`` or ``wsproto`` (``10xgraph-api[websocket]``): + ``true`` without one refuses to boot, unset only logs a warning. """ max_connections: int | None + enabled: bool | None = None max_connections_per_user: int | None = DEFAULT_WS_MAX_CONNECTIONS_PER_USER realtime_models: tuple[str, ...] = () realtime_client_overrides: tuple[str, ...] = DEFAULT_REALTIME_CLIENT_OVERRIDES @@ -340,8 +347,10 @@ class WebSocketConfig: def from_dict(cls, data: dict) -> "WebSocketConfig": if not isinstance(data, dict): raise ValueError("websocket must be an object") + enabled = data.get("enabled") return cls( max_connections=_connection_limit(data, "max_connections", DEFAULT_WS_MAX_CONNECTIONS), + enabled=None if enabled is None else _parse_bool(enabled, field="websocket.enabled"), max_connections_per_user=_connection_limit( data, "max_connections_per_user", DEFAULT_WS_MAX_CONNECTIONS_PER_USER ), diff --git a/tenxgraph_api/src/app/core/middleware/rate_limit/factory.py b/tenxgraph_api/src/app/core/middleware/rate_limit/factory.py index 04409c2..0f09609 100644 --- a/tenxgraph_api/src/app/core/middleware/rate_limit/factory.py +++ b/tenxgraph_api/src/app/core/middleware/rate_limit/factory.py @@ -1,5 +1,5 @@ import logging -from typing import TYPE_CHECKING, Any +from typing import Any from injectq import InjectQ @@ -10,8 +10,11 @@ from .redis import RedisRateLimitBackend -if TYPE_CHECKING: +try: + # Needed at runtime: _bind_redis binds the client under its class too. from redis.asyncio import Redis +except ImportError: # the redis extra is not installed + Redis = None # type: ignore[assignment,misc] logger = logging.getLogger("tenxgraph_api.rate_limit") diff --git a/tenxgraph_api/src/app/main.py b/tenxgraph_api/src/app/main.py index 3d82bb3..d264994 100644 --- a/tenxgraph_api/src/app/main.py +++ b/tenxgraph_api/src/app/main.py @@ -32,7 +32,7 @@ SHUTDOWN_DRAIN_TIMEOUT_SECONDS = 30.0 # GraphConfig loads the ``env`` file named in 10xgraph.json, so it must run before Settings -# is first built. `10xgraph api` loads that file itself, but plain `gunicorn ...main:app` +# is first built. `10xgraph dev` loads that file itself, but plain `gunicorn ...main:app` # (the generated Dockerfile) does not, and Settings is cached after the first read. graph_path = os.environ.get("GRAPH_PATH") or default_config_path() graph_config = GraphConfig(graph_path) @@ -187,8 +187,13 @@ async def _drain_background_tasks(timeout: float = SHUTDOWN_DRAIN_TIMEOUT_SECOND # init routes # The evals viewer reads local report files and has no auth, so it is a development tool only. -# The AG-UI endpoint is opt-in through 10xgraph.json. -init_routes(app, evals=settings.MODE != "production", ag_ui=graph_config.ag_ui.enabled) +# The AG-UI endpoint is opt-in through 10xgraph.json; the WebSocket routes can be turned off there. +init_routes( + app, + evals=settings.MODE != "production", + ag_ui=graph_config.ag_ui.enabled, + websocket=graph_config.websocket.enabled, +) # Secure by construction: refuse to boot if any non-public route forgot its # RequirePermission guard (a forgotten guard would otherwise ship an open endpoint). diff --git a/tenxgraph_api/src/app/routers/setup_router.py b/tenxgraph_api/src/app/routers/setup_router.py index f7f5ee8..ffa71e6 100644 --- a/tenxgraph_api/src/app/routers/setup_router.py +++ b/tenxgraph_api/src/app/routers/setup_router.py @@ -1,4 +1,8 @@ +import importlib.util +import logging + from fastapi import APIRouter, FastAPI +from fastapi.routing import APIWebSocketRoute from .checkpointer.router import router as checkpointer_router from .evals import router as evals_router @@ -8,7 +12,10 @@ from .store import router as store_router +logger = logging.getLogger("tenxgraph_api") + AG_UI_INSTALL_HINT = 'pip install "10xgraph-api[ag-ui]"' +WEBSOCKET_INSTALL_HINT = 'pip install "10xgraph-api[websocket]"' def _import_ag_ui_router() -> APIRouter: @@ -34,7 +41,40 @@ def _ag_ui_router() -> APIRouter: raise -def init_routes(app: FastAPI, *, evals: bool = True, ag_ui: bool = False): +def websocket_library_available() -> bool: + """Uvicorn serves WebSockets only with ``websockets`` or ``wsproto`` installed.""" + return any(importlib.util.find_spec(name) is not None for name in ("websockets", "wsproto")) + + +def _graph_router(websocket: bool | None) -> APIRouter: + """The graph router, without ``/v1/graph/ws`` and ``/v1/graph/live`` when disabled. + + ``websocket`` is ``websocket.enabled`` from 10xgraph.json: ``False`` drops both routes, + ``True`` requires a WebSocket library, ``None`` (unset) keeps them and only warns when the + library is missing, as servers did before the key existed. + """ + if websocket is False: + http_only = APIRouter() + http_only.routes = [ + route for route in graph_router.routes if not isinstance(route, APIWebSocketRoute) + ] + return http_only + + if not websocket_library_available(): + message = ( + "WebSocket routes (/v1/graph/ws, /v1/graph/live) need the websockets package. " + f"Install it with: {WEBSOCKET_INSTALL_HINT}, or set " + '"websocket": {"enabled": false} in 10xgraph.json.' + ) + if websocket is True: + raise RuntimeError(f"websocket.enabled is true, but no WebSocket library. {message}") + logger.warning(message) + return graph_router + + +def init_routes( + app: FastAPI, *, evals: bool = True, ag_ui: bool = False, websocket: bool | None = None +): """ Initialize the routes for the FastAPI application. @@ -48,8 +88,10 @@ def init_routes(app: FastAPI, *, evals: bool = True, ag_ui: bool = False): leaves it out. ag_ui (bool): Mount the AG-UI endpoint (``POST /v1/ag-ui``). Off unless ``ag_ui.enabled`` is set in 10xgraph.json. + websocket (bool | None): ``websocket.enabled`` from 10xgraph.json. ``False`` leaves + out ``/v1/graph/ws`` and ``/v1/graph/live``; see ``_graph_router``. """ - app.include_router(graph_router) + app.include_router(_graph_router(websocket)) app.include_router(checkpointer_router) app.include_router(store_router) if evals: diff --git a/tenxgraph_api/src/app/utils/callable_helper.py b/tenxgraph_api/src/app/utils/callable_helper.py index b6beb34..9cd36ef 100644 --- a/tenxgraph_api/src/app/utils/callable_helper.py +++ b/tenxgraph_api/src/app/utils/callable_helper.py @@ -6,7 +6,7 @@ def _is_async_callable(func: Callable[..., Any]) -> bool: """Detect if a callable is a coroutine function (async def).""" - return asyncio.iscoroutinefunction(func) + return inspect.iscoroutinefunction(func) async def call_sync_or_async(func: Callable[..., Any], *args, **kwargs) -> Any: diff --git a/tests/cli/test_cli_demo.py b/tests/cli/test_cli_demo.py index b012bd8..6bbe8c8 100644 --- a/tests/cli/test_cli_demo.py +++ b/tests/cli/test_cli_demo.py @@ -16,7 +16,7 @@ def test_demo_renders_all_themes_without_side_effects(monkeypatch) -> None: assert command.execute() == 0 rendered = stream.getvalue() - for theme in ("Typing", "Api", "Init", "Build", "Eval"): + for theme in ("Typing", "Dev", "Init", "Build", "Eval"): assert theme in rendered assert "All preview states rendered successfully" in rendered assert "none" in rendered @@ -30,10 +30,10 @@ def test_demo_rejects_unknown_style() -> None: assert "Unknown animation style" in stream.getvalue() -def test_play_alias_selects_typing_theme(monkeypatch) -> None: +def test_typing_style_selects_typing_theme(monkeypatch) -> None: monkeypatch.setattr(demo_module.time, "sleep", lambda _seconds: None) stream = io.StringIO() command = DemoCommand(OutputFormatter(stream=stream)) - assert command.execute(style="play") == 0 + assert command.execute(style="typing") == 0 assert "Typing" in stream.getvalue() diff --git a/tests/cli/test_cli_main.py b/tests/cli/test_cli_main.py index cd308a7..d321886 100644 --- a/tests/cli/test_cli_main.py +++ b/tests/cli/test_cli_main.py @@ -10,18 +10,14 @@ runner = CliRunner() -def test_play_command_delegates_to_api_command(monkeypatch): +def test_dev_command_opens_the_playground_by_default(monkeypatch): called = {} - monkeypatch.setattr(main_mod, "setup_cli_logging", lambda **kwargs: None) + monkeypatch.setattr( + main_mod.APICommand, "execute", lambda self, **kwargs: called.update(kwargs) or 0 + ) - def fake_execute(self, **kwargs): - called.update(kwargs) - return 0 - - monkeypatch.setattr(main_mod.APICommand, "execute", fake_execute) - - result = runner.invoke(main_mod.app, ["play", "--port", "9001", "--no-reload"]) + result = runner.invoke(main_mod.app, ["dev", "--port", "9001", "--no-reload"]) assert result.exit_code == 0 assert called["config"] == "10xgraph.json" @@ -31,7 +27,7 @@ def fake_execute(self, **kwargs): assert called["open_playground"] is True -def test_api_command(monkeypatch): +def test_dev_command_no_open_serves_only(monkeypatch): called = {} monkeypatch.setattr(main_mod, "setup_cli_logging", lambda **kwargs: None) monkeypatch.setattr( @@ -39,13 +35,20 @@ def test_api_command(monkeypatch): ) result = runner.invoke( main_mod.app, - ["api", "-c", "custom.json", "-H", "1.2.3.4", "-p", "8080", "--no-reload", "--verbose"], + ["dev", "-c", "custom.json", "-H", "1.2.3.4", "-p", "8080", "--no-reload", "--no-open"], ) assert result.exit_code == 0 assert called["config"] == "custom.json" assert called["host"] == "1.2.3.4" assert called["port"] == 8080 assert called["reload"] is False + assert called["open_playground"] is False + + +@pytest.mark.parametrize("removed", ["api", "play"]) +def test_api_and_play_commands_are_removed(removed): + result = runner.invoke(main_mod.app, [removed]) + assert result.exit_code != 0 def test_version_command(monkeypatch): diff --git a/tests/cli/test_init_non_interactive.py b/tests/cli/test_init_non_interactive.py index 0a035b1..4663ee0 100644 --- a/tests/cli/test_init_non_interactive.py +++ b/tests/cli/test_init_non_interactive.py @@ -55,3 +55,105 @@ def test_non_interactive_rejects_production_options_on_quick_start(tmp_path: Pat non_interactive=True, ) assert result != 0 + + +def _dependencies(target: Path) -> list[str]: + import tomllib + + return tomllib.loads((target / "pyproject.toml").read_text())["project"]["dependencies"] + + +def test_production_dependencies_follow_the_answers(tmp_path: Path) -> None: + """JWT and Redis add their extras; the Dockerfile installs exactly this list.""" + target = tmp_path / "secure-agent" + command = InitCommand(OutputFormatter(stream=io.StringIO())) + result = command.execute( + path=str(target), + agent_name="Secure Agent", + template="production", + auth="jwt", + rate_limit="redis", + provider="openai", + non_interactive=True, + ) + assert result == 0 + assert _dependencies(target) == ["10xgraph[openai]", "10xgraph-api[jwt,redis]"] + + import json + + config = json.loads((target / "10xgraph.json").read_text()) + assert config["rate_limit"]["redis"] == {"url": "${REDIS_URL}"} + + agent_py = (target / "graph" / "agent.py").read_text() + assert 'model="gpt-4o-mini"' in agent_py + assert 'provider="openai"' in agent_py + assert 'OPENAI_API_KEY=""' in (target / ".env.example").read_text() + assert '"OPENAI_API_KEY"' in (target / "tests" / "conftest.py").read_text() + assert "$core_requirement" not in (target / "pyproject.toml").read_text() + assert "$api_requirement" not in (target / "pyproject.toml").read_text() + + +def test_production_without_jwt_or_redis_installs_neither(tmp_path: Path) -> None: + target = tmp_path / "open-agent" + command = InitCommand(OutputFormatter(stream=io.StringIO())) + result = command.execute( + path=str(target), + agent_name="Open Agent", + template="production", + auth="custom", + rate_limit="memory", + provider="anthropic", + non_interactive=True, + ) + assert result == 0 + assert _dependencies(target) == ["10xgraph[anthropic]", "10xgraph-api"] + assert 'model="claude-sonnet-5-5"' in (target / "graph" / "agent.py").read_text() + + +def test_quick_start_defaults_to_google(tmp_path: Path) -> None: + target = tmp_path / "quick-agent" + command = InitCommand(OutputFormatter(stream=io.StringIO())) + assert command.execute(path=str(target), agent_name="Quick", non_interactive=True) == 0 + agent_py = (target / "graph" / "agent.py").read_text() + assert 'model="gemini-2.5-flash"' in agent_py + assert 'provider="google"' in agent_py + assert 'GOOGLE_API_KEY=""' in (target / ".env.example").read_text() + + +def test_non_interactive_rejects_unknown_provider(tmp_path: Path) -> None: + command = InitCommand(OutputFormatter(stream=io.StringIO())) + result = command.execute(path=str(tmp_path / "x"), provider="mistral", non_interactive=True) + assert result != 0 + + +def test_websocket_off_by_default_and_not_installed(tmp_path: Path) -> None: + import json + + target = tmp_path / "no-ws" + command = InitCommand(OutputFormatter(stream=io.StringIO())) + result = command.execute( + path=str(target), template="production", provider="openai", non_interactive=True + ) + assert result == 0 + assert _dependencies(target) == ["10xgraph[openai]", "10xgraph-api"] + config = json.loads((target / "10xgraph.json").read_text()) + assert config["websocket"] == {"enabled": False} + + +def test_websocket_on_adds_the_extra_and_enables_the_routes(tmp_path: Path) -> None: + import json + + target = tmp_path / "ws" + command = InitCommand(OutputFormatter(stream=io.StringIO())) + result = command.execute( + path=str(target), + template="production", + auth="jwt", + provider="anthropic", + websocket=True, + non_interactive=True, + ) + assert result == 0 + assert _dependencies(target) == ["10xgraph[anthropic]", "10xgraph-api[jwt,websocket]"] + config = json.loads((target / "10xgraph.json").read_text()) + assert config["websocket"] == {"enabled": True} diff --git a/tests/unit_tests/test_cli_animations.py b/tests/unit_tests/test_cli_animations.py index cb7992d..e26f287 100644 --- a/tests/unit_tests/test_cli_animations.py +++ b/tests/unit_tests/test_cli_animations.py @@ -43,7 +43,7 @@ def plain_tail(rendered: str) -> str: def test_intro_releases_every_alternate_screen_it_claims() -> None: console, stream = terminal() - anim.render_command_intro(console, command="play", subtitle="Serve it", unicode=True) + anim.render_command_intro(console, command="dev", subtitle="Serve it", unicode=True) rendered = stream.getvalue() assert rendered.count(ALT_SCREEN_ON) == 1 @@ -54,10 +54,10 @@ def test_intro_releases_every_alternate_screen_it_claims() -> None: def test_intro_leaves_a_durable_header_in_the_normal_buffer() -> None: """The bug this guards: an alternate screen discards whatever it held.""" console, stream = terminal() - anim.render_command_intro(console, command="play", subtitle="Serve it", unicode=True) + anim.render_command_intro(console, command="dev", subtitle="Serve it", unicode=True) survives = plain_tail(stream.getvalue()) - assert "play" in survives + assert "dev" in survives assert "Serve it" in survives assert UNICODE_GLYPHS.rule in survives @@ -136,11 +136,11 @@ def test_wordmark_uses_the_compact_form_without_block_glyphs() -> None: def test_signature_pipeline_fills_in_step_with_progress() -> None: glyphs = UNICODE_GLYPHS - empty = anim._signature_nodes("play", 0.0, glyphs).plain - done = anim._signature_nodes("play", 1.0, glyphs).plain + empty = anim._signature_nodes("dev", 0.0, glyphs).plain + done = anim._signature_nodes("dev", 1.0, glyphs).plain assert empty.count(glyphs.node) == 0 - assert done.count(glyphs.node) == len(anim._SIGNATURES["play"]) + assert done.count(glyphs.node) == len(anim._SIGNATURES["dev"]) def test_unknown_commands_get_the_default_signature_and_tagline() -> None: @@ -166,7 +166,7 @@ def test_every_cinematic_frame_renders_without_error() -> None: console.print( anim._cinematic_frame( console, - command="play", + command="dev", tagline="Local agent runtime.", glyphs=UNICODE_GLYPHS, progress=index / 10, @@ -189,7 +189,7 @@ def test_intro_frame_shows_versions_and_the_skip_hint() -> None: console, _stream = terminal() frame = anim._cinematic_frame( console, - command="api", + command="dev", tagline="Serving", glyphs=UNICODE_GLYPHS, progress=1.0, @@ -212,7 +212,7 @@ def pressed(): monkeypatch.setattr(anim, "_skip_on_keypress", pressed) console, _stream = terminal() started = time.monotonic() - anim._play_cinematic(console, command="api", subtitle=None, glyphs=UNICODE_GLYPHS) + anim._play_cinematic(console, command="dev", subtitle=None, glyphs=UNICODE_GLYPHS) assert time.monotonic() - started < anim.INTRO_DURATION_SECONDS / 2 diff --git a/tests/unit_tests/test_cli_output.py b/tests/unit_tests/test_cli_output.py index 950e7c4..1508034 100644 --- a/tests/unit_tests/test_cli_output.py +++ b/tests/unit_tests/test_cli_output.py @@ -152,8 +152,8 @@ def test_plain_command_header_degrades_to_static_banner() -> None: output_format=OutputFormat.PLAIN, progress_mode=ProgressMode.PLAIN, ) - rendered.command_header("play", "Start the playground") - assert "== Play ==" in stream.getvalue() + rendered.command_header("dev", "Start the playground") + assert "== Dev ==" in stream.getvalue() assert "Start the playground" in stream.getvalue() @@ -175,9 +175,9 @@ def isatty(self) -> bool: output_format=OutputFormat.HUMAN, progress_mode=ProgressMode.TTY, ) - rendered.command_header("play", "Start the playground") + rendered.command_header("dev", "Start the playground") # The intro plays by default, on a temporary screen (not the opt-in pinned frame). - assert calls == [("play", "Start the playground", True, False)] + assert calls == [("dev", "Start the playground", True, False)] class FakeConsole: @@ -305,7 +305,7 @@ def test_command_header_claims_the_screen_when_fullscreen_was_requested(monkeypa ) rendered.request_fullscreen(True) - rendered.command_header("play", "Serve it") + rendered.command_header("dev", "Serve it") assert rendered.fullscreen_active is True assert console.alt_screen_calls == [True] @@ -320,7 +320,7 @@ def test_command_header_stays_in_the_normal_buffer_without_fullscreen(monkeypatc ) rendered.request_fullscreen(False) - rendered.command_header("play", "Serve it") + rendered.command_header("dev", "Serve it") assert rendered.fullscreen_active is False assert console.alt_screen_calls == [] @@ -336,11 +336,11 @@ def test_command_header_pins_chrome_inside_a_fullscreen_session(monkeypatch) -> ) assert rendered.start_fullscreen_session() is True - rendered.command_header("play", "Serve it", hint="Ctrl+C to stop") + rendered.command_header("dev", "Serve it", hint="Ctrl+C to stop") written = console.file.getvalue() # Header, footer, and a scrolling region confining output between them. - assert "play" in written + assert "dev" in written assert "Ctrl+C to stop" in written assert f"\x1b[5;{console.height - 2}r" in written diff --git a/tests/unit_tests/test_env_file_load_order.py b/tests/unit_tests/test_env_file_load_order.py index 9863d74..98b5242 100644 --- a/tests/unit_tests/test_env_file_load_order.py +++ b/tests/unit_tests/test_env_file_load_order.py @@ -1,6 +1,6 @@ """Settings see the ``env`` file from 10xgraph.json under plain gunicorn (L10). -``10xgraph api`` loads that file before importing the app, but the generated Dockerfile +``10xgraph dev`` loads that file before importing the app, but the generated Dockerfile runs ``gunicorn tenxgraph_api.src.app.main:app`` directly. Settings used to be built (and cached) before GraphConfig loaded the file, so its values were silently ignored. """ diff --git a/tests/unit_tests/test_production_surface.py b/tests/unit_tests/test_production_surface.py index 0e195e6..c61580a 100644 --- a/tests/unit_tests/test_production_surface.py +++ b/tests/unit_tests/test_production_surface.py @@ -117,6 +117,8 @@ def test_dockerfile_runtime_stage_ships_only_the_venv_and_app(): assert not any("curl" in line for line in commands) assert not any("chown -R" in line for line in commands) assert "pip install" not in runtime + # pip is removed from the copied venv; the server never runs it + assert "RUN pip uninstall -y pip" in builder def test_requirements_in_subfolder_keep_their_path(tmp_path): diff --git a/tests/unit_tests/test_rate_limit.py b/tests/unit_tests/test_rate_limit.py index 41e5661..9aa4202 100644 --- a/tests/unit_tests/test_rate_limit.py +++ b/tests/unit_tests/test_rate_limit.py @@ -218,6 +218,20 @@ def test_redis_backend_requires_url_when_no_injected_client(): build_backend(_config(backend="redis"), container=InjectQ()) +def test_redis_backend_from_url_builds_and_binds_the_client(): + """A Redis URL with no injected client must build the backend, not crash.""" + from redis.asyncio import Redis + + container = InjectQ() + backend = build_backend( + _config(backend="redis", redis={"url": "redis://localhost:6379/0"}), container=container + ) + + assert isinstance(backend, RedisRateLimitBackend) + assert container.try_get("redis") is backend._redis + assert container.try_get(Redis) is backend._redis + + def test_redis_backend_from_url_requires_optional_extra(monkeypatch): monkeypatch.setattr( "tenxgraph_api.src.app.core.middleware.rate_limit.redis._REDIS_AVAILABLE", diff --git a/tests/unit_tests/test_slim_image.py b/tests/unit_tests/test_slim_image.py new file mode 100644 index 0000000..94f4f4f --- /dev/null +++ b/tests/unit_tests/test_slim_image.py @@ -0,0 +1,67 @@ +"""The Dockerfile's slim step removes CLI-only packages, never one something else needs.""" + +from __future__ import annotations + +from tenxgraph_api import _slim_image +from tenxgraph_api.cli.templates.defaults import generate_dockerfile_content + + +class _Dist: + def __init__(self, name: str, *requires: str) -> None: + self.metadata = {"Name": name} + self.requires = list(requires) + + +def _installed(*dists: _Dist) -> dict[str, _Dist]: + return {_slim_image._norm(d.metadata["Name"]): d for d in dists} + + +CLI_STACK = ( + _Dist("10xgraph-api", "fastapi", "typer>=0.17", "rich>=14", "questionary>=2"), + _Dist("typer", "click>=8", "rich>=13.8", "shellingham>=1.3"), + _Dist("rich", "Pygments>=2.13", "markdown-it-py>=2.2"), + _Dist("markdown_it_py", "mdurl~=0.1"), + _Dist("mdurl"), + _Dist("Pygments"), + _Dist("questionary", "prompt_toolkit>=2,<4"), + _Dist("prompt_toolkit", "wcwidth"), + _Dist("wcwidth"), + _Dist("shellingham"), + _Dist("click"), + _Dist("uvicorn", "click>=7", "h11"), + _Dist("fastapi", "starlette"), +) + + +def test_removes_every_cli_only_package_when_nothing_else_needs_them() -> None: + assert _slim_image.removable(_installed(*CLI_STACK)) == sorted(_slim_image.CLI_ONLY) + + +def test_keeps_a_cli_package_another_dependency_needs_and_what_it_needs() -> None: + dists = _installed(*CLI_STACK, _Dist("my-reporting-lib", "rich>=13")) + remove = _slim_image.removable(dists) + + for kept in ("rich", "pygments", "markdown-it-py", "mdurl"): + assert kept not in remove + for gone in ("typer", "shellingham", "questionary", "prompt-toolkit", "wcwidth"): + assert gone in remove + + +def test_requirements_behind_an_extra_do_not_keep_a_package() -> None: + dists = _installed(*CLI_STACK, _Dist("some-lib", 'rich>=13; extra == "pretty"')) + assert "rich" in _slim_image.removable(dists) + + +def test_never_removes_click_which_uvicorn_needs() -> None: + assert "click" not in _slim_image.removable(_installed(*CLI_STACK)) + + +def test_dockerfile_runs_the_slim_step_in_the_builder_before_dropping_pip() -> None: + content = generate_dockerfile_content( + "3.13", 8000, "requirements.txt", has_requirements=False, has_pyproject=True + ) + builder, _runtime = content.split("# ---- Stage 2: runtime image ----") + lines = builder.splitlines() + slim = next(i for i, line in enumerate(lines) if "tenxgraph_api._slim_image" in line) + drop_pip = lines.index("RUN pip uninstall -y pip") + assert slim < drop_pip diff --git a/tests/unit_tests/test_websocket_toggle.py b/tests/unit_tests/test_websocket_toggle.py new file mode 100644 index 0000000..0ddf6f7 --- /dev/null +++ b/tests/unit_tests/test_websocket_toggle.py @@ -0,0 +1,85 @@ +"""``websocket.enabled`` in 10xgraph.json decides whether the WebSocket routes exist.""" + +from __future__ import annotations + +import logging + +import pytest +from fastapi import FastAPI +from fastapi.routing import APIWebSocketRoute + +from tenxgraph_api.src.app.core.auth.route_guard import _iter_routes, find_unprotected_routes +from tenxgraph_api.src.app.core.config.graph_config import WebSocketConfig +from tenxgraph_api.src.app.routers import setup_router + + +WS_PATHS = {"/v1/graph/ws", "/v1/graph/live"} + + +def _ws_paths(app: FastAPI) -> set[str]: + # _iter_routes also walks FastAPI 0.139+'s lazy included-router wrappers. + return { + path for path, route, _ in _iter_routes(app.routes) if isinstance(route, APIWebSocketRoute) + } + + +def _http_paths(app: FastAPI) -> set[str]: + return {path for path, _, _ in _iter_routes(app.routes)} - WS_PATHS + + +@pytest.mark.parametrize( + ("raw", "expected"), + [ + ({}, None), + ({"enabled": True}, True), + ({"enabled": False}, False), + ({"enabled": "off"}, False), + ], +) +def test_config_parses_enabled(raw, expected) -> None: + assert WebSocketConfig.from_dict(raw).enabled is expected + + +def test_config_rejects_non_bool_enabled() -> None: + with pytest.raises(ValueError, match="websocket.enabled"): + WebSocketConfig.from_dict({"enabled": "sometimes"}) + + +def test_disabled_leaves_out_both_websocket_routes_only(monkeypatch) -> None: + monkeypatch.setattr(setup_router, "websocket_library_available", lambda: False) + on, off = FastAPI(), FastAPI() + setup_router.init_routes(on, evals=False, websocket=None) + setup_router.init_routes(off, evals=False, websocket=False) + + assert _ws_paths(on) == WS_PATHS + assert _ws_paths(off) == set() + # Every HTTP route survives, still guarded; only the two WebSocket routes go. + assert _http_paths(off) == _http_paths(on) + assert len(_http_paths(off)) > 5 + assert find_unprotected_routes(off) == find_unprotected_routes(on) + + +def test_enabled_without_a_websocket_library_refuses_to_boot(monkeypatch) -> None: + monkeypatch.setattr(setup_router, "websocket_library_available", lambda: False) + with pytest.raises(RuntimeError, match=r"10xgraph-api\[websocket\]"): + setup_router.init_routes(FastAPI(), evals=False, websocket=True) + + +def test_unset_without_a_websocket_library_warns_and_keeps_routes(monkeypatch, caplog) -> None: + monkeypatch.setattr(setup_router, "websocket_library_available", lambda: False) + app = FastAPI() + with caplog.at_level(logging.WARNING, logger="tenxgraph_api"): + setup_router.init_routes(app, evals=False, websocket=None) + + assert _ws_paths(app) == WS_PATHS + assert "10xgraph-api[websocket]" in caplog.text + + +def test_enabled_with_a_websocket_library_mounts_quietly(monkeypatch, caplog) -> None: + monkeypatch.setattr(setup_router, "websocket_library_available", lambda: True) + app = FastAPI() + with caplog.at_level(logging.WARNING, logger="tenxgraph_api"): + setup_router.init_routes(app, evals=False, websocket=True) + + assert _ws_paths(app) == WS_PATHS + assert "websocket" not in caplog.text.lower() From f0e1762aff70b203ac8abd4cc8324cd8fe7fef0b Mon Sep 17 00:00:00 2001 From: Shudipto Trafder Date: Thu, 22 Oct 2026 14:54:51 +0600 Subject: [PATCH 2/3] Refactor documentation in _slim_image.py to clarify purpose and usage of CLI package removal --- tenxgraph_api/_slim_image.py | 51 +++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/tenxgraph_api/_slim_image.py b/tenxgraph_api/_slim_image.py index 8d9d2e7..7a861ed 100644 --- a/tenxgraph_api/_slim_image.py +++ b/tenxgraph_api/_slim_image.py @@ -1,17 +1,38 @@ -"""Remove CLI-only packages from a production image. Run by the generated Dockerfile. - -The server never imports the CLI libraries (typer, rich, questionary and what they pull in), -so the image does not need them. A package is removed only when nothing else installed still -requires it, so a project dependency that uses rich (for example) keeps it and everything it -needs. Standard library only: it runs in the Docker builder stage. - - python -I -m tenxgraph_api._slim_image +"""Drop the CLI's own libraries from a production Docker image. + +Why this exists + ``10xgraph-api`` ships the server and the ``10xgraph`` CLI in one package, so installing it + also installs the CLI's libraries: typer, rich, questionary and what only they pull in + (pygments, markdown-it-py, mdurl, prompt-toolkit, wcwidth, shellingham). The server never + imports any of them. In an image they are about 25 MB (bytecode included) that never runs, + plus extra packages for vulnerability scanners to flag. + +Why not a plain ``pip uninstall`` line in the Dockerfile + The user's own project may depend on one of these packages, for example a library that + prints with rich. A fixed uninstall list would delete it, and the app would fail at startup + with ``ModuleNotFoundError``. This module reads every installed package's requirements and + removes a CLI library only when nothing else still needs it, directly or through another + package. + +How it runs + Only inside Docker. The Dockerfile from ``10xgraph build`` (``generate_dockerfile_content`` + in ``cli/templates/defaults.py``) runs it once in the builders tage, before pip itself is + removed, and skips it when the installed 10xgraph-api is older than this module. To run it + by hand:: + + python -I -m tenxgraph_api._slim_image + + The server and the CLI never import it. It uses only the standard library, so it never + imports a package it is about to remove. """ from __future__ import annotations import re -import subprocess + +# Used only to run pip, which has no supported Python API (pip's docs say to call it as a +# subprocess). See the comment at the call in main(). +import subprocess # nosec B404 import sys import sysconfig from importlib import metadata @@ -19,7 +40,9 @@ CLI_PACKAGE = "10xgraph-api" -# The CLI's own dependencies and everything only they pull in. +# The CLI's own dependencies (typer, rich, questionary in pyproject.toml) and everything only +# they pull in. Keep it in sync when the CLI gains or drops a dependency. click is left out on +# purpose: uvicorn needs it, so the server does too. CLI_ONLY = frozenset( { "typer", @@ -77,8 +100,12 @@ def main() -> int: sys.stdout.write("slim image: no CLI-only packages to remove\n") return 0 - # Fixed package names from CLI_ONLY, never user input. - subprocess.check_call([sys.executable, "-m", "pip", "uninstall", "-y", "-q", *remove]) # noqa: S603 + # Safe to run: the program is this interpreter's own pip (sys.executable, no shell) and the + # arguments are names from CLI_ONLY, never user input. pip must run as a subprocess because + # it has no supported in-process API. + subprocess.check_call( # noqa: S603 # nosec B603 + [sys.executable, "-m", "pip", "uninstall", "-y", "-q", *remove] + ) sys.stdout.write(f"slim image: removed {', '.join(remove)}\n") if "typer" in remove: From 9c2d9f4dfe9af4e41308712ae92472761091e5f3 Mon Sep 17 00:00:00 2001 From: Shudipto Trafder Date: Thu, 22 Oct 2026 15:00:11 +0600 Subject: [PATCH 3/3] Refactor code structure for improved readability and maintainability --- tenxgraph_api/cli/commands/init.py | 6 +- uv.lock | 428 +++++++---------------------- 2 files changed, 97 insertions(+), 337 deletions(-) diff --git a/tenxgraph_api/cli/commands/init.py b/tenxgraph_api/cli/commands/init.py index 4dca689..ea23e29 100644 --- a/tenxgraph_api/cli/commands/init.py +++ b/tenxgraph_api/cli/commands/init.py @@ -200,7 +200,7 @@ def execute( # noqa: PLR0913 step.detail(str(config_path)) agent_name = context["agent_name"] - provider = _PROVIDERS[context.get("provider", _DEFAULT_PROVIDER)] + provider_info = _PROVIDERS[context.get("provider", _DEFAULT_PROVIDER)] # Production lists these in pyproject.toml; quick-start has no pyproject, so it # needs the install step. install_step = ( @@ -216,13 +216,13 @@ def execute( # noqa: PLR0913 "Location": base_path.resolve(), "Template": "production" if is_prod else "quick-start", "Auth": context["auth"], - "AI provider": provider["label"], + "AI provider": provider_info["label"], "WebSocket": "on" if context.get("websocket") else "off", }, next_steps=[ f"cd {base_path}", *install_step, - f"Copy .env.example to .env and set {provider['api_key_env']}.", + f"Copy .env.example to .env and set {provider_info['api_key_env']}.", "10xgraph dev", ], ) diff --git a/uv.lock b/uv.lock index 1de7f17..3dd2157 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.15'", @@ -24,16 +24,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/55/71fe096c2cd2efe1744835e923a319d615a102a068b4f41f938bea3ed853/10xgraph-0.10.0-py3-none-any.whl", hash = "sha256:2eb417c2d527be2f3478f4915f390518ebb61a15a12631b35b10f58b0b0cf095", size = 611400, upload-time = "2026-10-10T14:33:51.574Z" }, ] +[package.optional-dependencies] +images = [ + { name = "pillow" }, +] + [[package]] name = "10xgraph-api" -version = "0.7.0" +version = "0.8.0" source = { editable = "." } dependencies = [ { name = "10xgraph" }, { name = "fastapi" }, - { name = "gunicorn" }, - { name = "orjson" }, - { name = "platformdirs" }, { name = "pydantic" }, { name = "pydantic-settings" }, { name = "python-dotenv" }, @@ -55,6 +57,9 @@ firebase = [ gcloud = [ { name = "google-cloud-logging" }, ] +images = [ + { name = "10xgraph", extra = ["images"] }, +] jwt = [ { name = "pyjwt" }, ] @@ -76,18 +81,16 @@ sentry = [ snowflakekit = [ { name = "snowflakekit" }, ] +websocket = [ + { name = "websockets" }, +] [package.dev-dependencies] dev = [ { name = "10xgraph" }, { name = "ag-ui-protocol" }, { name = "httpx" }, - { name = "lib" }, - { name = "markdown-it-py" }, - { name = "mkdocs-gen-files" }, - { name = "mkdocstrings" }, { name = "mypy" }, - { name = "mypy-extensions" }, { name = "pre-commit" }, { name = "pyjwt" }, { name = "pytest" }, @@ -105,18 +108,16 @@ dev = [ [package.metadata] requires-dist = [ { name = "10xgraph", specifier = ">=0.10.0,<2.0" }, + { name = "10xgraph", extras = ["images"], marker = "extra == 'images'", specifier = ">=0.10.0,<2.0" }, { name = "ag-ui-protocol", marker = "extra == 'ag-ui'", specifier = ">=1.0,<2" }, { name = "fastapi", specifier = ">=0.116,<1.0" }, { name = "firebase-admin", marker = "extra == 'firebase'", specifier = ">=6.5.0" }, { name = "google-cloud-logging", marker = "extra == 'gcloud'" }, - { name = "gunicorn", specifier = ">=23.0,<24" }, { name = "oauth2client", marker = "extra == 'firebase'", specifier = ">=4.1.3" }, { name = "opentelemetry-api", marker = "extra == 'otel'", specifier = ">=1.20.0" }, { name = "opentelemetry-exporter-otlp", marker = "extra == 'otel'", specifier = ">=1.20.0" }, { name = "opentelemetry-instrumentation-fastapi", marker = "extra == 'otel'", specifier = ">=0.40b0" }, { name = "opentelemetry-sdk", marker = "extra == 'otel'", specifier = ">=1.20.0" }, - { name = "orjson", specifier = ">=3.11,<4" }, - { name = "platformdirs", specifier = ">=4.0,<5" }, { name = "pydantic", specifier = ">=2.13,<3" }, { name = "pydantic-settings", specifier = ">=2.3,<3" }, { name = "pyjwt", marker = "extra == 'jwt'", specifier = ">=2.8,<3" }, @@ -129,21 +130,17 @@ requires-dist = [ { name = "snowflakekit", marker = "extra == 'snowflakekit'", specifier = ">=0.1.1,<1.0" }, { name = "textxtract", extras = ["pdf", "docx", "html", "xml", "md"], marker = "extra == 'media'", specifier = ">=0.2.0" }, { name = "typer", specifier = ">=0.17,<1.0" }, - { name = "uvicorn", specifier = ">=0.30,<1.0" }, + { name = "uvicorn", specifier = ">=0.36,<1.0" }, + { name = "websockets", marker = "extra == 'websocket'", specifier = ">=13.0,<17" }, ] -provides-extras = ["sentry", "firebase", "snowflakekit", "redis", "jwt", "media", "gcloud", "otel", "ag-ui"] +provides-extras = ["sentry", "firebase", "snowflakekit", "redis", "jwt", "media", "images", "gcloud", "otel", "ag-ui", "websocket"] [package.metadata.requires-dev] dev = [ { name = "10xgraph", specifier = ">=0.10.0" }, { name = "ag-ui-protocol", specifier = ">=1.0,<2" }, { name = "httpx", specifier = "==0.27.0" }, - { name = "lib", specifier = "==4.0.0" }, - { name = "markdown-it-py", specifier = "==3.0.0" }, - { name = "mkdocs-gen-files", specifier = "==0.5.0" }, - { name = "mkdocstrings", specifier = "==0.25.2" }, { name = "mypy", specifier = ">=1.11.0" }, - { name = "mypy-extensions", specifier = "==1.0.0" }, { name = "pre-commit", specifier = ">=3.8.0" }, { name = "pyjwt" }, { name = "pytest", specifier = ">=8.4.2" }, @@ -641,18 +638,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f6/e4/a4fea0c28787e6fadfdc6bf76f497c8136fdbb915f2942de1070918c1202/firebase_admin-6.8.0-py3-none-any.whl", hash = "sha256:84d5fd82859c4d27b63338c3fe9724667dfe400aa2fd9fef0efffbf6e23bca82", size = 134188, upload-time = "2025-04-24T18:53:23.182Z" }, ] -[[package]] -name = "ghp-import" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, -] - [[package]] name = "google-api-core" version = "2.30.3" @@ -935,18 +920,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/46/5c/2f6c7e24b99dbaf5f8d7e5b1413fc9fc23360cdeb7f290b49a1c87b49560/grpcio_status-1.82.1-py3-none-any.whl", hash = "sha256:71c7f2bea725c0027fa396b77a55d4e9d90591bab90de4c1c03d4df9a56552f0", size = 14636, upload-time = "2026-07-08T12:39:23.113Z" }, ] -[[package]] -name = "gunicorn" -version = "23.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" }, -] - [[package]] name = "h11" version = "0.16.0" @@ -1036,27 +1009,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/34/a7/329aa7afeb0617b413fcd8b10d0e9c11eef8c9bfd8db5a254b15bd7d5b73/injectq-0.4.2-py3-none-any.whl", hash = "sha256:a8b8873e3b1d53ea2a3020478f42ba2698b39436eed2855fac4ee6698b5e650f", size = 74072, upload-time = "2026-05-07T11:55:05.975Z" }, ] -[[package]] -name = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, -] - -[[package]] -name = "lib" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/09/213f0ca3259ce805212fb0615858dc7d87d30bf776179f19d23a5d690aa4/lib-4.0.0.tar.gz", hash = "sha256:da861f02e9ee8748a8f168584de94f0ed85aad65e70f62760f1962ae515c94e1", size = 2769, upload-time = "2021-12-14T03:13:30.296Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/4d/a1a283987b6e478f5e1c89f1721b54b546a7375c76b77578e7d770861ea3/lib-4.0.0-py3-none-any.whl", hash = "sha256:d0a9e03b27d2b94c403f43c1af39366500032d587dcd45ffdf707131cbdf58c6", size = 4047, upload-time = "2021-12-14T03:13:28.763Z" }, -] - [[package]] name = "librt" version = "0.13.0" @@ -1220,69 +1172,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] -[[package]] -name = "markupsafe" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -1292,98 +1181,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] -[[package]] -name = "mergedeep" -version = "1.3.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, -] - -[[package]] -name = "mkdocs" -version = "1.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "ghp-import" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mergedeep" }, - { name = "mkdocs-get-deps" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "pyyaml" }, - { name = "pyyaml-env-tag" }, - { name = "watchdog" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, -] - -[[package]] -name = "mkdocs-autorefs" -version = "1.4.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mkdocs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, -] - -[[package]] -name = "mkdocs-gen-files" -version = "0.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mkdocs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/48/85/2d634462fd59136197d3126ca431ffb666f412e3db38fd5ce3a60566303e/mkdocs_gen_files-0.5.0.tar.gz", hash = "sha256:4c7cf256b5d67062a788f6b1d035e157fc1a9498c2399be9af5257d4ff4d19bc", size = 7539, upload-time = "2023-04-27T19:48:04.894Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/0f/1e55b3fd490ad2cecb6e7b31892d27cb9fc4218ec1dab780440ba8579e74/mkdocs_gen_files-0.5.0-py3-none-any.whl", hash = "sha256:7ac060096f3f40bd19039e7277dd3050be9a453c8ac578645844d4d91d7978ea", size = 8380, upload-time = "2023-04-27T19:48:07.059Z" }, -] - -[[package]] -name = "mkdocs-get-deps" -version = "0.2.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mergedeep" }, - { name = "platformdirs" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, -] - -[[package]] -name = "mkdocstrings" -version = "0.25.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mkdocs" }, - { name = "mkdocs-autorefs" }, - { name = "platformdirs" }, - { name = "pymdown-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/24/a6/d544fae9749b19e23fb590f6344f9eae3a312323065070b4874236bb0e04/mkdocstrings-0.25.2.tar.gz", hash = "sha256:5cf57ad7f61e8be3111a2458b4e49c2029c9cb35525393b179f9c916ca8042dc", size = 91796, upload-time = "2024-07-25T14:55:49.895Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/86/ee2aef075cc9a62a4f087c3c3f4e3e8a8318afe05a92f8f8415f1bf1af64/mkdocstrings-0.25.2-py3-none-any.whl", hash = "sha256:9e2cda5e2e12db8bb98d21e3410f3f27f8faab685a24b03b06ba7daa5b92abfc", size = 29289, upload-time = "2024-07-25T14:55:47.275Z" }, -] - [[package]] name = "msgpack" version = "1.2.1" @@ -1683,59 +1480,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/3f/ab8d29df207ce5f470a07fa96ebb48af4e95b7fab7e7635311b9a32f2fab/opentelemetry_util_http-0.65b0-py3-none-any.whl", hash = "sha256:7553b606f963097cb190536dc30556cce85090692e471a422fff30ca29b04348", size = 8245, upload-time = "2026-07-16T15:25:46.482Z" }, ] -[[package]] -name = "orjson" -version = "3.11.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/6d/11867a3ffa3a3608d84a4de51ef4dd0896d6b5cc9132fbe1daf593e677bc/orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49", size = 228515, upload-time = "2026-05-06T15:09:57.265Z" }, - { url = "https://files.pythonhosted.org/packages/24/75/05912954c8b288f34fcf5cd4b9b071cb4f6e77b9961e175e56ebb258089f/orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291", size = 128409, upload-time = "2026-05-06T15:09:59.063Z" }, - { url = "https://files.pythonhosted.org/packages/ab/86/1c3a47df3bc8191ea9ac51603bbb872a95167a364320c269f2557911f406/orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09", size = 132106, upload-time = "2026-05-06T15:10:00.798Z" }, - { url = "https://files.pythonhosted.org/packages/d7/cf/b33b5f3e695ae7d63feef9d915c37cc3b8f465493dcd4f8e0b4c697a2366/orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4", size = 127864, upload-time = "2026-05-06T15:10:02.15Z" }, - { url = "https://files.pythonhosted.org/packages/31/6a/6cf69385a58208024fcb8c014e2141b8ce838aba6492b589f8acfff97fab/orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882", size = 135213, upload-time = "2026-05-06T15:10:03.515Z" }, - { url = "https://files.pythonhosted.org/packages/e8/f8/0b1bd3e8f2efcdd376af5c8cfd79eaf13f018080c0089c80ebd724e3c7fb/orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff", size = 145994, upload-time = "2026-05-06T15:10:05.083Z" }, - { url = "https://files.pythonhosted.org/packages/f3/59/dab79f61044c529d2c81aecdc589b1f833a1c8dec11ba3b1c2498a02ca7e/orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe", size = 132744, upload-time = "2026-05-06T15:10:06.853Z" }, - { url = "https://files.pythonhosted.org/packages/0e/a4/82b7a2fe5d8a67a59ed831b24d59a3d46ea7d207b66e1602d376541d94a6/orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61", size = 134014, upload-time = "2026-05-06T15:10:08.213Z" }, - { url = "https://files.pythonhosted.org/packages/50/c7/375e83a76851b73b2e39f3bcf0e5a19e2b89bad13e5bca97d0b293d27f24/orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2", size = 141509, upload-time = "2026-05-06T15:10:09.595Z" }, - { url = "https://files.pythonhosted.org/packages/7f/7c/49d5d82a3d3097f641f094f552131f1e2723b0b8cb0fa2874ab65ecfffa6/orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206", size = 415127, upload-time = "2026-05-06T15:10:11.049Z" }, - { url = "https://files.pythonhosted.org/packages/3a/dc/7446c538590d55f455647e5f3c61fc33f7108714e7afcffa6a2a033f8350/orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f", size = 148025, upload-time = "2026-05-06T15:10:12.842Z" }, - { url = "https://files.pythonhosted.org/packages/df/e5/4d2d8af06f788329b4f78f8cc3679bb395392fcaa1e4d8d3c33e85308fa4/orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa", size = 136943, upload-time = "2026-05-06T15:10:14.405Z" }, - { url = "https://files.pythonhosted.org/packages/06/69/850264ccf6d80f6b174620d30a87f65c9b1490aba33fe6b62798e618cad3/orjson-3.11.9-cp312-cp312-win32.whl", hash = "sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470", size = 131606, upload-time = "2026-05-06T15:10:15.791Z" }, - { url = "https://files.pythonhosted.org/packages/b9/d5/973a43fc9c55e20f2051e9830997649f669be0cb3ca52192087c0143f118/orjson-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be", size = 127101, upload-time = "2026-05-06T15:10:17.129Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ae/495470f0e4a18f73fa10b7f6b84b464ec4cc5291c4e0c7c2a6c400bef006/orjson-3.11.9-cp312-cp312-win_arm64.whl", hash = "sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624", size = 126736, upload-time = "2026-05-06T15:10:18.645Z" }, - { url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458, upload-time = "2026-05-06T15:10:20.079Z" }, - { url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368, upload-time = "2026-05-06T15:10:21.549Z" }, - { url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070, upload-time = "2026-05-06T15:10:23.371Z" }, - { url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892, upload-time = "2026-05-06T15:10:24.714Z" }, - { url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217, upload-time = "2026-05-06T15:10:26.084Z" }, - { url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980, upload-time = "2026-05-06T15:10:28.062Z" }, - { url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738, upload-time = "2026-05-06T15:10:29.727Z" }, - { url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033, upload-time = "2026-05-06T15:10:31.152Z" }, - { url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492, upload-time = "2026-05-06T15:10:32.641Z" }, - { url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087, upload-time = "2026-05-06T15:10:34.438Z" }, - { url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031, upload-time = "2026-05-06T15:10:36.358Z" }, - { url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915, upload-time = "2026-05-06T15:10:38.013Z" }, - { url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613, upload-time = "2026-05-06T15:10:39.569Z" }, - { url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086, upload-time = "2026-05-06T15:10:41.262Z" }, - { url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696, upload-time = "2026-05-06T15:10:42.651Z" }, - { url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465, upload-time = "2026-05-06T15:10:44.097Z" }, - { url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364, upload-time = "2026-05-06T15:10:45.839Z" }, - { url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063, upload-time = "2026-05-06T15:10:47.267Z" }, - { url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356, upload-time = "2026-05-06T15:10:48.765Z" }, - { url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592, upload-time = "2026-05-06T15:10:50.13Z" }, - { url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491, upload-time = "2026-05-06T15:10:51.582Z" }, - { url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309, upload-time = "2026-05-06T15:10:53.329Z" }, - { url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030, upload-time = "2026-05-06T15:10:54.988Z" }, - { url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482, upload-time = "2026-05-06T15:10:56.663Z" }, - { url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178, upload-time = "2026-05-06T15:10:58.446Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089, upload-time = "2026-05-06T15:11:00.441Z" }, - { url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921, upload-time = "2026-05-06T15:11:02.176Z" }, - { url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638, upload-time = "2026-05-06T15:11:03.696Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078, upload-time = "2026-05-06T15:11:05.123Z" }, - { url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687, upload-time = "2026-05-06T15:11:06.602Z" }, -] - [[package]] name = "packaging" version = "26.2" @@ -2055,19 +1799,6 @@ crypto = [ { name = "cryptography" }, ] -[[package]] -name = "pymdown-extensions" -version = "11.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/21/a9/5f0c535ba3b08fe09270c16808e053a968868242ecbd5676d4e3a488bf28/pymdown_extensions-11.0.1.tar.gz", hash = "sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0", size = 857113, upload-time = "2026-07-02T17:59:22.955Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" }, -] - [[package]] name = "pymupdf" version = "1.28.0" @@ -2163,18 +1894,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - [[package]] name = "python-discovery" version = "1.4.4" @@ -2265,18 +1984,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] -[[package]] -name = "pyyaml-env-tag" -version = "1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, -] - [[package]] name = "questionary" version = "2.1.1" @@ -2543,30 +2250,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/7c/4e7225d46d634a0d8d534dd8a6ce0c319d09b4d0cf0337eb314ca4789d8c/virtualenv-21.6.1-py3-none-any.whl", hash = "sha256:afe991df855715a2b2f60edfcc0107ef95a79fdfd8cb4cdaa71603d1c12e463b", size = 5506392, upload-time = "2026-07-10T19:33:51.629Z" }, ] -[[package]] -name = "watchdog" -version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, - { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, - { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, - { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, - { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, - { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, - { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, - { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, - { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, - { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, -] - [[package]] name = "wcwidth" version = "0.8.2" @@ -2576,6 +2259,83 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, ] +[[package]] +name = "websockets" +version = "16.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/f7/bc3a25c5ec26ce62ce487690becc2f3710bbc7b33338f005ad390db0b986/websockets-16.1.1.tar.gz", hash = "sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57", size = 182204, upload-time = "2026-07-17T22:51:05.858Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/9d/681cda21c9eee743203a6cb79b9d3d05adad9aa60ec660c6c9bf4dd619ca/websockets-16.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00", size = 179600, upload-time = "2026-07-17T22:49:13.92Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8d/6195a88b45e8d2a8f745fc2046e36f885a3c9763e6767d2c46229bf9510c/websockets-16.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b", size = 177272, upload-time = "2026-07-17T22:49:15.453Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/fe2d498c64dea0095c9a9f9a351af4cd6eef31b618395582bc1f38ba45ff/websockets-16.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175", size = 177542, upload-time = "2026-07-17T22:49:16.875Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ed/f1831681fce0e3242346e5458486003c5f124ed69e5e0b847fd029db4973/websockets-16.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1", size = 187137, upload-time = "2026-07-17T22:49:18.323Z" }, + { url = "https://files.pythonhosted.org/packages/6f/79/4ff9dcc1bb46f6b4c536936dde1fd60f9b564f3304307274db97f4c9496d/websockets-16.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15", size = 188374, upload-time = "2026-07-17T22:49:19.65Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/5c49b6efb36cab733d23773f6de575e1dba65736ead17d5d2b2a1daef779/websockets-16.1.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa", size = 191155, upload-time = "2026-07-17T22:49:21.331Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f6/56ccceda3a4838d18f1d40821480da4775397e8b1eecf4031e20c50e2e90/websockets-16.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab", size = 189011, upload-time = "2026-07-17T22:49:22.889Z" }, + { url = "https://files.pythonhosted.org/packages/86/d6/ad5286241a2bce1107e2798d3bfbd62cf79aee167bdb654f8cb1e9dbf949/websockets-16.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847", size = 187766, upload-time = "2026-07-17T22:49:24.339Z" }, + { url = "https://files.pythonhosted.org/packages/bc/67/d65c970b7e347fdca69479beb7811c2060529956730a7a4e3ae7c66b0e31/websockets-16.1.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428", size = 185173, upload-time = "2026-07-17T22:49:25.743Z" }, + { url = "https://files.pythonhosted.org/packages/1d/5b/14af3cd4ee69d8ea9baca58f3dc3cfb1ba78332a347fd478cb096549d60e/websockets-16.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf", size = 187809, upload-time = "2026-07-17T22:49:27.147Z" }, + { url = "https://files.pythonhosted.org/packages/7b/11/be301710d70de97e3e7b3586e6d492c9c06d6a61bf1c2202c36cf0c75607/websockets-16.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751", size = 186412, upload-time = "2026-07-17T22:49:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/db/07/fe1435bf6fe738a3d3b54dbe0c18dabf12cba4d909ac8b58b539ce27c1f4/websockets-16.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f", size = 188290, upload-time = "2026-07-17T22:49:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0a/81f394aff8efcbb01208c1ced77df0a3c7fcce584a88c7273663697946c2/websockets-16.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2", size = 185844, upload-time = "2026-07-17T22:49:31.447Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/dd485b995473f415510251fe9bd708f2d24458f439fce958daf8d66dc7c6/websockets-16.1.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383", size = 186823, upload-time = "2026-07-17T22:49:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0b/f78de76ff446f1e66af12b43c48a35f31744de93cfdec2f4ea67d5d7bbf1/websockets-16.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3", size = 187102, upload-time = "2026-07-17T22:49:34.616Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/4cf892007778eaf84ad162bfc98046e0ed89b63ac55949e3236626b2a23f/websockets-16.1.1-cp312-cp312-win32.whl", hash = "sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747", size = 179943, upload-time = "2026-07-17T22:49:36.213Z" }, + { url = "https://files.pythonhosted.org/packages/d9/de/6abe251d28c3a3f217096575400b27750b18e0b1d2fff3a2a239960fea07/websockets-16.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7", size = 180243, upload-time = "2026-07-17T22:49:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fd/6ec6c6d2850aea25b1b2aa9901a016980bb87d01e89b3eb00470b1b5d471/websockets-16.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1", size = 179587, upload-time = "2026-07-17T22:49:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d8/1d299d2dd34087db39831a34cc645ef8a6f89d78efada6983093513cd81c/websockets-16.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df", size = 177272, upload-time = "2026-07-17T22:49:40.293Z" }, + { url = "https://files.pythonhosted.org/packages/3d/86/0a70d3ae2f0f2256bb41302d9804dbca65d4360281e7feb3e1f94102ac46/websockets-16.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac", size = 177530, upload-time = "2026-07-17T22:49:41.786Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c2/c676c69444d9db448b3f0a55a98dcc534affce0bce961d9d2f0b8499b10a/websockets-16.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8", size = 187197, upload-time = "2026-07-17T22:49:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0b/13/88137fbaf726ebe29d62c1117fa11fa2bbb6209dc79d4ad738efbe36a2aa/websockets-16.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6", size = 188433, upload-time = "2026-07-17T22:49:45.147Z" }, + { url = "https://files.pythonhosted.org/packages/01/6d/46c2f2ce6751cb26f39293e1ecbf8544cb01321397cd476c2756b98c216d/websockets-16.1.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854", size = 189868, upload-time = "2026-07-17T22:49:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/29/2b/170a9e8097636cfde4dc3c592b6e00b18a44a2f5407606d96ca542dd5838/websockets-16.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a", size = 189059, upload-time = "2026-07-17T22:49:47.972Z" }, + { url = "https://files.pythonhosted.org/packages/a7/48/f0d4ebc9ab4b473b8861b9e20fdb663d515d42f7befdf62cdb60fee7a1ec/websockets-16.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49", size = 187814, upload-time = "2026-07-17T22:49:49.344Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ba/39a41d3ae8e72696a9492581900611c5a91e2b07563b0bcd2523adea9854/websockets-16.1.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785", size = 185229, upload-time = "2026-07-17T22:49:50.787Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/ac15b604f850d1907f0a85ed721cefe47cd45034b3620069b829746cccbe/websockets-16.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56", size = 187874, upload-time = "2026-07-17T22:49:52.228Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/3fbd5d71d59299c3770faa5884d4f45070236ca5a35ab3a61830812c409a/websockets-16.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509", size = 186469, upload-time = "2026-07-17T22:49:53.776Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fc/dd90349bba58af2a53ef2ddd9c32716c81eb6d59a0687939fff561860878/websockets-16.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1", size = 188347, upload-time = "2026-07-17T22:49:55.202Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/f73ba86427682da59b78c11d77ba56d5b801c32e84afe79b274bbd6a9bb2/websockets-16.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead", size = 185903, upload-time = "2026-07-17T22:49:56.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/7c/f95eb20e80104173b3a0a092291f89ea4047ef6e608e0a57ca06eb14eecb/websockets-16.1.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e", size = 186855, upload-time = "2026-07-17T22:49:58.467Z" }, + { url = "https://files.pythonhosted.org/packages/b0/35/dd875b3e050ff232d60fa377707f890e369f74d134f1be32e8f68879747c/websockets-16.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87", size = 187140, upload-time = "2026-07-17T22:50:00.016Z" }, + { url = "https://files.pythonhosted.org/packages/e8/dc/5cbfcb41824502f6af93b8f3943a4d06c67c23c7d2e31eb18748c4a5b2a7/websockets-16.1.1-cp313-cp313-win32.whl", hash = "sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea", size = 179928, upload-time = "2026-07-17T22:50:01.685Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c1/71e5deb5b7f8f226997ab64908c184ac3105c0155ce2d486f318e5dd08a8/websockets-16.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68", size = 180242, upload-time = "2026-07-17T22:50:03.117Z" }, + { url = "https://files.pythonhosted.org/packages/73/a2/ba78a164eeea4620df4a4df4bd2ed6017438c4655cc0f36f2c0bc0432355/websockets-16.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8", size = 179635, upload-time = "2026-07-17T22:50:05.001Z" }, + { url = "https://files.pythonhosted.org/packages/b9/08/d26d7a7628cd4ac34cbbdb63ac80914ca842ed8e42938c40a53567806df3/websockets-16.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293", size = 177320, upload-time = "2026-07-17T22:50:06.427Z" }, + { url = "https://files.pythonhosted.org/packages/0f/45/ebec83e6269536aa5932533c67b0af5c781f3e73fdbcd68672dcf43f4f44/websockets-16.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051", size = 177544, upload-time = "2026-07-17T22:50:07.834Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d5/abc614d2297f6c1c3e01e61260364457a47c25cc1cf6a879038902bc6aa8/websockets-16.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1", size = 187270, upload-time = "2026-07-17T22:50:09.275Z" }, + { url = "https://files.pythonhosted.org/packages/52/71/4c99af3b87dff1b2927981f6876607d4acb45338c665242168d3982f7758/websockets-16.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7", size = 188509, upload-time = "2026-07-17T22:50:10.722Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b4/5c8ca14b0df7eb84ed0524165c5359150210140817a3312aee57bf62a1cf/websockets-16.1.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31", size = 189882, upload-time = "2026-07-17T22:50:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/25/c1/bedfba9e70557129cb8083748d167bdcc01483dedf0f0df143676df05cbe/websockets-16.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0", size = 189114, upload-time = "2026-07-17T22:50:13.789Z" }, + { url = "https://files.pythonhosted.org/packages/df/09/aa835b2787835aebd839114be5de51b797cb480b63ba42b26d34dfe147cb/websockets-16.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3", size = 187861, upload-time = "2026-07-17T22:50:15.179Z" }, + { url = "https://files.pythonhosted.org/packages/20/26/f6408330694dbc9830857d9d23bc14ac4f6875127a480cfdda8d5ca21198/websockets-16.1.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562", size = 185286, upload-time = "2026-07-17T22:50:16.741Z" }, + { url = "https://files.pythonhosted.org/packages/17/9a/e0675e70dd8a80762cf35bb18799d3f290a4890ffe6439bc51d222796083/websockets-16.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b", size = 187935, upload-time = "2026-07-17T22:50:18.213Z" }, + { url = "https://files.pythonhosted.org/packages/33/c1/3234cfb86afde01b81e9bddcc6e534c440975d60a13991259e833069ab3e/websockets-16.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a", size = 186444, upload-time = "2026-07-17T22:50:19.67Z" }, + { url = "https://files.pythonhosted.org/packages/89/87/9c15206e1d778923d8daa9657de07aa62ea815e13448319c98458c37b281/websockets-16.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c", size = 188409, upload-time = "2026-07-17T22:50:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/cf5de5c67676de2d3eef8b2a518f168f6796595447a5b7161ba0d012915c/websockets-16.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499", size = 185958, upload-time = "2026-07-17T22:50:22.719Z" }, + { url = "https://files.pythonhosted.org/packages/62/c0/731b6ddede2e4136912ec4cff2cffbda35af73546be4762c3d7bd3bd79af/websockets-16.1.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985", size = 186911, upload-time = "2026-07-17T22:50:24.108Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7f/39c634472c4469a24a7c09cecddffb08fac6d0e74f73881a94ee8a40a196/websockets-16.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9", size = 187204, upload-time = "2026-07-17T22:50:25.548Z" }, + { url = "https://files.pythonhosted.org/packages/26/89/9667c256c256dafcc62d21328ce7a40067da857969b68ee9af375b0aaf72/websockets-16.1.1-cp314-cp314-win32.whl", hash = "sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328", size = 179603, upload-time = "2026-07-17T22:50:27.086Z" }, + { url = "https://files.pythonhosted.org/packages/bd/dd/1c099d6c0fc5deb6b46ccdbb6981fdb4b12c917869cb3952408409dc18db/websockets-16.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc", size = 179948, upload-time = "2026-07-17T22:50:28.521Z" }, + { url = "https://files.pythonhosted.org/packages/35/25/9956b2d5e0529d5d23924f21bba1440d4c5c88a562e4f08550871ffa97a7/websockets-16.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573", size = 179963, upload-time = "2026-07-17T22:50:29.982Z" }, + { url = "https://files.pythonhosted.org/packages/17/06/55ffc976c488b6aee9ea05761ff7c4e88e7c1fd82818c8ca7b556ad2f90c/websockets-16.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999", size = 177497, upload-time = "2026-07-17T22:50:31.396Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/f7dac2e980bacc92bdc26cebae4ae4d50cae5380732c50980598fc0bbae4/websockets-16.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe", size = 177698, upload-time = "2026-07-17T22:50:32.829Z" }, + { url = "https://files.pythonhosted.org/packages/b2/39/26762f734113e22da2b942c3aca85798e0c0405d64c256549540ff31e5a1/websockets-16.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d", size = 187561, upload-time = "2026-07-17T22:50:34.24Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/c3f330851806b9b02138b774d593478323e73c99238681b4b93efe64e02d/websockets-16.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392", size = 188732, upload-time = "2026-07-17T22:50:36.088Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f2/eb2c450f052de334ae33cf200ece6e87b0e14d186807074e4eb1cd2cdea2/websockets-16.1.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7", size = 190872, upload-time = "2026-07-17T22:50:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/70/31/2ac8cecf3a74f7fed9132129fc3d90b3998a1554570c11a69b2a8c20332d/websockets-16.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499", size = 189305, upload-time = "2026-07-17T22:50:39.53Z" }, + { url = "https://files.pythonhosted.org/packages/6a/cf/8ab19650d3c0d4562c92e70ab47c257c4aa5c6a713ed87fe63766b31fefc/websockets-16.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43", size = 188033, upload-time = "2026-07-17T22:50:40.912Z" }, + { url = "https://files.pythonhosted.org/packages/66/d7/a49a38a6127a4acb134fb1912b215d900cc657605cff32445bf519f3acc4/websockets-16.1.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458", size = 185748, upload-time = "2026-07-17T22:50:42.559Z" }, + { url = "https://files.pythonhosted.org/packages/95/3e/ad1fa40388c7f2e0bb2c7930d0090b6c5498594bd1cdaec18864df3d9e97/websockets-16.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62", size = 188285, upload-time = "2026-07-17T22:50:43.974Z" }, + { url = "https://files.pythonhosted.org/packages/35/b8/d5db28ca264b9104f82196f92dc8843e35fd391f763d42e4ad358f5bc97e/websockets-16.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb", size = 186777, upload-time = "2026-07-17T22:50:45.474Z" }, + { url = "https://files.pythonhosted.org/packages/42/9c/726cb39d0cc43ae848dce4aa2acb04eecc6738b1264ec6d700bf6bcfb9f8/websockets-16.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51", size = 188682, upload-time = "2026-07-17T22:50:46.973Z" }, + { url = "https://files.pythonhosted.org/packages/be/c7/1168704de8c2dd483edabe4a22cbe4465dd8be8dd95561d214f9fe092871/websockets-16.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0", size = 186377, upload-time = "2026-07-17T22:50:48.413Z" }, + { url = "https://files.pythonhosted.org/packages/ca/40/f9ff2d630ffce4e7dfea0b2288e1caf9ebbf9ff8a9ec9396136ce8b94935/websockets-16.1.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217", size = 187148, upload-time = "2026-07-17T22:50:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/e177c8299f78d7cbe2d14df228643c10c70c0e86e108e092056bbcc16e46/websockets-16.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737", size = 187578, upload-time = "2026-07-17T22:50:51.619Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/b6987faf330f5af5c787a2610124c2e8403d51724f9001ec4fff6311fe7a/websockets-16.1.1-cp314-cp314t-win32.whl", hash = "sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7", size = 179729, upload-time = "2026-07-17T22:50:53.269Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6e/fbac6ed878dd362fbad7d415fa4f84d38e3e33fed8cde45c64e783acf826/websockets-16.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231", size = 180072, upload-time = "2026-07-17T22:50:54.969Z" }, + { url = "https://files.pythonhosted.org/packages/be/4d/2d0d67834092e354d2b0498f014a41249a89556bc406cf86f3e1557bb463/websockets-16.1.1-py3-none-any.whl", hash = "sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3", size = 173814, upload-time = "2026-07-17T22:51:04.184Z" }, +] + [[package]] name = "wrapt" version = "2.2.2"