From a87332aaa3b8524c2561e15eb0e26ed5394c5228 Mon Sep 17 00:00:00 2001 From: "const.koutsakis@aurecongroup.com" Date: Mon, 27 Apr 2026 03:27:22 +1000 Subject: [PATCH] feat: backend scaffold + Pydantic StrictModel + example schemas (#17, #18) --- .github/workflows/ci.yml | 8 +- pyproject.toml | 3 + src/api/main.py | 63 ++++++++++++ src/api/routes.py | 56 +++++++++++ src/api/sessions.py | 63 ++++++++++++ src/models/_base.py | 22 +++++ src/models/config.py | 75 +++++++++++++++ src/models/health.py | 24 +++++ src/models/session.py | 24 +++++ tests/conftest.py | 20 ++++ tests/test_api.py | 70 ++++++++++++++ tests/test_models.py | 62 ++++++++++++ tests/test_placeholder.py | 9 -- uv.lock | 195 ++++++++++++++++++++++++++++++++++++++ 14 files changed, 679 insertions(+), 15 deletions(-) create mode 100644 src/api/main.py create mode 100644 src/api/routes.py create mode 100644 src/api/sessions.py create mode 100644 src/models/_base.py create mode 100644 src/models/config.py create mode 100644 src/models/health.py create mode 100644 src/models/session.py create mode 100644 tests/conftest.py create mode 100644 tests/test_api.py create mode 100644 tests/test_models.py delete mode 100644 tests/test_placeholder.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3af145f..ffd451b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,11 +54,7 @@ jobs: coverage: name: Coverage runs-on: ubuntu-latest - # Runs the suite with coverage. Until ticket #17 lands real source under - # src/, the template has no measurable coverage; pyproject.toml's - # [tool.coverage.report].fail_under stays at 75 (the eventual target), - # while CI uses --cov-fail-under=0 so the empty scaffold doesn't fail. - # When #17 + #18 ship real source + tests, drop the override here. + # Enforces [tool.coverage.report].fail_under from pyproject.toml (75%). steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 @@ -66,7 +62,7 @@ jobs: with: python-version: "3.14" - run: uv sync --frozen --extra dev - - run: uv run pytest tests/ --cov=src --cov-report=term-missing --cov-fail-under=0 + - run: uv run pytest tests/ --cov=src --cov-report=term-missing architecture: name: Architecture (import-linter) diff --git a/pyproject.toml b/pyproject.toml index 4c3ea7f..aae2e82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,10 @@ classifiers = [ ] dependencies = [ "fastapi>=0.115.0", + "uvicorn[standard]>=0.34.0", "pydantic>=2.11.0", + "pydantic-settings>=2.9.0", + "httpx>=0.28.1", ] [project.optional-dependencies] diff --git a/src/api/main.py b/src/api/main.py new file mode 100644 index 0000000..e8f97ad --- /dev/null +++ b/src/api/main.py @@ -0,0 +1,63 @@ +"""harness-python-react — FastAPI application entry point.""" + +from __future__ import annotations + +import logging +from contextlib import asynccontextmanager +from importlib.metadata import PackageNotFoundError, version +from typing import TYPE_CHECKING + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from src.api.routes import router as v1_router +from src.api.sessions import SessionStore + +if TYPE_CHECKING: + from collections.abc import AsyncIterator + +logger = logging.getLogger(__name__) + + +def _package_version() -> str: + """Resolve the running package version, falling back when not installed. + + `[tool.uv] package = false` skips installing the workspace as a Python + package, so `importlib.metadata.version()` raises in local dev. Tests + + `uvicorn --reload` should still boot; callers see ``0.0.0+local`` and the + Docker image (which DOES install the package) reports the real value. + """ + try: + return version("harness-python-react") + except PackageNotFoundError: + return "0.0.0+local" + + +@asynccontextmanager +async def lifespan(app: FastAPI) -> AsyncIterator[None]: + """Application lifespan: initialise process-wide services on startup.""" + app.state.session_store = SessionStore() + logger.info("harness-python-react API started (v%s)", _package_version()) + yield + logger.info("harness-python-react API stopped") + + +app = FastAPI( + title="harness-python-react", + description="Production-quality LLM-driven coding harness — backend scaffold.", + version=_package_version(), + lifespan=lifespan, +) + +# CORS — wide-open in the scaffold so the Vite dev server on :5173 can hit +# the backend on :8000 without preflight friction. Tighten via config in a +# real deployment. +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=False, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(v1_router) diff --git a/src/api/routes.py b/src/api/routes.py new file mode 100644 index 0000000..672d78d --- /dev/null +++ b/src/api/routes.py @@ -0,0 +1,56 @@ +"""Versioned API routes under /api/v1/. + +Two endpoints in the scaffold: + +- ``GET /api/v1/health`` — liveness + running package version. +- ``GET /api/v1/echo`` — returns the ``msg`` query param wrapped in a + ``StrictModel``. Demonstrates the request/response + contract pattern that downstream tickets follow. + +Session-store wiring is plumbed via ``request.app.state.session_store`` for +endpoints that need it (none in the scaffold; pattern is preserved for the +agent / chat endpoint a real project would add). +""" + +from __future__ import annotations + +from importlib.metadata import PackageNotFoundError, version + +from fastapi import APIRouter +from pydantic import Field + +from src.models._base import StrictModel +from src.models.health import HealthResponse + + +def _package_version() -> str: + try: + return version("harness-python-react") + except PackageNotFoundError: + return "0.0.0+local" + + +router = APIRouter(prefix="/api/v1") + + +class EchoResponse(StrictModel, strict=True): + """Response body for `GET /api/v1/echo`.""" + + echoed: str = Field(description="Whatever the client sent in `?msg=`") + + +@router.get("/health") +async def health() -> HealthResponse: + """Liveness signal + the running package version. + + The version is sourced from ``importlib.metadata`` so the deployed + container can be correlated with a ``pyproject.toml`` revision and a + release tag without inspecting the image. + """ + return HealthResponse(status="ok", version=_package_version()) + + +@router.get("/echo") +async def echo(msg: str) -> EchoResponse: + """Echo the ``msg`` query parameter back as a typed response.""" + return EchoResponse(echoed=msg) diff --git a/src/api/sessions.py b/src/api/sessions.py new file mode 100644 index 0000000..6a3ac40 --- /dev/null +++ b/src/api/sessions.py @@ -0,0 +1,63 @@ +"""In-memory session store — a portable harness pattern. + +Single-process, GIL-protected. Each session holds its conversation history +as a list of message dicts compatible with the typical OpenAI chat +completions format. Not safe for multi-process deployments; replace with +Redis or a database for persistence and true concurrency safety. +""" + +from __future__ import annotations + +from datetime import UTC, datetime +from typing import Any +from uuid import UUID, uuid4 + +from src.models.session import SessionInfo + + +class SessionStore: + """In-memory session store.""" + + def __init__(self) -> None: + self._sessions: dict[str, dict[str, Any]] = {} + + def create(self) -> SessionInfo: + """Create a new session with an empty conversation history.""" + session_id = str(uuid4()) + self._sessions[session_id] = { + "session_id": session_id, + "created_at": datetime.now(tz=UTC), + "messages": [], + } + return SessionInfo( + session_id=UUID(session_id), + created_at=self._sessions[session_id]["created_at"], + message_count=0, + ) + + def get(self, session_id: str) -> SessionInfo | None: + """Get session info, or None if not found.""" + data = self._sessions.get(session_id) + if data is None: + return None + return SessionInfo( + session_id=UUID(data["session_id"]), + created_at=data["created_at"], + message_count=len(data["messages"]), + ) + + def get_messages(self, session_id: str) -> list[dict[str, Any]] | None: + """Get conversation history for a session, or None if not found.""" + data = self._sessions.get(session_id) + if data is None: + return None + return list(data["messages"]) + + def set_messages(self, session_id: str, messages: list[dict[str, Any]]) -> None: + """Replace the conversation history for a session.""" + if session_id in self._sessions: + self._sessions[session_id]["messages"] = messages + + def exists(self, session_id: str) -> bool: + """Check if a session exists.""" + return session_id in self._sessions diff --git a/src/models/_base.py b/src/models/_base.py new file mode 100644 index 0000000..d726a8f --- /dev/null +++ b/src/models/_base.py @@ -0,0 +1,22 @@ +"""Base class for every Pydantic contract that crosses a module or process seam. + +Inheriting from ``StrictModel`` gives a contract ``extra="forbid"``: unknown +keys raise ``ValidationError`` at construction. Typos and renamed fields fail +immediately at the seam instead of silently surfacing three calls deep. + +Classes that additionally want strict type checking (no implicit coercion — +e.g. rejecting ``"3.14"`` for a ``float`` field) opt in per-class via the +``strict=True`` keyword, e.g. ``class Foo(StrictModel, strict=True)``. This is +deliberate: models that cross the HTTP boundary need JSON coercion for UUIDs +and integers, while internal result contracts do not. +""" + +from __future__ import annotations + +from pydantic import BaseModel, ConfigDict + + +class StrictModel(BaseModel): + """Base for every contract that crosses a module or process seam.""" + + model_config = ConfigDict(extra="forbid") diff --git a/src/models/config.py b/src/models/config.py new file mode 100644 index 0000000..5772dbf --- /dev/null +++ b/src/models/config.py @@ -0,0 +1,75 @@ +"""Application configuration — pluggable LLM provider via environment variables. + +The agent / eval harness call into an LLM through a single seam configured +at startup. Keep the seam provider-agnostic so the template doesn't lock +users into a specific vendor: the four ``LLM_*`` environment variables below +are all that's required to point at OpenAI, Anthropic, Azure OpenAI, a +self-hosted vLLM endpoint, or any OpenAI-compatible gateway. + +Wired through `pydantic-settings` so the same value precedence applies for +local dev (`.env`), Docker (env), and CI (secrets): + + 1. environment variable + 2. .env file + 3. default declared on the field below + +A real production deployment would also want a vault-backed secret-manager +fetch for `LLM_API_KEY`; that's intentionally out of scope for the scaffold. +""" + +from __future__ import annotations + +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + """Process-wide configuration loaded from environment variables.""" + + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + extra="ignore", + case_sensitive=False, + ) + + llm_provider: str = Field( + default="openai", + description=( + "Identifier for the LLM provider — used by the LLM-client seam to" + " select the right SDK adapter. Examples: openai, anthropic," + " azure-openai, vllm." + ), + ) + llm_api_key: str = Field( + default="", + description=( + "API key passed to the LLM provider. Empty in local dev when no" + " LLM call is exercised; required when the eval harness or agent" + " loop runs." + ), + ) + llm_base_url: str | None = Field( + default=None, + description=( + "Optional base URL — set when pointing at Azure OpenAI, a" + " self-hosted vLLM endpoint, or any OpenAI-compatible gateway." + " Leave None to use the provider SDK's default." + ), + ) + llm_model: str = Field( + default="gpt-4o-mini", + description=( + "Model identifier passed to the provider. Defaults to a small," + " inexpensive model so an accidental run doesn't burn credits." + ), + ) + + +def get_settings() -> Settings: + """Construct a fresh Settings instance. + + Not memoised — tests need to override env variables and re-construct. + Production callers should hold a single instance at app-startup. + """ + return Settings() diff --git a/src/models/health.py b/src/models/health.py new file mode 100644 index 0000000..b12e759 --- /dev/null +++ b/src/models/health.py @@ -0,0 +1,24 @@ +"""Pydantic model for the `/api/v1/health` endpoint.""" + +from __future__ import annotations + +from typing import Literal + +from pydantic import Field + +from src.models._base import StrictModel + + +class HealthResponse(StrictModel, strict=True): + """Response body for `GET /api/v1/health`. + + The `version` field is populated at request time from + `importlib.metadata.version("harness-python-react")` so the running + container can be correlated with a `pyproject.toml` revision and a + release tag without inspecting the image. + """ + + status: Literal["ok"] = Field( + description="Liveness signal — always 'ok' if the process is responsive" + ) + version: str = Field(description="Package version reported by importlib.metadata") diff --git a/src/models/session.py b/src/models/session.py new file mode 100644 index 0000000..b296676 --- /dev/null +++ b/src/models/session.py @@ -0,0 +1,24 @@ +"""Pydantic models for session management.""" + +from __future__ import annotations + +from datetime import datetime +from uuid import UUID + +from pydantic import Field + +from src.models._base import StrictModel + + +class SessionCreate(StrictModel, strict=True): + """Request body for creating a new session (currently empty).""" + + +class SessionInfo(StrictModel, strict=True): + """Public information about an existing session.""" + + session_id: UUID = Field(description="Unique session identifier") + created_at: datetime = Field(description="When the session was created") + message_count: int = Field( + default=0, description="Number of messages in the session" + ) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..31bc497 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,20 @@ +"""Pytest fixtures shared across the suite.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest +from fastapi.testclient import TestClient + +from src.api.main import app + +if TYPE_CHECKING: + from collections.abc import Iterator + + +@pytest.fixture() +def client() -> Iterator[TestClient]: + """FastAPI TestClient with the app's full lifespan exercised.""" + with TestClient(app) as test_client: + yield test_client diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..48e2025 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,70 @@ +"""Tests for the FastAPI scaffold (`/api/v1/health`, `/api/v1/echo`).""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from fastapi.testclient import TestClient + + +def test_health_returns_ok_with_version(client: TestClient) -> None: + response = client.get("/api/v1/health") + assert response.status_code == 200 + body = response.json() + assert body["status"] == "ok" + assert isinstance(body["version"], str) + assert body["version"] # non-empty + + +def test_health_response_rejects_unknown_keys() -> None: + """The HealthResponse contract is StrictModel — extra keys raise.""" + from pydantic import ValidationError + + from src.models.health import HealthResponse + + try: + HealthResponse(status="ok", version="0.1.0", extra="boom") # type: ignore[call-arg] + except ValidationError as exc: + assert "extra" in str(exc) + else: + msg = "expected ValidationError on unknown key" + raise AssertionError(msg) + + +def test_echo_returns_message(client: TestClient) -> None: + response = client.get("/api/v1/echo", params={"msg": "hi"}) + assert response.status_code == 200 + assert response.json() == {"echoed": "hi"} + + +def test_echo_requires_msg_query_param(client: TestClient) -> None: + response = client.get("/api/v1/echo") + assert response.status_code == 422 # FastAPI's missing-required-query default + + +def test_session_store_create_and_get() -> None: + """Sessions store roundtrip — pattern test (not exercised by an endpoint).""" + from src.api.sessions import SessionStore + + store = SessionStore() + info = store.create() + assert info.message_count == 0 + + fetched = store.get(str(info.session_id)) + assert fetched is not None + assert fetched.session_id == info.session_id + + store.set_messages(str(info.session_id), [{"role": "user", "content": "hi"}]) + again = store.get(str(info.session_id)) + assert again is not None + assert again.message_count == 1 + + +def test_session_store_get_unknown_returns_none() -> None: + from src.api.sessions import SessionStore + + store = SessionStore() + assert store.get("does-not-exist") is None + assert store.get_messages("does-not-exist") is None + assert store.exists("does-not-exist") is False diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..b83a087 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,62 @@ +"""Tests for the Pydantic StrictModel base + example schemas.""" + +from __future__ import annotations + +import pytest +from pydantic import ValidationError + +from src.models._base import StrictModel +from src.models.health import HealthResponse +from src.models.session import SessionCreate, SessionInfo + + +class _Example(StrictModel): + name: str + + +def test_strict_model_rejects_unknown_keys() -> None: + with pytest.raises(ValidationError, match="extra"): + _Example(name="ok", surprise="boom") # type: ignore[call-arg] + + +def test_strict_model_accepts_declared_keys() -> None: + obj = _Example(name="ok") + assert obj.name == "ok" + + +def test_health_response_status_must_be_ok() -> None: + with pytest.raises(ValidationError): + HealthResponse(status="degraded", version="0.1.0") # type: ignore[arg-type] + + +def test_session_create_is_empty() -> None: + SessionCreate() # constructible with no fields + + +def test_session_info_requires_session_id_and_created_at() -> None: + with pytest.raises(ValidationError): + SessionInfo() # type: ignore[call-arg] + + +def test_settings_loads_with_env_defaults(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("LLM_PROVIDER", raising=False) + monkeypatch.delenv("LLM_API_KEY", raising=False) + from src.models.config import get_settings + + s = get_settings() + assert s.llm_provider == "openai" + assert s.llm_api_key == "" + assert s.llm_base_url is None + assert s.llm_model == "gpt-4o-mini" + + +def test_settings_reads_from_env(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("LLM_PROVIDER", "anthropic") + monkeypatch.setenv("LLM_API_KEY", "test-key") + monkeypatch.setenv("LLM_MODEL", "claude-haiku-4-5-20251001") + from src.models.config import get_settings + + s = get_settings() + assert s.llm_provider == "anthropic" + assert s.llm_api_key == "test-key" + assert s.llm_model == "claude-haiku-4-5-20251001" diff --git a/tests/test_placeholder.py b/tests/test_placeholder.py deleted file mode 100644 index ba6a741..0000000 --- a/tests/test_placeholder.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Placeholder so pytest does not exit with code 5 (no tests collected) on the -empty scaffold. Real tests land alongside #17 / #18 / #19; this file is -removed once those tests exist and exercise the suite.""" - -from __future__ import annotations - - -def test_placeholder() -> None: - assert 1 + 1 == 2 diff --git a/uv.lock b/uv.lock index 8860cb7..ec07200 100644 --- a/uv.lock +++ b/uv.lock @@ -45,6 +45,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, ] +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + [[package]] name = "cfgv" version = "3.5.0" @@ -266,13 +275,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/8f/774ce522de6a7e70fbeceeaeb6fbe502f5dfb8365728fb3bb4cb23463da8/grimp-3.14-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a424ad14d5deb56721ac24ab939747f72ab3d378d42e7d1f038317d33b052b77", size = 2515157, upload-time = "2025-12-10T17:54:55.874Z" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + [[package]] name = "harness-python-react" version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "fastapi" }, + { name = "httpx" }, { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "uvicorn", extra = ["standard"] }, ] [package.optional-dependencies] @@ -293,19 +314,65 @@ dev = [ requires-dist = [ { name = "commitizen", marker = "extra == 'dev'", specifier = ">=4.0.0" }, { name = "fastapi", specifier = ">=0.115.0" }, + { name = "httpx", specifier = ">=0.28.1" }, { name = "import-linter", marker = "extra == 'dev'", specifier = ">=2.0.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.15.0" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.0.0" }, { name = "pydantic", specifier = ">=2.11.0" }, + { name = "pydantic-settings", specifier = ">=2.9.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.25.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.1.0" }, { name = "pytest-timeout", marker = "extra == 'dev'", specifier = ">=2.3.0" }, { name = "pyyaml", marker = "extra == 'dev'", specifier = ">=6.0.3" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.11.0" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.34.0" }, ] provides-extras = ["dev"] +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + [[package]] name = "identify" version = "2.6.19" @@ -612,6 +679,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" }, ] +[[package]] +name = "pydantic-settings" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/98/c8345dccdc31de4228c039a98f6467a941e39558da41c1744fbe29fa5666/pydantic_settings-2.14.0.tar.gz", hash = "sha256:24285fd4b0e0c06507dd9fdfd331ee23794305352aaec8fc4eb92d4047aeb67d", size = 235709, upload-time = "2026-04-20T13:37:40.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/dd/bebff3040138f00ae8a102d426b27349b9a49acc310fcae7f92112d867e3/pydantic_settings-2.14.0-py3-none-any.whl", hash = "sha256:fc8d5d692eb7092e43c8647c1c35a3ecd00e040fcf02ed86f4cb5458ca62182e", size = 60940, upload-time = "2026-04-20T13:37:38.586Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -688,6 +769,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, ] +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -815,6 +905,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "uvicorn" +version = "0.46.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/93/041fca8274050e40e6791f267d82e0e2e27dd165627bd640d3e0e378d877/uvicorn-0.46.0.tar.gz", hash = "sha256:fb9da0926999cc6cb22dc7cd71a94a632f078e6ae47ff683c5c420750fb7413d", size = 88758, upload-time = "2026-04-23T07:16:00.151Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl", hash = "sha256:bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048", size = 70926, upload-time = "2026-04-23T07:15:58.355Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + [[package]] name = "virtualenv" version = "21.2.4" @@ -830,6 +964,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl", hash = "sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac", size = 5831232, upload-time = "2026-04-14T22:15:29.342Z" }, ] +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, +] + [[package]] name = "wcwidth" version = "0.6.0" @@ -839,6 +1007,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, ] +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + [[package]] name = "wrapt" version = "2.1.2"