From 2e5da2438fa68f881efde904d5301245c89fdc23 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Wed, 29 Apr 2026 00:19:43 +0200 Subject: [PATCH 01/13] feat(runner): minimal Pipeline SDK + BYOC hello-world E2E MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds livepeer_gateway.runner — a Pipeline ABC and a thin aiohttp serve layer — plus a hello-world example that runs end-to-end against an unmodified go-livepeer BYOC stack. Surface: - livepeer_gateway.runner.Pipeline — ABC with predict() - livepeer_gateway.runner.serve(pipeline) → aiohttp app: - POST /predict — body JSON kwargs to predict(); TypeError → 400, other exception → 500 - GET /health — {"status": "ready"} - examples/runner/hello_world/ — Pipeline subclass + Dockerfile + docker-compose + capability registration + e2e curl test The container's /predict path matches the existing go-livepeer BYOC contract — no go-livepeer changes required. ./examples/runner/hello_world/test.sh printing PASS proves the round-trip: curl → gateway → orchestrator → SDK container → response. Co-Authored-By: Claude Opus 4.7 (1M context) --- examples/runner/hello_world/Dockerfile | 15 +++++ examples/runner/hello_world/README.md | 45 +++++++++++++ .../runner/hello_world/docker-compose.yml | 67 +++++++++++++++++++ examples/runner/hello_world/pipeline.py | 12 ++++ .../runner/hello_world/register_capability.py | 52 ++++++++++++++ examples/runner/hello_world/test.sh | 39 +++++++++++ src/livepeer_gateway/runner/__init__.py | 7 ++ src/livepeer_gateway/runner/pipeline.py | 16 +++++ src/livepeer_gateway/runner/serve.py | 65 ++++++++++++++++++ 9 files changed, 318 insertions(+) create mode 100644 examples/runner/hello_world/Dockerfile create mode 100644 examples/runner/hello_world/README.md create mode 100644 examples/runner/hello_world/docker-compose.yml create mode 100644 examples/runner/hello_world/pipeline.py create mode 100644 examples/runner/hello_world/register_capability.py create mode 100755 examples/runner/hello_world/test.sh create mode 100644 src/livepeer_gateway/runner/__init__.py create mode 100644 src/livepeer_gateway/runner/pipeline.py create mode 100644 src/livepeer_gateway/runner/serve.py diff --git a/examples/runner/hello_world/Dockerfile b/examples/runner/hello_world/Dockerfile new file mode 100644 index 0000000..c5803b8 --- /dev/null +++ b/examples/runner/hello_world/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install the package from source — pulls aiohttp, grpcio, protobuf, av per +# pyproject.toml. Build context is the repo root. +COPY pyproject.toml README ./ +COPY src /app/src +RUN pip install --no-cache-dir /app + +COPY examples/runner/hello_world/pipeline.py /app/pipeline.py + +EXPOSE 5000 + +CMD ["python", "/app/pipeline.py"] diff --git a/examples/runner/hello_world/README.md b/examples/runner/hello_world/README.md new file mode 100644 index 0000000..e50e993 --- /dev/null +++ b/examples/runner/hello_world/README.md @@ -0,0 +1,45 @@ +# Hello world (BYOC) + +Smallest end-to-end test of the Pipeline SDK against a real +[go-livepeer](https://github.com/livepeer/go-livepeer) BYOC stack. A `Pipeline` +subclass returns `{"message": "hello, X"}` over HTTP. Registered as a BYOC +capability, called through the gateway, response flows back end-to-end. + +## Run + +```bash +docker compose up -d +./test.sh +docker compose down +``` + +`test.sh` prints `PASS` on success. + +## What's running + +```mermaid +sequenceDiagram + autonumber + participant curl + participant gateway + participant orchestrator + participant hello_world as hello_world
(SDK container) + + curl->>gateway: POST /process/request/predict + gateway->>orchestrator: forward (Livepeer-signed) + orchestrator->>hello_world: POST /predict {"name":"..."} + hello_world-->>orchestrator: {"message":"hello, ..."} + orchestrator-->>gateway: response + gateway-->>curl: response +``` + +Four compose services: + +| Service | What it is | +| --- | --- | +| `gateway`, `orchestrator` | `livepeer/go-livepeer:master` from Docker Hub | +| `hello_world` | The pipeline container — a [BYOC](https://github.com/livepeer/go-livepeer/blob/main/doc/byoc.md) capability built with `livepeer_gateway.runner`. Attached via HTTP register, not the `-aiWorker` mechanism. | +| `register_capability` | One-shot helper that POSTs to `orchestrator:8935/capability/register` | + +First `docker compose up` pulls `livepeer/go-livepeer:master` (~few hundred MB, +cached after) and builds the `hello_world` image locally. diff --git a/examples/runner/hello_world/docker-compose.yml b/examples/runner/hello_world/docker-compose.yml new file mode 100644 index 0000000..7675a4a --- /dev/null +++ b/examples/runner/hello_world/docker-compose.yml @@ -0,0 +1,67 @@ +services: + # Mirrors go-livepeer/doc/byoc.md: on-chain mode against a public Arbitrum + # RPC, but `pricePerUnit 0` keeps the registration free — no balance ever + # leaves the auto-generated keystore. No real chain interaction occurs. + # + # TODO: once livepeer/go-livepeer#3906 ships in :master, drop -network / + # -ethUrl / -ethPassword and run with bare `-network offchain`. Tracked + # in livepeer/go-livepeer#3905. + + orchestrator: + image: livepeer/go-livepeer:master + container_name: orchestrator + command: > + -network arbitrum-one-mainnet + -orchestrator + -ethUrl https://arb1.arbitrum.io/rpc + -ethPassword secret-password + -pricePerUnit 1 + -serviceAddr=orchestrator:8935 + -orchSecret=orch-secret + -v 6 + + gateway: + image: livepeer/go-livepeer:master + container_name: gateway + command: > + -network arbitrum-one-mainnet + -gateway + -ethUrl https://arb1.arbitrum.io/rpc + -ethPassword secret-password + -orchAddr=orchestrator:8935 + -httpAddr=0.0.0.0:9935 + -httpIngest + -v 6 + ports: + - "9935:9935" + depends_on: + - orchestrator + + hello_world: + build: + context: ../../.. + dockerfile: examples/runner/hello_world/Dockerfile + container_name: hello_world + expose: + - "5000" + depends_on: + - orchestrator + + register_capability: + image: python:3.11-slim + container_name: register_capability + command: sh -c "pip install --quiet requests && python /app/register_capability.py" + volumes: + - ./register_capability.py:/app/register_capability.py:ro + environment: + ORCH_URL: https://orchestrator:8935 + ORCH_SECRET: orch-secret + CAPABILITY_NAME: hello-world + CAPABILITY_URL: http://hello_world:5000 + depends_on: + - hello_world + - orchestrator + +networks: + default: + name: livepeer diff --git a/examples/runner/hello_world/pipeline.py b/examples/runner/hello_world/pipeline.py new file mode 100644 index 0000000..49c44b6 --- /dev/null +++ b/examples/runner/hello_world/pipeline.py @@ -0,0 +1,12 @@ +"""Hello-world BYOC pipeline. Run via ``docker compose up``.""" + +from livepeer_gateway.runner import Pipeline, serve + + +class HelloWorld(Pipeline): + def predict(self, name: str = "world") -> dict: + return {"message": f"hello, {name}"} + + +if __name__ == "__main__": + serve(HelloWorld()) diff --git a/examples/runner/hello_world/register_capability.py b/examples/runner/hello_world/register_capability.py new file mode 100644 index 0000000..9e92bf6 --- /dev/null +++ b/examples/runner/hello_world/register_capability.py @@ -0,0 +1,52 @@ +"""Register the hello-world capability with the orchestrator. + +Wire format follows +https://github.com/livepeer/go-livepeer/blob/main/doc/byoc.md +(handler in ``byoc/job_orchestrator.go``). +""" + +import os +import sys +import time + +import requests +import urllib3 + +# Orchestrator's HTTPS endpoint uses a self-signed cert. +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +ORCH_URL = os.environ.get("ORCH_URL", "https://orchestrator:8935") +ORCH_SECRET = os.environ.get("ORCH_SECRET", "orch-secret") +CAPABILITY_NAME = os.environ.get("CAPABILITY_NAME", "hello-world") +CAPABILITY_URL = os.environ.get("CAPABILITY_URL", "http://hello_world:5000") +MAX_ATTEMPTS = int(os.environ.get("MAX_ATTEMPTS", "30")) + +data = { + "name": CAPABILITY_NAME, + "url": CAPABILITY_URL, + "capacity": 1, + "price_per_unit": 0, + "price_scaling": 1, + "currency": "wei", +} +headers = {"Authorization": ORCH_SECRET} + +for attempt in range(1, MAX_ATTEMPTS + 1): + try: + r = requests.post( + f"{ORCH_URL}/capability/register", + json=data, + headers=headers, + verify=False, + timeout=5, + ) + if r.status_code == 200: + print(f"registered {CAPABILITY_NAME} -> {CAPABILITY_URL}") + sys.exit(0) + print(f"attempt {attempt}: status={r.status_code} body={r.text!r}") + except Exception as exc: + print(f"attempt {attempt}: {exc}") + time.sleep(2) + +print("registration failed after timeout") +sys.exit(1) diff --git a/examples/runner/hello_world/test.sh b/examples/runner/hello_world/test.sh new file mode 100755 index 0000000..8a21e67 --- /dev/null +++ b/examples/runner/hello_world/test.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# E2E: send a request through the gateway, assert the response from the +# hello_world container comes back through the orchestrator. + +set -euo pipefail +cd "$(dirname "$0")" + +GATEWAY_URL="${GATEWAY_URL:-http://localhost:9935}" +NAME="${NAME:-livepeer}" +EXPECTED_MSG="hello, ${NAME}" + +echo "Waiting for capability registration..." +for i in $(seq 1 60); do + if docker logs register_capability 2>&1 | grep -q "registered hello-world"; then + echo " registered." + break + fi + sleep 2 +done + +# TODO: swap curl for a livepeer_gateway batch caller (post PR #6) — drops +# the gateway service from compose. +LIVEPEER_HDR=$(printf '%s' '{"request":"{}","parameters":"{}","capability":"hello-world","timeout_seconds":30}' | base64 -w0) + +echo "Sending request through gateway..." +RESPONSE=$(curl -fsS -X POST "${GATEWAY_URL}/process/request/predict" \ + -H "Livepeer: ${LIVEPEER_HDR}" \ + -H "Content-Type: application/json" \ + -d "{\"name\":\"${NAME}\"}") + +echo "Response: ${RESPONSE}" + +if echo "${RESPONSE}" | grep -qE "\"message\"[[:space:]]*:[[:space:]]*\"${EXPECTED_MSG}\""; then + echo "PASS" + exit 0 +fi + +echo "FAIL" +exit 1 diff --git a/src/livepeer_gateway/runner/__init__.py b/src/livepeer_gateway/runner/__init__.py new file mode 100644 index 0000000..2532771 --- /dev/null +++ b/src/livepeer_gateway/runner/__init__.py @@ -0,0 +1,7 @@ +"""Pipeline SDK for creating BYOC-compatible AI capabilities from a simple Python class. +""" + +from .pipeline import Pipeline +from .serve import make_app, serve + +__all__ = ["Pipeline", "make_app", "serve"] diff --git a/src/livepeer_gateway/runner/pipeline.py b/src/livepeer_gateway/runner/pipeline.py new file mode 100644 index 0000000..b5b0fdc --- /dev/null +++ b/src/livepeer_gateway/runner/pipeline.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any + + +class Pipeline(ABC): + """Base class for batch inference pipelines.""" + + @abstractmethod + def predict(self, **kwargs: Any) -> Any: + """Run one inference; kwargs are the JSON request body fields. + + Return any JSON-serialisable value, or raise to signal an error. + """ + ... diff --git a/src/livepeer_gateway/runner/serve.py b/src/livepeer_gateway/runner/serve.py new file mode 100644 index 0000000..65446e9 --- /dev/null +++ b/src/livepeer_gateway/runner/serve.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import logging +from typing import Any + +from aiohttp import web + +from .pipeline import Pipeline + +_LOG = logging.getLogger(__name__) + + +async def handle_predict(request: web.Request) -> web.Response: + """Run one inference. + + Body is JSON; passed as kwargs to ``pipeline.predict()``. Returns the + result as JSON. ``TypeError`` from ``predict()`` becomes HTTP 400; + other exceptions become 500. + """ + pipeline: Pipeline = request.app["pipeline"] + + try: + body = await request.json() + except Exception as exc: + return web.json_response( + {"error": f"invalid JSON body: {exc}"}, + status=400, + ) + if not isinstance(body, dict): + return web.json_response( + {"error": "request body must be a JSON object"}, + status=400, + ) + + try: + result: Any = pipeline.predict(**body) + except TypeError as exc: + return web.json_response( + {"error": f"input mismatch: {exc}"}, + status=400, + ) + except Exception: + _LOG.exception("predict() failed") + return web.json_response({"error": "internal error"}, status=500) + + return web.json_response(result) + + +async def handle_health(_: web.Request) -> web.Response: + """Health probe. Returns ``{"status": "ready"}``.""" + return web.json_response({"status": "ready"}) + + +def make_app(pipeline: Pipeline) -> web.Application: + """Build an aiohttp application exposing ``pipeline`` over HTTP.""" + app = web.Application() + app["pipeline"] = pipeline + app.router.add_post("/predict", handle_predict) + app.router.add_get("/health", handle_health) + return app + + +def serve(pipeline: Pipeline, *, host: str = "0.0.0.0", port: int = 5000) -> None: + """Run the pipeline as an HTTP server on host:port.""" + web.run_app(make_app(pipeline), host=host, port=port) From 07cd5c4b05c377d41545469797cb4b1260d60186 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Wed, 29 Apr 2026 09:32:22 +0200 Subject: [PATCH 02/13] feat(runner): setup() lifecycle hook + HF sentiment example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pipeline.setup() is a non-abstract no-op called once before serve() accepts requests. Subclasses override to load model weights. Adds examples/runner/sentiment/ — a Pipeline subclass that classifies text via Hugging Face transformers. setup() loads the distilbert model from the local HF cache populated at build time by prepare_models.py. Surface: - Pipeline.setup() no-op default - make_app() invokes pipeline.setup() before binding routes - examples/runner/sentiment/ — pipeline + prepare_models + Dockerfile + docker-compose + register + test.sh + README Co-Authored-By: Claude Opus 4.7 (1M context) --- examples/runner/sentiment/Dockerfile | 26 ++++++ examples/runner/sentiment/README.md | 69 ++++++++++++++++ examples/runner/sentiment/docker-compose.yml | 79 +++++++++++++++++++ examples/runner/sentiment/pipeline.py | 25 ++++++ examples/runner/sentiment/prepare_models.py | 12 +++ .../runner/sentiment/register_capability.py | 52 ++++++++++++ examples/runner/sentiment/requirements.txt | 4 + examples/runner/sentiment/test.sh | 39 +++++++++ src/livepeer_gateway/runner/pipeline.py | 7 ++ src/livepeer_gateway/runner/serve.py | 7 +- 10 files changed, 319 insertions(+), 1 deletion(-) create mode 100644 examples/runner/sentiment/Dockerfile create mode 100644 examples/runner/sentiment/README.md create mode 100644 examples/runner/sentiment/docker-compose.yml create mode 100644 examples/runner/sentiment/pipeline.py create mode 100644 examples/runner/sentiment/prepare_models.py create mode 100644 examples/runner/sentiment/register_capability.py create mode 100644 examples/runner/sentiment/requirements.txt create mode 100755 examples/runner/sentiment/test.sh diff --git a/examples/runner/sentiment/Dockerfile b/examples/runner/sentiment/Dockerfile new file mode 100644 index 0000000..6a12b5b --- /dev/null +++ b/examples/runner/sentiment/Dockerfile @@ -0,0 +1,26 @@ +FROM python:3.11-slim + +WORKDIR /app + +# SDK install (in-repo source until livepeer-gateway publishes; will collapse +# to a single `pip install livepeer-gateway` line once on PyPI). +COPY pyproject.toml README ./ +COPY src /app/src +RUN pip install --no-cache-dir /app + +# Pipeline-specific deps. The requirements.txt sets --extra-index-url to +# pull the CPU-only torch wheel (~200 MB vs ~5 GB for the CUDA variant). +COPY examples/runner/sentiment/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Bake model weights at build time so setup() loads from local disk in +# milliseconds. +COPY examples/runner/sentiment/prepare_models.py /app/prepare_models.py +RUN python /app/prepare_models.py + +# Pipeline code last so edits don't invalidate the bake layer above. +COPY examples/runner/sentiment/pipeline.py /app/pipeline.py + +EXPOSE 5000 + +CMD ["python", "/app/pipeline.py"] diff --git a/examples/runner/sentiment/README.md b/examples/runner/sentiment/README.md new file mode 100644 index 0000000..6293ef0 --- /dev/null +++ b/examples/runner/sentiment/README.md @@ -0,0 +1,69 @@ +# Sentiment analysis (BYOC) + +A Hugging Face sentiment classifier shipped as a BYOC capability. Demonstrates +the `setup()` lifecycle hook for one-time model loading. Built on +[distilbert-base-uncased-finetuned-sst-2-english](https://huggingface.co/distilbert/distilbert-base-uncased-finetuned-sst-2-english) — small enough to run on CPU. + +A `Pipeline` subclass loads the model once in `setup()`, then classifies text +on each `POST /predict`. Registered as a BYOC capability, called through the +gateway, response flows back end-to-end. + +## Run + +```bash +docker compose up -d --wait +./test.sh +docker compose down +``` + +`test.sh` prints `PASS` on success. + +> **First build is ~5 minutes** — pulls torch CPU (~200 MB), transformers, and +> bakes the ~250 MB model into the image. Cached after that; rebuilds are fast. + +## What's running + +```mermaid +sequenceDiagram + autonumber + participant curl + participant gateway + participant orchestrator + participant sentiment as sentiment
(SDK container) + + curl->>gateway: POST /process/request/predict + gateway->>orchestrator: forward (Livepeer-signed) + orchestrator->>sentiment: POST /predict {"text":"..."} + sentiment-->>orchestrator: {"label":"POSITIVE","score":0.99,...} + orchestrator-->>gateway: response + gateway-->>curl: response +``` + +Four compose services: + +| Service | What it is | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `gateway`, `orchestrator` | `livepeer/go-livepeer:master` from Docker Hub | +| `sentiment` | The pipeline container — a [BYOC](https://github.com/livepeer/go-livepeer/blob/main/doc/byoc.md) capability built with `livepeer_gateway.runner`. Loads the HF model in `setup()`. | +| `register_capability` | One-shot helper that POSTs to `orchestrator:8935/capability/register` once `sentiment` is healthy | + +The sentiment service has a healthcheck that probes `GET /health` until the +model finishes loading. `register_capability` waits on `service_healthy`, so +the orchestrator never sees a "registered but not loaded" container. + +## Try variations + +```bash +TEXT="this is awful" EXPECTED_LABEL=NEGATIVE ./test.sh +``` + +Or manually: + +```bash +LIVEPEER_HDR=$(printf '%s' '{"request":"{}","parameters":"{}","capability":"sentiment","timeout_seconds":30}' | base64 -w0) + +curl -X POST http://localhost:9935/process/request/predict \ + -H "Livepeer: ${LIVEPEER_HDR}" \ + -H "Content-Type: application/json" \ + -d '{"text":"distributed inference is the future"}' +``` diff --git a/examples/runner/sentiment/docker-compose.yml b/examples/runner/sentiment/docker-compose.yml new file mode 100644 index 0000000..33f0bb9 --- /dev/null +++ b/examples/runner/sentiment/docker-compose.yml @@ -0,0 +1,79 @@ +services: + # Mirrors go-livepeer/doc/byoc.md: on-chain mode against a public Arbitrum + # RPC, but `pricePerUnit 0` keeps the registration free — no balance ever + # leaves the auto-generated keystore. No real chain interaction occurs. + # + # TODO: once livepeer/go-livepeer#3906 ships in :master, drop -network / + # -ethUrl / -ethPassword and run with bare `-network offchain`. Tracked + # in livepeer/go-livepeer#3905. + + orchestrator: + image: livepeer/go-livepeer:master + container_name: orchestrator + command: > + -network arbitrum-one-mainnet + -orchestrator + -ethUrl https://arb1.arbitrum.io/rpc + -ethPassword secret-password + -pricePerUnit 1 + -serviceAddr=orchestrator:8935 + -orchSecret=orch-secret + -v 6 + + gateway: + image: livepeer/go-livepeer:master + container_name: gateway + command: > + -network arbitrum-one-mainnet + -gateway + -ethUrl https://arb1.arbitrum.io/rpc + -ethPassword secret-password + -orchAddr=orchestrator:8935 + -httpAddr=0.0.0.0:9935 + -httpIngest + -v 6 + ports: + - "9935:9935" + depends_on: + - orchestrator + + sentiment: + build: + context: ../../.. + dockerfile: examples/runner/sentiment/Dockerfile + container_name: sentiment + expose: + - "5000" + # Healthcheck waits for setup() to finish loading the model before the + # orchestrator can route requests here. Without this, register_capability + # could complete and the test could fire before the model is loaded. + healthcheck: + test: ["CMD", "python", "-c", + "import urllib.request; urllib.request.urlopen('http://localhost:5000/health').read()"] + interval: 5s + timeout: 5s + retries: 30 + start_period: 60s + depends_on: + - orchestrator + + register_capability: + image: python:3.11-slim + container_name: register_capability + command: sh -c "pip install --quiet requests && python /app/register_capability.py" + volumes: + - ./register_capability.py:/app/register_capability.py:ro + environment: + ORCH_URL: https://orchestrator:8935 + ORCH_SECRET: orch-secret + CAPABILITY_NAME: sentiment + CAPABILITY_URL: http://sentiment:5000 + depends_on: + sentiment: + condition: service_healthy + orchestrator: + condition: service_started + +networks: + default: + name: livepeer diff --git a/examples/runner/sentiment/pipeline.py b/examples/runner/sentiment/pipeline.py new file mode 100644 index 0000000..0e45d6f --- /dev/null +++ b/examples/runner/sentiment/pipeline.py @@ -0,0 +1,25 @@ +"""Sentiment-analysis BYOC pipeline. Run via ``docker compose up`` — see README.md.""" + +from livepeer_gateway.runner import Pipeline, serve +from transformers import pipeline as hf_pipeline + + +class SentimentAnalyzer(Pipeline): + def setup(self): + # Loads from local HF cache populated at Docker build time. + self.model = hf_pipeline( + "sentiment-analysis", + model="distilbert-base-uncased-finetuned-sst-2-english", + ) + + def predict(self, text: str = "Livepeer is great") -> dict: + result = self.model(text)[0] + return { + "label": result["label"], + "score": float(result["score"]), + "text": text, + } + + +if __name__ == "__main__": + serve(SentimentAnalyzer()) diff --git a/examples/runner/sentiment/prepare_models.py b/examples/runner/sentiment/prepare_models.py new file mode 100644 index 0000000..dec0b19 --- /dev/null +++ b/examples/runner/sentiment/prepare_models.py @@ -0,0 +1,12 @@ +"""Download model weights into the local HF cache at build time. + +Invoked by the Dockerfile so ``setup()`` loads from local disk in +milliseconds instead of pulling from HF Hub on every container start. +""" + +from transformers import pipeline + +pipeline( + "sentiment-analysis", + model="distilbert-base-uncased-finetuned-sst-2-english", +) diff --git a/examples/runner/sentiment/register_capability.py b/examples/runner/sentiment/register_capability.py new file mode 100644 index 0000000..4f25e8b --- /dev/null +++ b/examples/runner/sentiment/register_capability.py @@ -0,0 +1,52 @@ +"""Register the sentiment capability with the orchestrator. + +Wire format follows +https://github.com/livepeer/go-livepeer/blob/main/doc/byoc.md +(handler in ``byoc/job_orchestrator.go``). +""" + +import os +import sys +import time + +import requests +import urllib3 + +# Orchestrator's HTTPS endpoint uses a self-signed cert. +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +ORCH_URL = os.environ.get("ORCH_URL", "https://orchestrator:8935") +ORCH_SECRET = os.environ.get("ORCH_SECRET", "orch-secret") +CAPABILITY_NAME = os.environ.get("CAPABILITY_NAME", "sentiment") +CAPABILITY_URL = os.environ.get("CAPABILITY_URL", "http://sentiment:5000") +MAX_ATTEMPTS = int(os.environ.get("MAX_ATTEMPTS", "30")) + +data = { + "name": CAPABILITY_NAME, + "url": CAPABILITY_URL, + "capacity": 1, + "price_per_unit": 0, + "price_scaling": 1, + "currency": "wei", +} +headers = {"Authorization": ORCH_SECRET} + +for attempt in range(1, MAX_ATTEMPTS + 1): + try: + r = requests.post( + f"{ORCH_URL}/capability/register", + json=data, + headers=headers, + verify=False, + timeout=5, + ) + if r.status_code == 200: + print(f"registered {CAPABILITY_NAME} -> {CAPABILITY_URL}") + sys.exit(0) + print(f"attempt {attempt}: status={r.status_code} body={r.text!r}") + except Exception as exc: + print(f"attempt {attempt}: {exc}") + time.sleep(2) + +print("registration failed after timeout") +sys.exit(1) diff --git a/examples/runner/sentiment/requirements.txt b/examples/runner/sentiment/requirements.txt new file mode 100644 index 0000000..f13a349 --- /dev/null +++ b/examples/runner/sentiment/requirements.txt @@ -0,0 +1,4 @@ +--extra-index-url https://download.pytorch.org/whl/cpu + +transformers +torch diff --git a/examples/runner/sentiment/test.sh b/examples/runner/sentiment/test.sh new file mode 100755 index 0000000..13cb6b0 --- /dev/null +++ b/examples/runner/sentiment/test.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# E2E: send a request through the gateway, assert the SentimentAnalyzer +# response (label + score) comes back through the orchestrator. + +set -euo pipefail +cd "$(dirname "$0")" + +GATEWAY_URL="${GATEWAY_URL:-http://localhost:9935}" +TEXT="${TEXT:-Livepeer makes decentralized inference effortless}" +EXPECTED_LABEL="${EXPECTED_LABEL:-POSITIVE}" + +echo "Waiting for capability registration..." +for i in $(seq 1 60); do + if docker logs register_capability 2>&1 | grep -q "registered sentiment"; then + echo " registered." + break + fi + sleep 2 +done + +# TODO: swap curl for a livepeer_gateway batch caller (post PR #6) — drops +# the gateway service from compose. +LIVEPEER_HDR=$(printf '%s' '{"request":"{}","parameters":"{}","capability":"sentiment","timeout_seconds":30}' | base64 -w0) + +echo "Sending request through gateway..." +RESPONSE=$(curl -fsS -X POST "${GATEWAY_URL}/process/request/predict" \ + -H "Livepeer: ${LIVEPEER_HDR}" \ + -H "Content-Type: application/json" \ + -d "{\"text\":\"${TEXT}\"}") + +echo "Response: ${RESPONSE}" + +if echo "${RESPONSE}" | grep -qE "\"label\"[[:space:]]*:[[:space:]]*\"${EXPECTED_LABEL}\""; then + echo "PASS" + exit 0 +fi + +echo "FAIL" +exit 1 diff --git a/src/livepeer_gateway/runner/pipeline.py b/src/livepeer_gateway/runner/pipeline.py index b5b0fdc..fbf810f 100644 --- a/src/livepeer_gateway/runner/pipeline.py +++ b/src/livepeer_gateway/runner/pipeline.py @@ -7,6 +7,13 @@ class Pipeline(ABC): """Base class for batch inference pipelines.""" + def setup(self) -> None: + """Hook called once before serve() accepts requests. + + Override to load model weights, warm up GPUs, allocate buffers. + Default: no-op for stateless pipelines. + """ + @abstractmethod def predict(self, **kwargs: Any) -> Any: """Run one inference; kwargs are the JSON request body fields. diff --git a/src/livepeer_gateway/runner/serve.py b/src/livepeer_gateway/runner/serve.py index 65446e9..eba677e 100644 --- a/src/livepeer_gateway/runner/serve.py +++ b/src/livepeer_gateway/runner/serve.py @@ -52,7 +52,12 @@ async def handle_health(_: web.Request) -> web.Response: def make_app(pipeline: Pipeline) -> web.Application: - """Build an aiohttp application exposing ``pipeline`` over HTTP.""" + """Build an aiohttp application exposing ``pipeline`` over HTTP. + + Calls ``pipeline.setup()`` synchronously before binding routes, so the + server only starts accepting requests once the pipeline is initialised. + """ + pipeline.setup() app = web.Application() app["pipeline"] = pipeline app.router.add_post("/predict", handle_predict) From 8fec59f9173b0944e3bd6e9a172ebeafdb6642f1 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Wed, 29 Apr 2026 09:32:31 +0200 Subject: [PATCH 03/13] chore: add TODO.md for repo-level follow-ups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tracks operational items not suited to code comments — examples extraction trigger, BYOC offchain compose cleanup pending #3906, SDK feature gaps mapped to planned commits, related upstream PRs. Working surface, drained as items land. Co-Authored-By: Claude Opus 4.7 (1M context) --- TODO.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..befcebb --- /dev/null +++ b/TODO.md @@ -0,0 +1,75 @@ +# TODO + +Repo-level follow-ups that don't belong in code comments or spec docs. +Pending operational decisions and triggers for future work. + +## Examples directory + +- [ ] **Extract `examples/runner/` to a dedicated repo + (`livepeer/livepeer-pipeline-examples`)** when any of these triggers hit: + - 3+ runner examples in this repo + - First multi-GB-model example (image gen, LLM, real-time video) — CI + cost becomes painful + - First community contribution + - Cog precedent: `replicate/cog-examples` lives separately from `cog`. + + Keep `hello_world` in this repo as the CI smoke test even after extraction. + Each example in the new repo pins to a specific `livepeer-gateway` version. + +## BYOC offchain support + +- [ ] **Drop `-network arbitrum-one-mainnet`, `-ethUrl`, `-ethPassword` + from example compose files** once + [livepeer/go-livepeer#3906](https://github.com/livepeer/go-livepeer/pull/3906) + ships in `:master`. After that, examples can run with bare + `-network offchain`. Tracked: TODO comment already in each compose. + +## SDK round-trip + +- [ ] **Replace `test.sh`'s curl + base64 Livepeer header with a Python SDK + batch caller** once a batch caller lands (built on + [livepeer/livepeer-python-gateway#6](https://github.com/livepeer/livepeer-python-gateway/pull/6)'s + signing primitives). At that point the example compose can drop the + `gateway` service entirely — caller talks direct to the orchestrator via + the remote signer (per + [livepeer/go-livepeer#3869](https://github.com/livepeer/go-livepeer/pull/3869)). + Tracked: TODO comment in each example's `test.sh`. + +## SDK feature gaps + +- [ ] **Health state machine** (`LOADING / READY / ERROR / IDLE`) on + `/health` body. Currently `setup()` blocks the server bind, and the + example uses a docker compose healthcheck as a workaround. When the + state machine lands, drop the healthcheck from `sentiment/docker-compose.yml`. + +- [ ] **`Input()` / `Output()` typed descriptors** (C3 in the planned + staircase). Required before schema generation. + +- [ ] **Schema generation + `GET /openapi.json`** (C4). Inspects + `predict()` / `on_frame()` signature, emits OpenAPI JSON. + +- [ ] **`GET /` discovery doc** (C5). Points at schema URL, capability id, + version, supported transports. Cog parallel. + +- [ ] **`StreamPipeline` for trickle transport.** With `on_frame()` / + `on_video_frame()` / `on_audio_frame()`. Reuse existing trickle primitives + in `livepeer_gateway.transport`. + +- [ ] **SSE auto-detection** for generators. When `predict()` yields, + emit `text/event-stream`. + +- [ ] **`livepeer push` CLI** + `livepeer.yaml` manifest. Parses the + manifest, generates the Dockerfile (no more hand-written examples), + builds, registers. Drops the example Dockerfiles. + +- [ ] **Schema as Docker image label** (`org.livepeer.pipeline.schema`). + Lands with `livepeer push`. Removes the runtime `/openapi.json` as the + primary schema delivery; keeps it as a fallback. + +## Tracking related upstream work + +- go-livepeer offchain BYOC: [#3905](https://github.com/livepeer/go-livepeer/issues/3905) / + [#3906](https://github.com/livepeer/go-livepeer/pull/3906) +- BYOC remote signer: [#3869](https://github.com/livepeer/go-livepeer/pull/3869) +- Caller-side BYOC SDK: [#6](https://github.com/livepeer/livepeer-python-gateway/pull/6) +- This SDK's draft PR: [#7](https://github.com/livepeer/livepeer-python-gateway/pull/7) From 4a99b9c2998caee0a8373c87c1b4230dd0dba053 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Wed, 29 Apr 2026 10:14:31 +0200 Subject: [PATCH 04/13] feat(runner): migrate HTTP layer from aiohttp to FastAPI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the aiohttp serve layer with FastAPI + uvicorn. Pipeline API unchanged — Pipeline.predict() and Pipeline.setup() behave identically. Free additions from FastAPI: - GET /docs (Swagger UI) - GET /redoc - GET /openapi.json (minimal until Input/Output land) Handler dispatch: - /predict and /health are sync def, so pipeline.predict() (CPU/GPU bound) runs in FastAPI's threadpool and never blocks the event loop. - Request body parsed via Body(...) — framework handles JSON parse errors and dict-type validation, returning HTTP 422. Notes: - Error response shape changes from {"error": ...} to {"detail": ...}. Body validation errors return 422 (was 400 in aiohttp). Other status codes unchanged: TypeError on wrong predict() kwargs → 400; pipeline exceptions → 500. - aiohttp stays in deps; livepeer_gateway.transport's trickle client uses aiohttp.ClientSession. FastAPI server + aiohttp client coexist. Refs livepeer/livepeer-python-gateway#8 (C3) --- pyproject.toml | 2 + src/livepeer_gateway/runner/serve.py | 76 ++-- uv.lock | 606 +++++++++++++++++++++++++++ 3 files changed, 632 insertions(+), 52 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index afa0751..dfbc413 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,8 @@ dependencies = [ "protobuf>=4.25.0", "aiohttp>=3.9.0", "av>=11.0.0", + "fastapi>=0.115.0", + "uvicorn[standard]>=0.30.0", ] [project.scripts] diff --git a/src/livepeer_gateway/runner/serve.py b/src/livepeer_gateway/runner/serve.py index eba677e..04ecd56 100644 --- a/src/livepeer_gateway/runner/serve.py +++ b/src/livepeer_gateway/runner/serve.py @@ -3,68 +3,40 @@ import logging from typing import Any -from aiohttp import web +import uvicorn +from fastapi import Body, FastAPI, HTTPException from .pipeline import Pipeline _LOG = logging.getLogger(__name__) -async def handle_predict(request: web.Request) -> web.Response: - """Run one inference. - - Body is JSON; passed as kwargs to ``pipeline.predict()``. Returns the - result as JSON. ``TypeError`` from ``predict()`` becomes HTTP 400; - other exceptions become 500. - """ - pipeline: Pipeline = request.app["pipeline"] - - try: - body = await request.json() - except Exception as exc: - return web.json_response( - {"error": f"invalid JSON body: {exc}"}, - status=400, - ) - if not isinstance(body, dict): - return web.json_response( - {"error": "request body must be a JSON object"}, - status=400, - ) - - try: - result: Any = pipeline.predict(**body) - except TypeError as exc: - return web.json_response( - {"error": f"input mismatch: {exc}"}, - status=400, - ) - except Exception: - _LOG.exception("predict() failed") - return web.json_response({"error": "internal error"}, status=500) - - return web.json_response(result) - - -async def handle_health(_: web.Request) -> web.Response: - """Health probe. Returns ``{"status": "ready"}``.""" - return web.json_response({"status": "ready"}) - +def make_app(pipeline: Pipeline) -> FastAPI: + """Build a FastAPI app exposing ``pipeline`` over HTTP.""" + pipeline.setup() -def make_app(pipeline: Pipeline) -> web.Application: - """Build an aiohttp application exposing ``pipeline`` over HTTP. + app = FastAPI(title=type(pipeline).__name__) + app.state.pipeline = pipeline + + @app.post("/predict", summary="Run one inference") + def handle_predict(body: dict = Body(...)) -> Any: + try: + return pipeline.predict(**body) + except TypeError as exc: + raise HTTPException(status_code=400, detail=f"input mismatch: {exc}") + except HTTPException: + raise + except Exception: + _LOG.exception("predict() failed") + raise HTTPException(status_code=500, detail="internal error") + + @app.get("/health", summary="Liveness probe") + def handle_health() -> dict: + return {"status": "ready"} - Calls ``pipeline.setup()`` synchronously before binding routes, so the - server only starts accepting requests once the pipeline is initialised. - """ - pipeline.setup() - app = web.Application() - app["pipeline"] = pipeline - app.router.add_post("/predict", handle_predict) - app.router.add_get("/health", handle_health) return app def serve(pipeline: Pipeline, *, host: str = "0.0.0.0", port: int = 5000) -> None: """Run the pipeline as an HTTP server on host:port.""" - web.run_app(make_app(pipeline), host=host, port=port) + uvicorn.run(make_app(pipeline), host=host, port=port) diff --git a/uv.lock b/uv.lock index 4003c14..bb8d731 100644 --- a/uv.lock +++ b/uv.lock @@ -148,6 +148,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + [[package]] name = "async-timeout" version = "5.0.1" @@ -223,6 +255,55 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/41/7f13361db54d7e02f11552575c0384dadaf0918138f4eaa82ea03a9f9580/av-16.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6f90dc082ff2068ddbe77618400b44d698d25d9c4edac57459e250c16b33d700", size = 31948164, upload-time = "2026-01-11T09:59:19.501Z" }, ] +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "fastapi" +version = "0.136.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/45/c130091c2dfa061bbfe3150f2a5091ef1adf149f2a8d2ae769ecaf6e99a2/fastapi-0.136.1.tar.gz", hash = "sha256:7af665ad7acfa0a3baf8983d393b6b471b9da10ede59c60045f49fbc89a0fa7f", size = 397448, upload-time = "2026-04-23T16:49:44.046Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" }, +] + [[package]] name = "frozenlist" version = "1.8.0" @@ -468,6 +549,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/95/4d/31236cddb7ffb09ba4a49f4f56d2608fec3bbb21c7a0a975d93bca7cd22e/grpcio_tools-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:2ccd2c8d041351cc29d0fc4a84529b11ee35494a700b535c1f820b642f2a72fc", size = 1190242, upload-time = "2025-10-21T16:26:25.296Z" }, ] +[[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 = "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/c7/e5/c07e0bcf4ec8db8164e9f6738c048b2e66aabf30e7506f440c4cc6953f60/httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78", size = 204531, upload-time = "2025-10-10T03:54:20.887Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/35e3a63f863a659f92ffd92bef131f3e81cf849af26e6435b49bd9f6f751/httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4", size = 109408, upload-time = "2025-10-10T03:54:22.455Z" }, + { url = "https://files.pythonhosted.org/packages/f5/71/b0a9193641d9e2471ac541d3b1b869538a5fb6419d52fd2669fa9c79e4b8/httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05", size = 440889, upload-time = "2025-10-10T03:54:23.753Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d9/2e34811397b76718750fea44658cb0205b84566e895192115252e008b152/httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed", size = 440460, upload-time = "2025-10-10T03:54:25.313Z" }, + { url = "https://files.pythonhosted.org/packages/01/3f/a04626ebeacc489866bb4d82362c0657b2262bef381d68310134be7f40bb/httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a", size = 425267, upload-time = "2025-10-10T03:54:26.81Z" }, + { url = "https://files.pythonhosted.org/packages/a5/99/adcd4f66614db627b587627c8ad6f4c55f18881549bab10ecf180562e7b9/httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b", size = 424429, upload-time = "2025-10-10T03:54:28.174Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/ec8fc904a8fd30ba022dfa85f3bbc64c3c7cd75b669e24242c0658e22f3c/httptools-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568", size = 86173, upload-time = "2025-10-10T03:54:29.5Z" }, + { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { 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 = "idna" version = "3.11" @@ -484,8 +617,10 @@ source = { editable = "." } dependencies = [ { name = "aiohttp" }, { name = "av" }, + { name = "fastapi" }, { name = "grpcio" }, { name = "protobuf" }, + { name = "uvicorn", extra = ["standard"] }, ] [package.optional-dependencies] @@ -502,11 +637,13 @@ examples = [ requires-dist = [ { name = "aiohttp", specifier = ">=3.9.0" }, { name = "av", specifier = ">=11.0.0" }, + { name = "fastapi", specifier = ">=0.115.0" }, { name = "grpcio", specifier = ">=1.65.0" }, { name = "grpcio-tools", marker = "extra == 'dev'", specifier = ">=1.65.0" }, { name = "numpy", marker = "extra == 'examples'", specifier = ">=2.2.6" }, { name = "opencv-python-headless", marker = "extra == 'examples'", specifier = ">=4.13.0.90" }, { name = "protobuf", specifier = ">=4.25.0" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.30.0" }, ] provides-extras = ["dev", "examples"] @@ -943,6 +1080,210 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/15/4f02896cc3df04fc465010a4c6a0cd89810f54617a32a70ef531ed75d61c/protobuf-6.33.2-py3-none-any.whl", hash = "sha256:7636aad9bb01768870266de5dc009de2d1b936771b38a793f73cbbf279c91c5c", size = 170501, upload-time = "2025-12-06T00:17:52.211Z" }, ] +[[package]] +name = "pydantic" +version = "2.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/98/b50eb9a411e87483b5c65dba4fa430a06bac4234d3403a40e5a9905ebcd0/pydantic_core-2.46.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1da3786b8018e60349680720158cc19161cc3b4bdd815beb0a321cd5ce1ad5b1", size = 2108971, upload-time = "2026-04-20T14:43:51.945Z" }, + { url = "https://files.pythonhosted.org/packages/08/4b/f364b9d161718ff2217160a4b5d41ce38de60aed91c3689ebffa1c939d23/pydantic_core-2.46.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc0988cb29d21bf4a9d5cf2ef970b5c0e38d8d8e107a493278c05dc6c1dda69f", size = 1949588, upload-time = "2026-04-20T14:44:10.386Z" }, + { url = "https://files.pythonhosted.org/packages/8f/8b/30bd03ee83b2f5e29f5ba8e647ab3c456bf56f2ec72fdbcc0215484a0854/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f9067c3bfadd04c55484b89c0d267981b2f3512850f6f66e1e74204a4e4ce3", size = 1975986, upload-time = "2026-04-20T14:43:57.106Z" }, + { url = "https://files.pythonhosted.org/packages/3c/54/13ccf954d84ec275d5d023d5786e4aa48840bc9f161f2838dc98e1153518/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a642ac886ecf6402d9882d10c405dcf4b902abeb2972cd5fb4a48c83cd59279a", size = 2055830, upload-time = "2026-04-20T14:44:15.499Z" }, + { url = "https://files.pythonhosted.org/packages/be/0e/65f38125e660fdbd72aa858e7dfae893645cfa0e7b13d333e174a367cd23/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79f561438481f28681584b89e2effb22855e2179880314bcddbf5968e935e807", size = 2222340, upload-time = "2026-04-20T14:41:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/d1/88/f3ab7739efe0e7e80777dbb84c59eb98518e3f57ea433206194c2e425272/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57a973eae4665352a47cf1a99b4ee864620f2fe663a217d7a8da68a1f3a5bfda", size = 2280727, upload-time = "2026-04-20T14:41:30.461Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6d/c228219080817bec4982f9531cadb18da6aaa770fdeb114f49c237ac2c9f/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83d002b97072a53ea150d63e0a3adfae5670cef5aa8a6e490240e482d3b22e57", size = 2092158, upload-time = "2026-04-20T14:44:07.305Z" }, + { url = "https://files.pythonhosted.org/packages/0f/b1/525a16711e7c6d61635fac3b0bd54600b5c5d9f60c6fc5aaab26b64a2297/pydantic_core-2.46.3-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b40ddd51e7c44b28cfaef746c9d3c506d658885e0a46f9eeef2ee815cbf8e045", size = 2116626, upload-time = "2026-04-20T14:42:34.118Z" }, + { url = "https://files.pythonhosted.org/packages/ef/7c/17d30673351439a6951bf54f564cf2443ab00ae264ec9df00e2efd710eb5/pydantic_core-2.46.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac5ec7fb9b87f04ee839af2d53bcadea57ded7d229719f56c0ed895bff987943", size = 2160691, upload-time = "2026-04-20T14:41:14.023Z" }, + { url = "https://files.pythonhosted.org/packages/86/66/af8adbcbc0886ead7f1a116606a534d75a307e71e6e08226000d51b880d2/pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a3b11c812f61b3129c4905781a2601dfdfdea5fe1e6c1cfb696b55d14e9c054f", size = 2182543, upload-time = "2026-04-20T14:40:48.886Z" }, + { url = "https://files.pythonhosted.org/packages/b0/37/6de71e0f54c54a4190010f57deb749e1ddf75c568ada3b1320b70067f121/pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1108da631e602e5b3c38d6d04fe5bb3bfa54349e6918e3ca6cf570b2e2b2f9d4", size = 2324513, upload-time = "2026-04-20T14:42:36.121Z" }, + { url = "https://files.pythonhosted.org/packages/51/b1/9fc74ce94f603d5ef59ff258ca9c2c8fb902fb548d340a96f77f4d1c3b7f/pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:de885175515bcfa98ae618c1df7a072f13d179f81376c8007112af20567fd08a", size = 2361853, upload-time = "2026-04-20T14:43:24.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/d0/4c652fc592db35f100279ee751d5a145aca1b9a7984b9684ba7c1b5b0535/pydantic_core-2.46.3-cp310-cp310-win32.whl", hash = "sha256:d11058e3201527d41bc6b545c79187c9e4bf85e15a236a6007f0e991518882b7", size = 1980465, upload-time = "2026-04-20T14:44:46.239Z" }, + { url = "https://files.pythonhosted.org/packages/27/b8/a920453c38afbe1f355e1ea0b0d94a0a3e0b0879d32d793108755fa171d5/pydantic_core-2.46.3-cp310-cp310-win_amd64.whl", hash = "sha256:3612edf65c8ea67ac13616c4d23af12faef1ae435a8a93e5934c2a0cbbdd1fd6", size = 2073884, upload-time = "2026-04-20T14:43:01.201Z" }, + { url = "https://files.pythonhosted.org/packages/22/a2/1ba90a83e85a3f94c796b184f3efde9c72f2830dcda493eea8d59ba78e6d/pydantic_core-2.46.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5", size = 2106740, upload-time = "2026-04-20T14:41:20.932Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f6/99ae893c89a0b9d3daec9f95487aa676709aa83f67643b3f0abaf4ab628a/pydantic_core-2.46.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c", size = 1948293, upload-time = "2026-04-20T14:43:42.115Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b8/2e8e636dc9e3f16c2e16bf0849e24be82c5ee82c603c65fc0326666328fc/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e", size = 1973222, upload-time = "2026-04-20T14:41:57.841Z" }, + { url = "https://files.pythonhosted.org/packages/34/36/0e730beec4d83c5306f417afbd82ff237d9a21e83c5edf675f31ed84c1fe/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287", size = 2053852, upload-time = "2026-04-20T14:40:43.077Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f0/3071131f47e39136a17814576e0fada9168569f7f8c0e6ac4d1ede6a4958/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe", size = 2221134, upload-time = "2026-04-20T14:43:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a9/a2dc023eec5aa4b02a467874bad32e2446957d2adcab14e107eab502e978/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050", size = 2279785, upload-time = "2026-04-20T14:41:19.285Z" }, + { url = "https://files.pythonhosted.org/packages/0a/44/93f489d16fb63fbd41c670441536541f6e8cfa1e5a69f40bc9c5d30d8c90/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2", size = 2089404, upload-time = "2026-04-20T14:43:10.108Z" }, + { url = "https://files.pythonhosted.org/packages/2a/78/8692e3aa72b2d004f7a5d937f1dfdc8552ba26caf0bec75f342c40f00dec/pydantic_core-2.46.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa", size = 2114898, upload-time = "2026-04-20T14:44:51.475Z" }, + { url = "https://files.pythonhosted.org/packages/6a/62/e83133f2e7832532060175cebf1f13748f4c7e7e7165cdd1f611f174494b/pydantic_core-2.46.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c", size = 2157856, upload-time = "2026-04-20T14:43:46.64Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ec/6a500e3ad7718ee50583fae79c8651f5d37e3abce1fa9ae177ae65842c53/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf", size = 2180168, upload-time = "2026-04-20T14:42:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/d8/53/8267811054b1aa7fc1dc7ded93812372ef79a839f5e23558136a6afbfde1/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b", size = 2322885, upload-time = "2026-04-20T14:41:05.253Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c1/1c0acdb3aa0856ddc4ecc55214578f896f2de16f400cf51627eb3c26c1c4/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e", size = 2360328, upload-time = "2026-04-20T14:41:43.991Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/ef39cd0f4a926814f360e71c1adeab48ad214d9727e4deb48eedfb5bce1a/pydantic_core-2.46.3-cp311-cp311-win32.whl", hash = "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb", size = 1979464, upload-time = "2026-04-20T14:43:12.215Z" }, + { url = "https://files.pythonhosted.org/packages/18/9c/f41951b0d858e343f1cf09398b2a7b3014013799744f2c4a8ad6a3eec4f2/pydantic_core-2.46.3-cp311-cp311-win_amd64.whl", hash = "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346", size = 2070837, upload-time = "2026-04-20T14:41:47.707Z" }, + { url = "https://files.pythonhosted.org/packages/9f/1e/264a17cd582f6ed50950d4d03dd5fefd84e570e238afe1cb3e25cf238769/pydantic_core-2.46.3-cp311-cp311-win_arm64.whl", hash = "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6", size = 2053647, upload-time = "2026-04-20T14:42:27.535Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" }, + { url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" }, + { url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" }, + { url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" }, + { url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" }, + { url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" }, + { url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" }, + { url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" }, + { url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" }, + { url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" }, + { url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" }, + { url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" }, + { url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" }, + { url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" }, + { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" }, + { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" }, + { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" }, + { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" }, + { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" }, + { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" }, + { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" }, + { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" }, + { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" }, + { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/66/7f/03dbad45cd3aa9083fbc93c210ae8b005af67e4136a14186950a747c6874/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46", size = 2105683, upload-time = "2026-04-20T14:42:19.779Z" }, + { url = "https://files.pythonhosted.org/packages/26/22/4dc186ac8ea6b257e9855031f51b62a9637beac4d68ac06bee02f046f836/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874", size = 1940052, upload-time = "2026-04-20T14:43:59.274Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/d376391a5aff1f2e8188960d7873543608130a870961c2b6b5236627c116/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76", size = 1988172, upload-time = "2026-04-20T14:41:17.469Z" }, + { url = "https://files.pythonhosted.org/packages/0e/6b/523b9f85c23788755d6ab949329de692a2e3a584bc6beb67fef5e035aa9d/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531", size = 2128596, upload-time = "2026-04-20T14:40:41.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" }, + { url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" }, + { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" }, + { url = "https://files.pythonhosted.org/packages/1f/da/99d40830684f81dec901cac521b5b91c095394cc1084b9433393cde1c2df/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25", size = 2107973, upload-time = "2026-04-20T14:42:06.175Z" }, + { url = "https://files.pythonhosted.org/packages/99/a5/87024121818d75bbb2a98ddbaf638e40e7a18b5e0f5492c9ca4b1b316107/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3", size = 1947191, upload-time = "2026-04-20T14:43:14.319Z" }, + { url = "https://files.pythonhosted.org/packages/60/62/0c1acfe10945b83a6a59d19fbaa92f48825381509e5701b855c08f13db76/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536", size = 2123791, upload-time = "2026-04-20T14:43:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/3b2393b4c8f44285561dc30b00cf307a56a2eff7c483a824db3b8221ca51/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1", size = 2153197, upload-time = "2026-04-20T14:44:27.932Z" }, + { url = "https://files.pythonhosted.org/packages/ba/75/5af02fb35505051eee727c061f2881c555ab4f8ddb2d42da715a42c9731b/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c", size = 2181073, upload-time = "2026-04-20T14:43:20.729Z" }, + { url = "https://files.pythonhosted.org/packages/10/92/7e0e1bd9ca3c68305db037560ca2876f89b2647deb2f8b6319005de37505/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85", size = 2315886, upload-time = "2026-04-20T14:44:04.826Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d8/101655f27eaf3e44558ead736b2795d12500598beed4683f279396fa186e/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8", size = 2360528, upload-time = "2026-04-20T14:40:47.431Z" }, + { url = "https://files.pythonhosted.org/packages/07/0f/1c34a74c8d07136f0d729ffe5e1fdab04fbdaa7684f61a92f92511a84a15/pydantic_core-2.46.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff", size = 2184144, upload-time = "2026-04-20T14:42:57Z" }, +] + +[[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" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { 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 = "setuptools" version = "80.9.0" @@ -952,6 +1293,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, ] +[[package]] +name = "starlette" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -961,6 +1315,258 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.46.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/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/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" }, + { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { 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 = "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/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + +[[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/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, + { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, + { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { 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/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { 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 = "yarl" version = "1.22.0" From 961190bb870dbd62f29b67d506d561bf04592d69 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Wed, 29 Apr 2026 12:24:10 +0200 Subject: [PATCH 05/13] chore(examples): publish runner port + update docs Switches expose: to ports: so /docs, /redoc, and /openapi.json are browsable on http://localhost:5000 during dev. Example READMEs updated. --- examples/runner/hello_world/README.md | 6 ++++++ examples/runner/hello_world/docker-compose.yml | 4 ++-- examples/runner/sentiment/README.md | 6 ++++++ examples/runner/sentiment/docker-compose.yml | 4 ++-- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/examples/runner/hello_world/README.md b/examples/runner/hello_world/README.md index e50e993..b7caa17 100644 --- a/examples/runner/hello_world/README.md +++ b/examples/runner/hello_world/README.md @@ -15,6 +15,12 @@ docker compose down `test.sh` prints `PASS` on success. +## Browse the API + +- Swagger UI: +- ReDoc: +- OpenAPI JSON: + ## What's running ```mermaid diff --git a/examples/runner/hello_world/docker-compose.yml b/examples/runner/hello_world/docker-compose.yml index 7675a4a..5cd238a 100644 --- a/examples/runner/hello_world/docker-compose.yml +++ b/examples/runner/hello_world/docker-compose.yml @@ -42,8 +42,8 @@ services: context: ../../.. dockerfile: examples/runner/hello_world/Dockerfile container_name: hello_world - expose: - - "5000" + ports: + - "5000:5000" depends_on: - orchestrator diff --git a/examples/runner/sentiment/README.md b/examples/runner/sentiment/README.md index 6293ef0..28b7e00 100644 --- a/examples/runner/sentiment/README.md +++ b/examples/runner/sentiment/README.md @@ -21,6 +21,12 @@ docker compose down > **First build is ~5 minutes** — pulls torch CPU (~200 MB), transformers, and > bakes the ~250 MB model into the image. Cached after that; rebuilds are fast. +## Browse the API + +- Swagger UI: +- ReDoc: +- OpenAPI JSON: + ## What's running ```mermaid diff --git a/examples/runner/sentiment/docker-compose.yml b/examples/runner/sentiment/docker-compose.yml index 33f0bb9..2e62a7f 100644 --- a/examples/runner/sentiment/docker-compose.yml +++ b/examples/runner/sentiment/docker-compose.yml @@ -42,8 +42,8 @@ services: context: ../../.. dockerfile: examples/runner/sentiment/Dockerfile container_name: sentiment - expose: - - "5000" + ports: + - "5000:5000" # Healthcheck waits for setup() to finish loading the model before the # orchestrator can route requests here. Without this, register_capability # could complete and the test could fire before the model is loaded. From dbf711e9ae2265a0e3df4611381a6f231cab0b88 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Wed, 29 Apr 2026 12:24:11 +0200 Subject: [PATCH 06/13] chore: ignore IDE / editor artifacts --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 540f0e2..e95e11a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,9 @@ lp_rpc.proto # environment file env + +# IDE +.vscode/ +*.code-workspace +.idea/ +.codex From 0da5fb79db6c4d892b241f94569e169f3bdfdef1 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Wed, 29 Apr 2026 14:48:55 +0200 Subject: [PATCH 07/13] feat(runner): typed inputs/outputs via Pydantic + signature introspection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit predict()'s signature drives FastAPI's body type and response model. Two paths: - Explicit BaseModel param: pass body to predict() directly - Bare typed params: auto-derive a Pydantic model via create_model and unpack as kwargs OpenAPI now reflects real types — /docs shows declared fields with descriptions, examples, constraints, and typed responses when the return annotation is a BaseModel. Refs livepeer/livepeer-python-gateway#8 (C4) --- examples/runner/sentiment/pipeline.py | 30 ++++++++--- src/livepeer_gateway/runner/serve.py | 76 ++++++++++++++++++++++----- 2 files changed, 85 insertions(+), 21 deletions(-) diff --git a/examples/runner/sentiment/pipeline.py b/examples/runner/sentiment/pipeline.py index 0e45d6f..a0fedde 100644 --- a/examples/runner/sentiment/pipeline.py +++ b/examples/runner/sentiment/pipeline.py @@ -1,8 +1,22 @@ """Sentiment-analysis BYOC pipeline. Run via ``docker compose up`` — see README.md.""" -from livepeer_gateway.runner import Pipeline, serve +from typing import Literal + +from pydantic import BaseModel, Field from transformers import pipeline as hf_pipeline +from livepeer_gateway.runner import Pipeline, serve + + +class SentimentInput(BaseModel): + text: str = Field(description="Text to classify", examples=["I love this!"]) + + +class SentimentOutput(BaseModel): + label: Literal["POSITIVE", "NEGATIVE"] + score: float = Field(ge=0.0, le=1.0) + text: str + class SentimentAnalyzer(Pipeline): def setup(self): @@ -12,13 +26,13 @@ def setup(self): model="distilbert-base-uncased-finetuned-sst-2-english", ) - def predict(self, text: str = "Livepeer is great") -> dict: - result = self.model(text)[0] - return { - "label": result["label"], - "score": float(result["score"]), - "text": text, - } + def predict(self, params: SentimentInput) -> SentimentOutput: + result = self.model(params.text)[0] + return SentimentOutput( + label=result["label"], + score=float(result["score"]), + text=params.text, + ) if __name__ == "__main__": diff --git a/src/livepeer_gateway/runner/serve.py b/src/livepeer_gateway/runner/serve.py index 04ecd56..dfa9c91 100644 --- a/src/livepeer_gateway/runner/serve.py +++ b/src/livepeer_gateway/runner/serve.py @@ -1,35 +1,85 @@ -from __future__ import annotations - +import inspect import logging from typing import Any import uvicorn -from fastapi import Body, FastAPI, HTTPException +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel, create_model from .pipeline import Pipeline _LOG = logging.getLogger(__name__) -def make_app(pipeline: Pipeline) -> FastAPI: - """Build a FastAPI app exposing ``pipeline`` over HTTP.""" - pipeline.setup() +def _is_basemodel(t: Any) -> bool: + return isinstance(t, type) and issubclass(t, BaseModel) + + +def _build_input_model(predict_fn: Any, owner_name: str) -> tuple[type[BaseModel], bool]: + """Inspect predict()'s signature; return (InputModel, is_explicit_basemodel). + + If predict() takes a single ``BaseModel`` parameter, use it directly. + Otherwise build a model from the bare parameters via ``create_model``. + """ + sig = inspect.signature(predict_fn) + params = [param for param in sig.parameters.values() if param.name != "self"] + + if len(params) == 1 and _is_basemodel(params[0].annotation): + return params[0].annotation, True + + fields: dict[str, tuple[Any, Any]] = {} + for param in params: + annotation = param.annotation if param.annotation is not inspect.Parameter.empty else Any + default = param.default if param.default is not inspect.Parameter.empty else ... + fields[param.name] = (annotation, default) + + return create_model(f"{owner_name}Input", **fields), False - app = FastAPI(title=type(pipeline).__name__) - app.state.pipeline = pipeline - @app.post("/predict", summary="Run one inference") - def handle_predict(body: dict = Body(...)) -> Any: +def _build_predict_handler( + pipeline: Pipeline, + InputModel: type[BaseModel], + OutputModel: type[BaseModel] | None, + explicit_basemodel: bool, +): + def handler(body: InputModel): try: - return pipeline.predict(**body) - except TypeError as exc: - raise HTTPException(status_code=400, detail=f"input mismatch: {exc}") + if explicit_basemodel: + return pipeline.predict(body) + return pipeline.predict(**body.model_dump()) except HTTPException: raise except Exception: _LOG.exception("predict() failed") raise HTTPException(status_code=500, detail="internal error") + if OutputModel is not None: + handler.__annotations__["return"] = OutputModel + return handler + + +def make_app(pipeline: Pipeline) -> FastAPI: + """Build a FastAPI app exposing ``pipeline`` over HTTP.""" + pipeline.setup() + + InputModel, explicit_basemodel = _build_input_model( + pipeline.predict, type(pipeline).__name__ + ) + return_annotation = inspect.signature(pipeline.predict).return_annotation + OutputModel = return_annotation if _is_basemodel(return_annotation) else None + + handler = _build_predict_handler(pipeline, InputModel, OutputModel, explicit_basemodel) + + app = FastAPI(title=type(pipeline).__name__) + app.state.pipeline = pipeline + + app.add_api_route( + "/predict", + handler, + methods=["POST"], + summary="Run one inference", + ) + @app.get("/health", summary="Liveness probe") def handle_health() -> dict: return {"status": "ready"} From fdc5fedd6820d058d9c90e6eb2f03a9d09a3173d Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Wed, 29 Apr 2026 15:35:15 +0200 Subject: [PATCH 08/13] feat(examples): image-upscale demonstrates binary I/O via Pydantic Base64Bytes Swin2SR x2 super-resolution as a BYOC capability. Input image is a base64-encoded JPEG/PNG; output is a base64-encoded PNG. Pydantic's Base64Bytes auto-decodes the request body to bytes, so the pipeline gets bytes directly and the SDK ships zero binary-handling code. Refs livepeer/livepeer-python-gateway#8 (C5) --- examples/runner/image_upscale/Dockerfile | 26 +++++ examples/runner/image_upscale/README.md | 101 ++++++++++++++++++ .../runner/image_upscale/docker-compose.yml | 79 ++++++++++++++ examples/runner/image_upscale/pipeline.py | 54 ++++++++++ .../runner/image_upscale/prepare_models.py | 11 ++ .../image_upscale/register_capability.py | 52 +++++++++ .../runner/image_upscale/requirements.txt | 6 ++ examples/runner/image_upscale/test.sh | 48 +++++++++ examples/runner/image_upscale/test_image.png | Bin 0 -> 138 bytes 9 files changed, 377 insertions(+) create mode 100644 examples/runner/image_upscale/Dockerfile create mode 100644 examples/runner/image_upscale/README.md create mode 100644 examples/runner/image_upscale/docker-compose.yml create mode 100644 examples/runner/image_upscale/pipeline.py create mode 100644 examples/runner/image_upscale/prepare_models.py create mode 100644 examples/runner/image_upscale/register_capability.py create mode 100644 examples/runner/image_upscale/requirements.txt create mode 100755 examples/runner/image_upscale/test.sh create mode 100644 examples/runner/image_upscale/test_image.png diff --git a/examples/runner/image_upscale/Dockerfile b/examples/runner/image_upscale/Dockerfile new file mode 100644 index 0000000..8481785 --- /dev/null +++ b/examples/runner/image_upscale/Dockerfile @@ -0,0 +1,26 @@ +FROM python:3.11-slim + +WORKDIR /app + +# SDK install (in-repo source until livepeer-gateway publishes; will collapse +# to a single `pip install livepeer-gateway` line once on PyPI). +COPY pyproject.toml README ./ +COPY src /app/src +RUN pip install --no-cache-dir /app + +# Pipeline-specific deps. The requirements.txt sets --extra-index-url to +# pull the CPU-only torch wheel (~200 MB vs ~5 GB for the CUDA variant). +COPY examples/runner/image_upscale/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Bake model weights at build time so setup() loads from local disk in +# milliseconds. +COPY examples/runner/image_upscale/prepare_models.py /app/prepare_models.py +RUN python /app/prepare_models.py + +# Pipeline code last so edits don't invalidate the bake layer above. +COPY examples/runner/image_upscale/pipeline.py /app/pipeline.py + +EXPOSE 5000 + +CMD ["python", "/app/pipeline.py"] diff --git a/examples/runner/image_upscale/README.md b/examples/runner/image_upscale/README.md new file mode 100644 index 0000000..47e331d --- /dev/null +++ b/examples/runner/image_upscale/README.md @@ -0,0 +1,101 @@ +# Image upscale (BYOC) + +A ~2x image super-resolution BYOC capability — proves the SDK handles binary +I/O cleanly via Pydantic's `Base64Bytes`. Built on +[Swin2SR](https://huggingface.co/caidas/swin2SR-classical-sr-x2-64), small +enough to run on CPU. + +A `Pipeline` subclass loads the model once in `setup()`, then takes a +base64-encoded image on each `POST /predict` and returns the upscaled PNG. +The processor pads inputs to its window size before upscaling, so output +dimensions are at least 2x input but may be slightly larger. Registered as +a BYOC capability, called through the gateway, response flows back +end-to-end. + +## Run + +```bash +docker compose up -d --wait +./test.sh +docker compose down +``` + +`test.sh` prints `PASS` on success. + +> **First build is ~5 minutes** — pulls torch CPU (~200 MB), transformers, and +> bakes the ~70 MB Swin2SR model into the image. Cached after that. + +## Browse the API + +- Swagger UI: +- ReDoc: +- OpenAPI JSON: + +## What's running + +```mermaid +sequenceDiagram + autonumber + participant curl + participant gateway + participant orchestrator + participant image_upscale as image_upscale
(SDK container) + + curl->>gateway: POST /process/request/predict + gateway->>orchestrator: forward (Livepeer-signed) + orchestrator->>image_upscale: POST /predict {"image":""} + image_upscale-->>orchestrator: {"image":"","width":W,"height":H} + orchestrator-->>gateway: response + gateway-->>curl: response +``` + +Four compose services: + +| Service | What it is | +| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| `gateway`, `orchestrator` | `livepeer/go-livepeer:master` from Docker Hub | +| `image_upscale` | The pipeline container — a [BYOC](https://github.com/livepeer/go-livepeer/blob/main/doc/byoc.md) capability built with `livepeer_gateway.runner`. | +| `register_capability` | One-shot helper that POSTs to `orchestrator:8935/capability/register` once `image_upscale` is healthy | + +The pipeline service has a healthcheck that probes `GET /health` until the +model finishes loading. `register_capability` waits on `service_healthy`, so +the orchestrator never sees a "registered but not loaded" container. + +## Binary I/O contract + +Both `image` fields use Pydantic's `Base64Bytes`: + +- **Input** — `image` is a base64-encoded string in the JSON body. Pydantic + decodes to `bytes` before `predict()` runs. +- **Output** — `image` is `bytes` in the pipeline; Pydantic encodes back to + base64 in the JSON response. + +`width` and `height` are returned alongside for convenience. The pipeline +always emits PNG; document the format in the field description if you need +to surface it to callers. + +## Try with your own image + +```bash +TEST_IMAGE=/path/to/your.png \ +INPUT_WIDTH=$W INPUT_HEIGHT=$H \ +./test.sh +``` + +The test asserts output is at least 2x input dimensions. + +Or manually: + +```bash +INPUT_B64=$(base64 -w0 < your.png) + +LIVEPEER_HDR=$(printf '%s' \ + '{"request":"{}","parameters":"{}","capability":"image-upscale","timeout_seconds":60}' \ + | base64 -w0) + +curl -X POST http://localhost:9935/process/request/predict \ + -H "Livepeer: ${LIVEPEER_HDR}" \ + -H "Content-Type: application/json" \ + -d "{\"image\":\"${INPUT_B64}\"}" \ + | jq -r '.image' | base64 -d > upscaled.png +``` diff --git a/examples/runner/image_upscale/docker-compose.yml b/examples/runner/image_upscale/docker-compose.yml new file mode 100644 index 0000000..719d0d7 --- /dev/null +++ b/examples/runner/image_upscale/docker-compose.yml @@ -0,0 +1,79 @@ +services: + # Mirrors go-livepeer/doc/byoc.md: on-chain mode against a public Arbitrum + # RPC, but `pricePerUnit 0` keeps the registration free — no balance ever + # leaves the auto-generated keystore. No real chain interaction occurs. + # + # TODO: once livepeer/go-livepeer#3906 ships in :master, drop -network / + # -ethUrl / -ethPassword and run with bare `-network offchain`. Tracked + # in livepeer/go-livepeer#3905. + + orchestrator: + image: livepeer/go-livepeer:master + container_name: orchestrator + command: > + -network arbitrum-one-mainnet + -orchestrator + -ethUrl https://arb1.arbitrum.io/rpc + -ethPassword secret-password + -pricePerUnit 1 + -serviceAddr=orchestrator:8935 + -orchSecret=orch-secret + -v 6 + + gateway: + image: livepeer/go-livepeer:master + container_name: gateway + command: > + -network arbitrum-one-mainnet + -gateway + -ethUrl https://arb1.arbitrum.io/rpc + -ethPassword secret-password + -orchAddr=orchestrator:8935 + -httpAddr=0.0.0.0:9935 + -httpIngest + -v 6 + ports: + - "9935:9935" + depends_on: + - orchestrator + + image_upscale: + build: + context: ../../.. + dockerfile: examples/runner/image_upscale/Dockerfile + container_name: image_upscale + ports: + - "5000:5000" + # Healthcheck waits for setup() to finish loading the model before the + # orchestrator can route requests here. Without this, register_capability + # could complete and the test could fire before the model is loaded. + healthcheck: + test: ["CMD", "python", "-c", + "import urllib.request; urllib.request.urlopen('http://localhost:5000/health').read()"] + interval: 5s + timeout: 5s + retries: 30 + start_period: 60s + depends_on: + - orchestrator + + register_capability: + image: python:3.11-slim + container_name: register_capability + command: sh -c "pip install --quiet requests && python /app/register_capability.py" + volumes: + - ./register_capability.py:/app/register_capability.py:ro + environment: + ORCH_URL: https://orchestrator:8935 + ORCH_SECRET: orch-secret + CAPABILITY_NAME: image-upscale + CAPABILITY_URL: http://image_upscale:5000 + depends_on: + image_upscale: + condition: service_healthy + orchestrator: + condition: service_started + +networks: + default: + name: livepeer diff --git a/examples/runner/image_upscale/pipeline.py b/examples/runner/image_upscale/pipeline.py new file mode 100644 index 0000000..e3e3454 --- /dev/null +++ b/examples/runner/image_upscale/pipeline.py @@ -0,0 +1,54 @@ +"""Image upscale BYOC pipeline. Run via ``docker compose up`` — see README.md.""" + +import base64 +import io + +import numpy as np +import torch +from PIL import Image +from pydantic import Base64Bytes, BaseModel, Field +from transformers import Swin2SRForImageSuperResolution, Swin2SRImageProcessor + +from livepeer_gateway.runner import Pipeline, serve + + +class UpscaleInput(BaseModel): + image: Base64Bytes = Field(description="Source image, base64-encoded PNG/JPEG") + + +class UpscaleOutput(BaseModel): + image: str = Field(description="Upscaled image, base64-encoded PNG") + width: int + height: int + + +class ImageUpscaler(Pipeline): + def setup(self): + # Loads from local HF cache populated at Docker build time. + model_id = "caidas/swin2SR-classical-sr-x2-64" + self.processor = Swin2SRImageProcessor.from_pretrained(model_id) + self.model = Swin2SRForImageSuperResolution.from_pretrained(model_id) + + def predict(self, params: UpscaleInput) -> UpscaleOutput: + src = Image.open(io.BytesIO(params.image)).convert("RGB") + + inputs = self.processor(images=src, return_tensors="pt") + with torch.no_grad(): + outputs = self.model(**inputs) + + # CHW float [0, 1] → HWC uint8 + chw = outputs.reconstruction.squeeze().clamp(0, 1).cpu().numpy() + hwc = np.moveaxis(chw, 0, -1) + upscaled = Image.fromarray((hwc * 255.0).round().astype(np.uint8)) + + buf = io.BytesIO() + upscaled.save(buf, format="PNG") + return UpscaleOutput( + image=base64.b64encode(buf.getvalue()).decode(), + width=upscaled.width, + height=upscaled.height, + ) + + +if __name__ == "__main__": + serve(ImageUpscaler()) diff --git a/examples/runner/image_upscale/prepare_models.py b/examples/runner/image_upscale/prepare_models.py new file mode 100644 index 0000000..f0c8ca7 --- /dev/null +++ b/examples/runner/image_upscale/prepare_models.py @@ -0,0 +1,11 @@ +"""Download model weights into the local HF cache at build time. + +Invoked by the Dockerfile so ``setup()`` loads from local disk in +milliseconds instead of pulling from HF Hub on every container start. +""" + +from transformers import Swin2SRForImageSuperResolution, Swin2SRImageProcessor + +model_id = "caidas/swin2SR-classical-sr-x2-64" +Swin2SRImageProcessor.from_pretrained(model_id) +Swin2SRForImageSuperResolution.from_pretrained(model_id) diff --git a/examples/runner/image_upscale/register_capability.py b/examples/runner/image_upscale/register_capability.py new file mode 100644 index 0000000..bf3f320 --- /dev/null +++ b/examples/runner/image_upscale/register_capability.py @@ -0,0 +1,52 @@ +"""Register the image-upscale capability with the orchestrator. + +Wire format follows +https://github.com/livepeer/go-livepeer/blob/main/doc/byoc.md +(handler in ``byoc/job_orchestrator.go``). +""" + +import os +import sys +import time + +import requests +import urllib3 + +# Orchestrator's HTTPS endpoint uses a self-signed cert. +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +ORCH_URL = os.environ.get("ORCH_URL", "https://orchestrator:8935") +ORCH_SECRET = os.environ.get("ORCH_SECRET", "orch-secret") +CAPABILITY_NAME = os.environ.get("CAPABILITY_NAME", "image-upscale") +CAPABILITY_URL = os.environ.get("CAPABILITY_URL", "http://image_upscale:5000") +MAX_ATTEMPTS = int(os.environ.get("MAX_ATTEMPTS", "30")) + +data = { + "name": CAPABILITY_NAME, + "url": CAPABILITY_URL, + "capacity": 1, + "price_per_unit": 0, + "price_scaling": 1, + "currency": "wei", +} +headers = {"Authorization": ORCH_SECRET} + +for attempt in range(1, MAX_ATTEMPTS + 1): + try: + r = requests.post( + f"{ORCH_URL}/capability/register", + json=data, + headers=headers, + verify=False, + timeout=5, + ) + if r.status_code == 200: + print(f"registered {CAPABILITY_NAME} -> {CAPABILITY_URL}") + sys.exit(0) + print(f"attempt {attempt}: status={r.status_code} body={r.text!r}") + except Exception as exc: + print(f"attempt {attempt}: {exc}") + time.sleep(2) + +print("registration failed after timeout") +sys.exit(1) diff --git a/examples/runner/image_upscale/requirements.txt b/examples/runner/image_upscale/requirements.txt new file mode 100644 index 0000000..80fa612 --- /dev/null +++ b/examples/runner/image_upscale/requirements.txt @@ -0,0 +1,6 @@ +--extra-index-url https://download.pytorch.org/whl/cpu + +transformers +torch +Pillow +numpy diff --git a/examples/runner/image_upscale/test.sh b/examples/runner/image_upscale/test.sh new file mode 100755 index 0000000..4d11db3 --- /dev/null +++ b/examples/runner/image_upscale/test.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# E2E: send a request through the gateway, assert the upscaled image +# (2x of the 32x32 fixture = 64x64) comes back through the orchestrator. + +set -euo pipefail +cd "$(dirname "$0")" + +GATEWAY_URL="${GATEWAY_URL:-http://localhost:9935}" +TEST_IMAGE="${TEST_IMAGE:-test_image.png}" +INPUT_WIDTH="${INPUT_WIDTH:-64}" +INPUT_HEIGHT="${INPUT_HEIGHT:-64}" + +echo "Waiting for capability registration..." +for i in $(seq 1 60); do + if docker logs register_capability 2>&1 | grep -q "registered image-upscale"; then + echo " registered." + break + fi + sleep 2 +done + +INPUT_B64=$(base64 -w0 < "${TEST_IMAGE}") + +# TODO: swap curl for a livepeer_gateway batch caller (post PR #6) — drops +# the gateway service from compose. +LIVEPEER_HDR=$(printf '%s' '{"request":"{}","parameters":"{}","capability":"image-upscale","timeout_seconds":60}' | base64 -w0) + +echo "Sending request through gateway..." +RESPONSE=$(curl -fsS -X POST "${GATEWAY_URL}/process/request/predict" \ + -H "Livepeer: ${LIVEPEER_HDR}" \ + -H "Content-Type: application/json" \ + -d "{\"image\":\"${INPUT_B64}\"}") + +# Trim the base64 image from the echoed response — keeps stdout readable. +echo "Response (image truncated): $(echo "${RESPONSE}" | sed 's/\("image":"\)[^"]*/\1/')" + +WIDTH=$(echo "${RESPONSE}" | grep -oE '"width"[[:space:]]*:[[:space:]]*[0-9]+' | grep -oE '[0-9]+$') +HEIGHT=$(echo "${RESPONSE}" | grep -oE '"height"[[:space:]]*:[[:space:]]*[0-9]+' | grep -oE '[0-9]+$') + +# The Swin2SR processor pads inputs to its window size before upscaling, so +# output is at least 2x input but may be slightly larger. +if [ "${WIDTH}" -ge $((INPUT_WIDTH * 2)) ] && [ "${HEIGHT}" -ge $((INPUT_HEIGHT * 2)) ]; then + echo "PASS (${WIDTH}x${HEIGHT}, >=2x of ${INPUT_WIDTH}x${INPUT_HEIGHT})" + exit 0 +fi + +echo "FAIL: expected >=${INPUT_WIDTH}x${INPUT_HEIGHT} doubled, got ${WIDTH}x${HEIGHT}" +exit 1 diff --git a/examples/runner/image_upscale/test_image.png b/examples/runner/image_upscale/test_image.png new file mode 100644 index 0000000000000000000000000000000000000000..c419e41ee12be309a9d1a2cf4be7300a72d0bf1c GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMKu;IPkcwMx&mZJuP~c(MxVhy2 u%(;i!1x_$oUNy77y|SN^gA}l*oN;}C(ihI Date: Wed, 29 Apr 2026 21:09:50 +0200 Subject: [PATCH 09/13] feat(runner): add /health state machine (LOADING/OK/ERROR/IDLE) Pipeline tracks state across setup() and exposes it via /health, matching go-livepeer's HealthCheck wire format (ai/worker/runner.gen.go). Re-raises on setup() failure so the container still exits fail-fast. Refs livepeer/livepeer-python-gateway#8 (C6) --- src/livepeer_gateway/runner/__init__.py | 4 ++-- src/livepeer_gateway/runner/pipeline.py | 12 +++++++++++ src/livepeer_gateway/runner/serve.py | 27 ++++++++++++++++++------- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/livepeer_gateway/runner/__init__.py b/src/livepeer_gateway/runner/__init__.py index 2532771..4b55f50 100644 --- a/src/livepeer_gateway/runner/__init__.py +++ b/src/livepeer_gateway/runner/__init__.py @@ -1,7 +1,7 @@ """Pipeline SDK for creating BYOC-compatible AI capabilities from a simple Python class. """ -from .pipeline import Pipeline +from .pipeline import Pipeline, PipelineState from .serve import make_app, serve -__all__ = ["Pipeline", "make_app", "serve"] +__all__ = ["Pipeline", "PipelineState", "make_app", "serve"] diff --git a/src/livepeer_gateway/runner/pipeline.py b/src/livepeer_gateway/runner/pipeline.py index fbf810f..cc00aea 100644 --- a/src/livepeer_gateway/runner/pipeline.py +++ b/src/livepeer_gateway/runner/pipeline.py @@ -1,12 +1,24 @@ from __future__ import annotations from abc import ABC, abstractmethod +from enum import Enum from typing import Any +class PipelineState(str, Enum): + """Wire-level health state — matches go-livepeer's HealthCheckStatus.""" + + LOADING = "LOADING" + OK = "OK" + ERROR = "ERROR" + IDLE = "IDLE" + + class Pipeline(ABC): """Base class for batch inference pipelines.""" + _state: PipelineState = PipelineState.LOADING + def setup(self) -> None: """Hook called once before serve() accepts requests. diff --git a/src/livepeer_gateway/runner/serve.py b/src/livepeer_gateway/runner/serve.py index dfa9c91..1ebe889 100644 --- a/src/livepeer_gateway/runner/serve.py +++ b/src/livepeer_gateway/runner/serve.py @@ -6,9 +6,15 @@ from fastapi import FastAPI, HTTPException from pydantic import BaseModel, create_model -from .pipeline import Pipeline +from .pipeline import Pipeline, PipelineState -_LOG = logging.getLogger(__name__) +logger = logging.getLogger(__name__) + + +class HealthResponse(BaseModel): + """Response shape for ``GET /health`` — matches go-livepeer's HealthCheck.""" + + status: PipelineState def _is_basemodel(t: Any) -> bool: @@ -50,7 +56,7 @@ def handler(body: InputModel): except HTTPException: raise except Exception: - _LOG.exception("predict() failed") + logger.exception("predict() failed") raise HTTPException(status_code=500, detail="internal error") if OutputModel is not None: @@ -60,7 +66,14 @@ def handler(body: InputModel): def make_app(pipeline: Pipeline) -> FastAPI: """Build a FastAPI app exposing ``pipeline`` over HTTP.""" - pipeline.setup() + pipeline._state = PipelineState.LOADING + try: + pipeline.setup() + pipeline._state = PipelineState.OK + except Exception: + pipeline._state = PipelineState.ERROR + logger.exception("setup() failed") + raise InputModel, explicit_basemodel = _build_input_model( pipeline.predict, type(pipeline).__name__ @@ -80,9 +93,9 @@ def make_app(pipeline: Pipeline) -> FastAPI: summary="Run one inference", ) - @app.get("/health", summary="Liveness probe") - def handle_health() -> dict: - return {"status": "ready"} + @app.get("/health", summary="Liveness probe", response_model=HealthResponse) + def handle_health() -> HealthResponse: + return HealthResponse(status=pipeline._state) return app From d2c43f636be8531f281690b2f7488baed632633a Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Thu, 30 Apr 2026 10:11:09 +0200 Subject: [PATCH 10/13] feat(runner,examples): SSE auto-detection + LLM chat example When predict() is a generator, the SDK wraps the response with StreamingResponse(text/event-stream) and frames each yielded value as an OpenAI-style SSE event terminated by [DONE]. Both go-livepeer's BYOC gateway and the Python caller-side gateway watch for [DONE] to end the stream. Co-authored-by: John | Elite Encoder --- examples/runner/llm/Dockerfile | 26 +++++++ examples/runner/llm/README.md | 91 ++++++++++++++++++++++ examples/runner/llm/docker-compose.yml | 78 +++++++++++++++++++ examples/runner/llm/pipeline.py | 62 +++++++++++++++ examples/runner/llm/prepare_models.py | 11 +++ examples/runner/llm/register_capability.py | 52 +++++++++++++ examples/runner/llm/requirements.txt | 4 + examples/runner/llm/test.sh | 42 ++++++++++ src/livepeer_gateway/runner/serve.py | 37 +++++++-- 9 files changed, 398 insertions(+), 5 deletions(-) create mode 100644 examples/runner/llm/Dockerfile create mode 100644 examples/runner/llm/README.md create mode 100644 examples/runner/llm/docker-compose.yml create mode 100644 examples/runner/llm/pipeline.py create mode 100644 examples/runner/llm/prepare_models.py create mode 100644 examples/runner/llm/register_capability.py create mode 100644 examples/runner/llm/requirements.txt create mode 100755 examples/runner/llm/test.sh diff --git a/examples/runner/llm/Dockerfile b/examples/runner/llm/Dockerfile new file mode 100644 index 0000000..4645671 --- /dev/null +++ b/examples/runner/llm/Dockerfile @@ -0,0 +1,26 @@ +FROM python:3.11-slim + +WORKDIR /app + +# SDK install (in-repo source until livepeer-gateway publishes; will collapse +# to a single `pip install livepeer-gateway` line once on PyPI). +COPY pyproject.toml README ./ +COPY src /app/src +RUN pip install --no-cache-dir /app + +# Pipeline-specific deps. The requirements.txt sets --extra-index-url to +# pull the CPU-only torch wheel (~200 MB vs ~5 GB for the CUDA variant). +COPY examples/runner/llm/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Bake model weights at build time so setup() loads from local disk in +# milliseconds. +COPY examples/runner/llm/prepare_models.py /app/prepare_models.py +RUN python /app/prepare_models.py + +# Pipeline code last so edits don't invalidate the bake layer above. +COPY examples/runner/llm/pipeline.py /app/pipeline.py + +EXPOSE 5000 + +CMD ["python", "/app/pipeline.py"] diff --git a/examples/runner/llm/README.md b/examples/runner/llm/README.md new file mode 100644 index 0000000..55a86cc --- /dev/null +++ b/examples/runner/llm/README.md @@ -0,0 +1,91 @@ +# LLM chat (BYOC, streaming) + +A streaming chat capability built on +[Qwen2.5-0.5B-Instruct](https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct) — +small enough to run on CPU. Demonstrates the SDK's SSE pattern: `predict()` +returns an iterator, the SDK detects the generator and frames each yielded +value as a Server-Sent Event. + +A `Pipeline` subclass loads the model once in `setup()`, then streams tokens +on each `POST /predict` via HuggingFace's `TextIteratorStreamer`. Registered +as a BYOC capability, called through the gateway, response flows back end-to-end. + +## Run + +```bash +docker compose up -d --wait --build +./test.sh +docker compose down +``` + +`test.sh` prints `PASS` on success. + +## Browse the API + +- Swagger UI: +- ReDoc: +- OpenAPI JSON: + +## What's running + +```mermaid +sequenceDiagram + autonumber + participant curl + participant gateway + participant orchestrator + participant llm as llm
(SDK container) + + curl->>gateway: POST /process/request/predict + gateway->>orchestrator: forward (Livepeer-signed) + orchestrator->>llm: POST /predict {"prompt":"..."} + loop each token + llm-->>orchestrator: data: {"token":"..."} + orchestrator-->>gateway: data: {"token":"..."} + gateway-->>curl: data: {"token":"..."} + end + llm-->>orchestrator: data: [DONE] + orchestrator-->>gateway: data: [DONE] + gateway-->>curl: data: [DONE] +``` + +Four compose services: + +| Service | What it is | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| `gateway`, `orchestrator` | `livepeer/go-livepeer:master` from Docker Hub | +| `llm` | The pipeline container — runs the model in-process, streams tokens via `TextIteratorStreamer` | +| `register_capability` | One-shot helper that registers the `llm` capability once the pipeline is healthy | + +## Streaming contract + +`predict()` returns `Iterator[ChatChunk]`. The SDK detects the generator and +wraps the response with `Content-Type: text/event-stream`. Each yielded +`ChatChunk` becomes an SSE event, terminated by `[DONE]`: + +```text +data: {"token": "Hello"} + +data: {"token": " world"} + +data: [DONE] + +``` + +Both go-livepeer and the Python caller-side gateway watch for `[DONE]` to +end the stream. + +## Try it yourself + +```bash +LIVEPEER_HDR=$(printf '%s' \ + '{"request":"{}","parameters":"{}","capability":"llm","timeout_seconds":120}' \ + | base64 -w0) + +curl -N -X POST http://localhost:9935/process/request/predict \ + -H "Livepeer: ${LIVEPEER_HDR}" \ + -H 'Content-Type: application/json' \ + -d '{"prompt":"Tell me a joke"}' +``` + +`-N` disables curl's output buffering so each token arrives as it's generated. diff --git a/examples/runner/llm/docker-compose.yml b/examples/runner/llm/docker-compose.yml new file mode 100644 index 0000000..f182c63 --- /dev/null +++ b/examples/runner/llm/docker-compose.yml @@ -0,0 +1,78 @@ +services: + # Mirrors go-livepeer/doc/byoc.md: on-chain mode against a public Arbitrum + # RPC, but `pricePerUnit 0` keeps the registration free — no balance ever + # leaves the auto-generated keystore. No real chain interaction occurs. + # + # TODO: once livepeer/go-livepeer#3906 ships in :master, drop -network / + # -ethUrl / -ethPassword and run with bare `-network offchain`. Tracked + # in livepeer/go-livepeer#3905. + + orchestrator: + image: livepeer/go-livepeer:master + container_name: orchestrator + command: > + -network arbitrum-one-mainnet + -orchestrator + -ethUrl https://arb1.arbitrum.io/rpc + -ethPassword secret-password + -pricePerUnit 1 + -serviceAddr=orchestrator:8935 + -orchSecret=orch-secret + -v 6 + + gateway: + image: livepeer/go-livepeer:master + container_name: gateway + command: > + -network arbitrum-one-mainnet + -gateway + -ethUrl https://arb1.arbitrum.io/rpc + -ethPassword secret-password + -orchAddr=orchestrator:8935 + -httpAddr=0.0.0.0:9935 + -httpIngest + -v 6 + ports: + - "9935:9935" + depends_on: + - orchestrator + + llm: + build: + context: ../../.. + dockerfile: examples/runner/llm/Dockerfile + container_name: llm + ports: + - "5000:5000" + # Healthcheck waits for setup() to finish loading the model before the + # orchestrator can route requests here. + healthcheck: + test: ["CMD", "python", "-c", + "import urllib.request; urllib.request.urlopen('http://localhost:5000/health').read()"] + interval: 5s + timeout: 5s + retries: 30 + start_period: 60s + depends_on: + - orchestrator + + register_capability: + image: python:3.11-slim + container_name: register_capability + command: sh -c "pip install --quiet requests && python /app/register_capability.py" + volumes: + - ./register_capability.py:/app/register_capability.py:ro + environment: + ORCH_URL: https://orchestrator:8935 + ORCH_SECRET: orch-secret + CAPABILITY_NAME: llm + CAPABILITY_URL: http://llm:5000 + depends_on: + llm: + condition: service_healthy + orchestrator: + condition: service_started + +networks: + default: + name: livepeer diff --git a/examples/runner/llm/pipeline.py b/examples/runner/llm/pipeline.py new file mode 100644 index 0000000..7d05006 --- /dev/null +++ b/examples/runner/llm/pipeline.py @@ -0,0 +1,62 @@ +"""LLM chat BYOC pipeline using HuggingFace transformers. Streams tokens via SSE.""" + +from threading import Thread +from typing import Iterator + +from pydantic import BaseModel, Field +from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer + +from livepeer_gateway.runner import Pipeline, serve + + +class ChatInput(BaseModel): + prompt: str = Field(description="User message") + system: str = Field(default="You are a helpful assistant.", description="System prompt") + max_tokens: int = Field(default=256, ge=1, le=1024, description="Max tokens to generate") + + +class ChatChunk(BaseModel): + token: str + + +class ChatPipeline(Pipeline): + def setup(self): + # Loads from local HF cache populated at Docker build time. + model_id = "Qwen/Qwen2.5-0.5B-Instruct" + self.tokenizer = AutoTokenizer.from_pretrained(model_id) + self.model = AutoModelForCausalLM.from_pretrained(model_id) + + def predict(self, params: ChatInput) -> Iterator[ChatChunk]: + messages = [ + {"role": "system", "content": params.system}, + {"role": "user", "content": params.prompt}, + ] + # Two-step (template → text → tokenize) avoids a BatchEncoding + # wrapper that model.generate() can't unpack. + text = self.tokenizer.apply_chat_template( + messages, tokenize=False, add_generation_prompt=True + ) + inputs = self.tokenizer(text, return_tensors="pt") + + # TextIteratorStreamer feeds tokens into a thread-safe queue as the + # model generates them. We drive .generate() in a background thread + # so this generator can yield chunks as they arrive. + streamer = TextIteratorStreamer( + self.tokenizer, skip_prompt=True, skip_special_tokens=True + ) + Thread( + target=self.model.generate, + kwargs={ + **inputs, + "max_new_tokens": params.max_tokens, + "streamer": streamer, + }, + ).start() + + for chunk in streamer: + if chunk: + yield ChatChunk(token=chunk) + + +if __name__ == "__main__": + serve(ChatPipeline()) diff --git a/examples/runner/llm/prepare_models.py b/examples/runner/llm/prepare_models.py new file mode 100644 index 0000000..28b4292 --- /dev/null +++ b/examples/runner/llm/prepare_models.py @@ -0,0 +1,11 @@ +"""Download model weights into the local HF cache at build time. + +Invoked by the Dockerfile so ``setup()`` loads from local disk in +milliseconds instead of pulling from HF Hub on every container start. +""" + +from transformers import AutoModelForCausalLM, AutoTokenizer + +model_id = "Qwen/Qwen2.5-0.5B-Instruct" +AutoTokenizer.from_pretrained(model_id) +AutoModelForCausalLM.from_pretrained(model_id) diff --git a/examples/runner/llm/register_capability.py b/examples/runner/llm/register_capability.py new file mode 100644 index 0000000..3fb4d22 --- /dev/null +++ b/examples/runner/llm/register_capability.py @@ -0,0 +1,52 @@ +"""Register the llm capability with the orchestrator. + +Wire format follows +https://github.com/livepeer/go-livepeer/blob/main/doc/byoc.md +(handler in ``byoc/job_orchestrator.go``). +""" + +import os +import sys +import time + +import requests +import urllib3 + +# Orchestrator's HTTPS endpoint uses a self-signed cert. +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +ORCH_URL = os.environ.get("ORCH_URL", "https://orchestrator:8935") +ORCH_SECRET = os.environ.get("ORCH_SECRET", "orch-secret") +CAPABILITY_NAME = os.environ.get("CAPABILITY_NAME", "llm") +CAPABILITY_URL = os.environ.get("CAPABILITY_URL", "http://llm:5000") +MAX_ATTEMPTS = int(os.environ.get("MAX_ATTEMPTS", "30")) + +data = { + "name": CAPABILITY_NAME, + "url": CAPABILITY_URL, + "capacity": 1, + "price_per_unit": 0, + "price_scaling": 1, + "currency": "wei", +} +headers = {"Authorization": ORCH_SECRET} + +for attempt in range(1, MAX_ATTEMPTS + 1): + try: + r = requests.post( + f"{ORCH_URL}/capability/register", + json=data, + headers=headers, + verify=False, + timeout=5, + ) + if r.status_code == 200: + print(f"registered {CAPABILITY_NAME} -> {CAPABILITY_URL}") + sys.exit(0) + print(f"attempt {attempt}: status={r.status_code} body={r.text!r}") + except Exception as exc: + print(f"attempt {attempt}: {exc}") + time.sleep(2) + +print("registration failed after timeout") +sys.exit(1) diff --git a/examples/runner/llm/requirements.txt b/examples/runner/llm/requirements.txt new file mode 100644 index 0000000..f13a349 --- /dev/null +++ b/examples/runner/llm/requirements.txt @@ -0,0 +1,4 @@ +--extra-index-url https://download.pytorch.org/whl/cpu + +transformers +torch diff --git a/examples/runner/llm/test.sh b/examples/runner/llm/test.sh new file mode 100755 index 0000000..d7b11c2 --- /dev/null +++ b/examples/runner/llm/test.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# E2E: send a chat request through the gateway, assert the LLM streams +# tokens back via SSE and terminates with [DONE]. + +set -euo pipefail +cd "$(dirname "$0")" + +GATEWAY_URL="${GATEWAY_URL:-http://localhost:9935}" +PROMPT="${PROMPT:-Say hello in three words}" + +echo "Waiting for capability registration..." +for i in $(seq 1 60); do + if docker logs register_capability 2>&1 | grep -q "registered llm"; then + echo " registered." + break + fi + sleep 2 +done + +# TODO: swap curl for a livepeer_gateway batch caller (post PR #6) — drops +# the gateway service from compose. +LIVEPEER_HDR=$(printf '%s' '{"request":"{}","parameters":"{}","capability":"llm","timeout_seconds":120}' | base64 -w0) + +echo "Sending chat request through gateway (streaming)..." +# -N disables curl output buffering so chunks arrive as they're generated. +RESPONSE=$(curl -fsSN -X POST "${GATEWAY_URL}/process/request/predict" \ + -H "Livepeer: ${LIVEPEER_HDR}" \ + -H "Content-Type: application/json" \ + -d "{\"prompt\":\"${PROMPT}\"}") + +echo "Response (first events):" +echo "${RESPONSE}" | head -10 + +# Verify SSE format: at least one token event + the [DONE] terminator. +if echo "${RESPONSE}" | grep -q '^data: {"token":' \ + && echo "${RESPONSE}" | grep -q '^data: \[DONE\]'; then + echo "PASS" + exit 0 +fi + +echo "FAIL: expected token events and [DONE] terminator" +exit 1 diff --git a/src/livepeer_gateway/runner/serve.py b/src/livepeer_gateway/runner/serve.py index 1ebe889..4f05f3b 100644 --- a/src/livepeer_gateway/runner/serve.py +++ b/src/livepeer_gateway/runner/serve.py @@ -1,9 +1,11 @@ import inspect +import json import logging -from typing import Any +from typing import Any, Iterator import uvicorn from fastapi import FastAPI, HTTPException +from fastapi.responses import StreamingResponse from pydantic import BaseModel, create_model from .pipeline import Pipeline, PipelineState @@ -42,24 +44,45 @@ def _build_input_model(predict_fn: Any, owner_name: str) -> tuple[type[BaseModel return create_model(f"{owner_name}Input", **fields), False +def _format_sse(generator: Iterator[Any]) -> Iterator[bytes]: + """Frame yielded values as SSE events with [DONE] terminator. + + Required by go-livepeer and the Python caller-side gateway. + """ + try: + for chunk in generator: + payload = chunk.model_dump_json() if isinstance(chunk, BaseModel) else json.dumps(chunk) + yield f"data: {payload}\n\n".encode() + except Exception: + logger.exception("predict() generator failed") + yield b'data: {"error": "internal error"}\n\n' + yield b"data: [DONE]\n\n" + + def _build_predict_handler( pipeline: Pipeline, InputModel: type[BaseModel], OutputModel: type[BaseModel] | None, explicit_basemodel: bool, + is_generator: bool, ): def handler(body: InputModel): try: if explicit_basemodel: - return pipeline.predict(body) - return pipeline.predict(**body.model_dump()) + result = pipeline.predict(body) + else: + result = pipeline.predict(**body.model_dump()) except HTTPException: raise except Exception: logger.exception("predict() failed") raise HTTPException(status_code=500, detail="internal error") - if OutputModel is not None: + if is_generator: + return StreamingResponse(_format_sse(result), media_type="text/event-stream") + return result + + if OutputModel is not None and not is_generator: handler.__annotations__["return"] = OutputModel return handler @@ -75,13 +98,17 @@ def make_app(pipeline: Pipeline) -> FastAPI: logger.exception("setup() failed") raise + is_generator = inspect.isgeneratorfunction(pipeline.predict) + InputModel, explicit_basemodel = _build_input_model( pipeline.predict, type(pipeline).__name__ ) return_annotation = inspect.signature(pipeline.predict).return_annotation OutputModel = return_annotation if _is_basemodel(return_annotation) else None - handler = _build_predict_handler(pipeline, InputModel, OutputModel, explicit_basemodel) + handler = _build_predict_handler( + pipeline, InputModel, OutputModel, explicit_basemodel, is_generator + ) app = FastAPI(title=type(pipeline).__name__) app.state.pipeline = pipeline From 9c920ff1e0865a40223e05df2d15b4ee8482a0d2 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Thu, 30 Apr 2026 10:26:22 +0200 Subject: [PATCH 11/13] chore(examples): pricePerUnit=0 + tighten READMEs pricePerUnit=0 means no orchestrator charges, no ticket settlement, empty wallet stays unused. Replaces the previous pricePerUnit=1 workaround that relied on tickets rarely firing. --- examples/runner/hello_world/docker-compose.yml | 2 +- examples/runner/image_upscale/README.md | 4 ++-- examples/runner/image_upscale/docker-compose.yml | 2 +- examples/runner/llm/README.md | 3 +++ examples/runner/llm/docker-compose.yml | 2 +- examples/runner/sentiment/README.md | 4 ++-- examples/runner/sentiment/docker-compose.yml | 2 +- 7 files changed, 11 insertions(+), 8 deletions(-) diff --git a/examples/runner/hello_world/docker-compose.yml b/examples/runner/hello_world/docker-compose.yml index 5cd238a..6b6e3d7 100644 --- a/examples/runner/hello_world/docker-compose.yml +++ b/examples/runner/hello_world/docker-compose.yml @@ -15,7 +15,7 @@ services: -orchestrator -ethUrl https://arb1.arbitrum.io/rpc -ethPassword secret-password - -pricePerUnit 1 + -pricePerUnit 0 -serviceAddr=orchestrator:8935 -orchSecret=orch-secret -v 6 diff --git a/examples/runner/image_upscale/README.md b/examples/runner/image_upscale/README.md index 47e331d..f0f8e83 100644 --- a/examples/runner/image_upscale/README.md +++ b/examples/runner/image_upscale/README.md @@ -22,8 +22,8 @@ docker compose down `test.sh` prints `PASS` on success. -> **First build is ~5 minutes** — pulls torch CPU (~200 MB), transformers, and -> bakes the ~70 MB Swin2SR model into the image. Cached after that. +`prepare_models.py` bakes the model into the image at build time so +`setup()` loads from local cache in milliseconds. ## Browse the API diff --git a/examples/runner/image_upscale/docker-compose.yml b/examples/runner/image_upscale/docker-compose.yml index 719d0d7..9a4ce48 100644 --- a/examples/runner/image_upscale/docker-compose.yml +++ b/examples/runner/image_upscale/docker-compose.yml @@ -15,7 +15,7 @@ services: -orchestrator -ethUrl https://arb1.arbitrum.io/rpc -ethPassword secret-password - -pricePerUnit 1 + -pricePerUnit 0 -serviceAddr=orchestrator:8935 -orchSecret=orch-secret -v 6 diff --git a/examples/runner/llm/README.md b/examples/runner/llm/README.md index 55a86cc..9cda969 100644 --- a/examples/runner/llm/README.md +++ b/examples/runner/llm/README.md @@ -20,6 +20,9 @@ docker compose down `test.sh` prints `PASS` on success. +`prepare_models.py` bakes the model into the image at build time so +`setup()` loads from local cache in milliseconds. + ## Browse the API - Swagger UI: diff --git a/examples/runner/llm/docker-compose.yml b/examples/runner/llm/docker-compose.yml index f182c63..55cd7c5 100644 --- a/examples/runner/llm/docker-compose.yml +++ b/examples/runner/llm/docker-compose.yml @@ -15,7 +15,7 @@ services: -orchestrator -ethUrl https://arb1.arbitrum.io/rpc -ethPassword secret-password - -pricePerUnit 1 + -pricePerUnit 0 -serviceAddr=orchestrator:8935 -orchSecret=orch-secret -v 6 diff --git a/examples/runner/sentiment/README.md b/examples/runner/sentiment/README.md index 28b7e00..3686a3a 100644 --- a/examples/runner/sentiment/README.md +++ b/examples/runner/sentiment/README.md @@ -18,8 +18,8 @@ docker compose down `test.sh` prints `PASS` on success. -> **First build is ~5 minutes** — pulls torch CPU (~200 MB), transformers, and -> bakes the ~250 MB model into the image. Cached after that; rebuilds are fast. +`prepare_models.py` bakes the model into the image at build time so +`setup()` loads from local cache in milliseconds. ## Browse the API diff --git a/examples/runner/sentiment/docker-compose.yml b/examples/runner/sentiment/docker-compose.yml index 2e62a7f..c474216 100644 --- a/examples/runner/sentiment/docker-compose.yml +++ b/examples/runner/sentiment/docker-compose.yml @@ -15,7 +15,7 @@ services: -orchestrator -ethUrl https://arb1.arbitrum.io/rpc -ethPassword secret-password - -pricePerUnit 1 + -pricePerUnit 0 -serviceAddr=orchestrator:8935 -orchSecret=orch-secret -v 6 From 04cc697b6586d9a88a4b86fa5ce6e95103e68403 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Thu, 30 Apr 2026 13:45:28 +0200 Subject: [PATCH 12/13] feat(runner): LivePipeline ABC + /stream/* HTTP skeleton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds LivePipeline base class with setup/on_stream_start/process_video/ process_audio/on_params_update/on_stream_stop hooks (all default-passthrough) plus emit_event/emit_data stubs. Splits make_app dispatch into _make_pipeline_app (Pipeline → /predict) and _make_live_pipeline_app (LivePipeline → /stream/start|stop|params), sharing _run_setup and _add_health_route. Routes accept and validate the orchestrator's wire contract; streaming coordinator (subscribe/publish loops, lifecycle dispatch) lands in subsequent commits. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/livepeer_gateway/runner/__init__.py | 3 +- src/livepeer_gateway/runner/live_pipeline.py | 92 ++++++++++++++++++++ src/livepeer_gateway/runner/serve.py | 61 +++++++++++-- 3 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 src/livepeer_gateway/runner/live_pipeline.py diff --git a/src/livepeer_gateway/runner/__init__.py b/src/livepeer_gateway/runner/__init__.py index 4b55f50..20a97db 100644 --- a/src/livepeer_gateway/runner/__init__.py +++ b/src/livepeer_gateway/runner/__init__.py @@ -1,7 +1,8 @@ """Pipeline SDK for creating BYOC-compatible AI capabilities from a simple Python class. """ +from .live_pipeline import LivePipeline from .pipeline import Pipeline, PipelineState from .serve import make_app, serve -__all__ = ["Pipeline", "PipelineState", "make_app", "serve"] +__all__ = ["LivePipeline", "Pipeline", "PipelineState", "make_app", "serve"] diff --git a/src/livepeer_gateway/runner/live_pipeline.py b/src/livepeer_gateway/runner/live_pipeline.py new file mode 100644 index 0000000..a4e88a2 --- /dev/null +++ b/src/livepeer_gateway/runner/live_pipeline.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from pydantic import BaseModel, ConfigDict, Field + +from .pipeline import PipelineState + +if TYPE_CHECKING: + # Gated to keep PyAV out of the import path for batch Pipeline users. + from ..media_decode import AudioDecodedMediaFrame, VideoDecodedMediaFrame + + +class StreamStartRequest(BaseModel): + """Body of ``POST /stream/start`` — sent by the orchestrator. + + `subscribe_url`, `publish_url`, `data_url` are absent when the orchestrator + has the corresponding `EnableVideoIngress` / `EnableVideoEgress` / + `EnableDataOutput` flag disabled — the runner must tolerate any subset. + """ + + model_config = ConfigDict(extra="allow") + + gateway_request_id: str + control_url: str + events_url: str + subscribe_url: str | None = None + publish_url: str | None = None + data_url: str | None = None + params: dict[str, Any] = Field(default_factory=dict) + + +class StreamParamsRequest(BaseModel): + """Body of ``POST /stream/params`` — passthrough JSON params from the caller.""" + + model_config = ConfigDict(extra="allow") + + +class LivePipeline: + """Base class for real-time A/V pipelines on the BYOC trickle protocol. + + Subclasses override any of the lifecycle / processing hooks below. + A subclass that overrides nothing is a valid passthrough relay. + """ + + _state: PipelineState = PipelineState.LOADING + + def setup(self) -> None: + """Hook called once before serve() accepts requests. + + Sync, container-init time. Override to load model weights, warm up GPUs. + """ + + async def on_stream_start(self, params: dict[str, Any]) -> None: + """Called when a new stream session begins, before the first frame. + + `params` is the initial pipeline params from the caller. + """ + + async def process_video( + self, frame: VideoDecodedMediaFrame + ) -> VideoDecodedMediaFrame: + """Transform one decoded video frame. Default: passthrough.""" + return frame + + async def process_audio( + self, frame: AudioDecodedMediaFrame + ) -> AudioDecodedMediaFrame: + """Transform one decoded audio frame. Default: passthrough.""" + return frame + + async def on_params_update(self, params: dict[str, Any]) -> None: + """Called when the caller posts new params mid-stream.""" + + async def on_stream_stop(self) -> None: + """Called when the stream session ends — for per-session cleanup.""" + + async def emit_event(self, payload: dict[str, Any]) -> None: + """Publish a JSON event on the events trickle channel. + + Bound at session start; calling outside an active session is a no-op. + """ + # TODO: passthrough for now; wire up in next phase. + return None + + async def emit_data(self, payload: dict[str, Any]) -> None: + """Publish a JSON record on the data trickle channel (when enabled). + + Bound at session start; calling outside an active session is a no-op. + """ + # TODO: passthrough for now; wire up in next phase. + return None diff --git a/src/livepeer_gateway/runner/serve.py b/src/livepeer_gateway/runner/serve.py index 4f05f3b..1b90930 100644 --- a/src/livepeer_gateway/runner/serve.py +++ b/src/livepeer_gateway/runner/serve.py @@ -8,6 +8,7 @@ from fastapi.responses import StreamingResponse from pydantic import BaseModel, create_model +from .live_pipeline import LivePipeline, StreamParamsRequest, StreamStartRequest from .pipeline import Pipeline, PipelineState logger = logging.getLogger(__name__) @@ -87,8 +88,13 @@ def handler(body: InputModel): return handler -def make_app(pipeline: Pipeline) -> FastAPI: - """Build a FastAPI app exposing ``pipeline`` over HTTP.""" +def _add_health_route(app: FastAPI, pipeline: Pipeline | LivePipeline) -> None: + @app.get("/health", summary="Liveness probe", response_model=HealthResponse) + def handle_health() -> HealthResponse: + return HealthResponse(status=pipeline._state) + + +def _run_setup(pipeline: Pipeline | LivePipeline) -> None: pipeline._state = PipelineState.LOADING try: pipeline.setup() @@ -98,6 +104,37 @@ def make_app(pipeline: Pipeline) -> FastAPI: logger.exception("setup() failed") raise + +def _make_live_pipeline_app(pipeline: LivePipeline) -> FastAPI: + """Build a FastAPI app for a real-time ``LivePipeline``.""" + _run_setup(pipeline) + + app = FastAPI(title=type(pipeline).__name__) + app.state.pipeline = pipeline + + @app.post("/stream/start", summary="Start a stream session") + async def handle_stream_start(body: StreamStartRequest) -> dict[str, Any]: + logger.info("stream/start request_id=%s", body.gateway_request_id) + return {"status": "started", "gateway_request_id": body.gateway_request_id} + + @app.post("/stream/stop", summary="Stop the active stream session") + async def handle_stream_stop() -> dict[str, str]: + logger.info("stream/stop") + return {"status": "stopped"} + + @app.post("/stream/params", summary="Update params on the active stream") + async def handle_stream_params(body: StreamParamsRequest) -> dict[str, str]: + logger.info("stream/params keys=%s", list(body.model_dump().keys())) + return {"status": "ok"} + + _add_health_route(app, pipeline) + return app + + +def _make_pipeline_app(pipeline: Pipeline) -> FastAPI: + """Build a FastAPI app for a request/response ``Pipeline`` (HTTP `/predict`).""" + _run_setup(pipeline) + is_generator = inspect.isgeneratorfunction(pipeline.predict) InputModel, explicit_basemodel = _build_input_model( @@ -120,13 +157,23 @@ def make_app(pipeline: Pipeline) -> FastAPI: summary="Run one inference", ) - @app.get("/health", summary="Liveness probe", response_model=HealthResponse) - def handle_health() -> HealthResponse: - return HealthResponse(status=pipeline._state) - + _add_health_route(app, pipeline) return app -def serve(pipeline: Pipeline, *, host: str = "0.0.0.0", port: int = 5000) -> None: +def make_app(pipeline: Pipeline | LivePipeline) -> FastAPI: + """Build a FastAPI app exposing ``pipeline`` over HTTP. + + Dispatches on `LivePipeline` vs `Pipeline` to register `/stream/*` + or `/predict` respectively. + """ + if isinstance(pipeline, LivePipeline): + return _make_live_pipeline_app(pipeline) + return _make_pipeline_app(pipeline) + + +def serve( + pipeline: Pipeline | LivePipeline, *, host: str = "0.0.0.0", port: int = 5000 +) -> None: """Run the pipeline as an HTTP server on host:port.""" uvicorn.run(make_app(pipeline), host=host, port=port) From 831ee4455a956f5c1d1f67182abe2bd1c06c079a Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Thu, 30 Apr 2026 15:38:58 +0200 Subject: [PATCH 13/13] feat(runner): trickle bytes-through on /stream/start|stop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds _run_passthrough coroutine that bridges subscribe → publish trickle channels segment-by-segment using the existing TrickleSubscriber and TricklePublisher. /stream/start spawns it as a background task on the LivePipeline; /stream/stop cancels and waits up to 5s for graceful cleanup before returning. Single-session for now (409 on double-start); data-only / event-only streams (no subscribe_url + publish_url) return 400 — both extensions land in subsequent commits. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/livepeer_gateway/runner/live_pipeline.py | 36 ++++++++++++++++ src/livepeer_gateway/runner/serve.py | 44 +++++++++++++++++++- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/livepeer_gateway/runner/live_pipeline.py b/src/livepeer_gateway/runner/live_pipeline.py index a4e88a2..123c8b3 100644 --- a/src/livepeer_gateway/runner/live_pipeline.py +++ b/src/livepeer_gateway/runner/live_pipeline.py @@ -1,9 +1,12 @@ from __future__ import annotations +import asyncio from typing import TYPE_CHECKING, Any from pydantic import BaseModel, ConfigDict, Field +from ..trickle_publisher import TricklePublisher +from ..trickle_subscriber import TrickleSubscriber from .pipeline import PipelineState if TYPE_CHECKING: @@ -44,6 +47,8 @@ class LivePipeline: """ _state: PipelineState = PipelineState.LOADING + # Single-session for now; multi-session is post-C8 (capacity demand-driven). + _session_task: asyncio.Task[None] | None = None def setup(self) -> None: """Hook called once before serve() accepts requests. @@ -90,3 +95,34 @@ async def emit_data(self, payload: dict[str, Any]) -> None: """ # TODO: passthrough for now; wire up in next phase. return None + + +async def _run_passthrough(subscribe_url: str, publish_url: str) -> None: + """Forward bytes from a subscribe URL to a publish URL, unmodified. + + Each inbound trickle segment becomes one outbound segment (1:1) — never + merged or split, so downstream consumers see the same segment count and + ordering as the upstream sender. Returns when the subscribe channel ends + (orchestrator deletes it → 404 from the trickle server), or when the + task is cancelled / either side raises. + """ + sub = TrickleSubscriber(subscribe_url) + try: + # MIME must match go-livepeer's publish channel (stream_orchestrator.go). + async with TricklePublisher(publish_url, mime_type="video/MP2T") as pub: + while True: + segment = await sub.next() + if segment is None: # EOS — channel ended + return + try: + reader = segment.make_reader() + async with await pub.next() as writer: + while True: + chunk = await reader.read() + if not chunk: + break + await writer.write(chunk) + finally: + await segment.close() + finally: + await sub.close() diff --git a/src/livepeer_gateway/runner/serve.py b/src/livepeer_gateway/runner/serve.py index 1b90930..8bd470d 100644 --- a/src/livepeer_gateway/runner/serve.py +++ b/src/livepeer_gateway/runner/serve.py @@ -1,3 +1,4 @@ +import asyncio import inspect import json import logging @@ -8,9 +9,19 @@ from fastapi.responses import StreamingResponse from pydantic import BaseModel, create_model -from .live_pipeline import LivePipeline, StreamParamsRequest, StreamStartRequest +from .live_pipeline import ( + LivePipeline, + StreamParamsRequest, + StreamStartRequest, + _run_passthrough, +) from .pipeline import Pipeline, PipelineState +# Bound the wait for an in-flight session task to terminate after cancel. +# 5s covers the trickle close paths (segment close + session close + queue +# drain). If we exceed it, the task is left to finish in the background. +_STOP_TIMEOUT_S = 5.0 + logger = logging.getLogger(__name__) @@ -115,11 +126,42 @@ def _make_live_pipeline_app(pipeline: LivePipeline) -> FastAPI: @app.post("/stream/start", summary="Start a stream session") async def handle_stream_start(body: StreamStartRequest) -> dict[str, Any]: logger.info("stream/start request_id=%s", body.gateway_request_id) + + if pipeline._session_task and not pipeline._session_task.done(): + # Single-session for now; orchestrator shouldn't double-start. + raise HTTPException(status_code=409, detail="session already active") + + if not (body.subscribe_url and body.publish_url): + # Bytes-through requires both directions; data-only / event-only + # streams land in later phases. + raise HTTPException( + status_code=400, + detail="subscribe_url and publish_url required for video streams", + ) + + pipeline._session_task = asyncio.create_task( + _run_passthrough(body.subscribe_url, body.publish_url), + name=f"live-session-{body.gateway_request_id}", + ) return {"status": "started", "gateway_request_id": body.gateway_request_id} @app.post("/stream/stop", summary="Stop the active stream session") async def handle_stream_stop() -> dict[str, str]: logger.info("stream/stop") + task = pipeline._session_task + pipeline._session_task = None + if task is None or task.done(): + return {"status": "stopped"} + + task.cancel() + try: + await asyncio.wait_for(task, timeout=_STOP_TIMEOUT_S) + except asyncio.TimeoutError: + logger.warning("session task did not terminate within %.1fs", _STOP_TIMEOUT_S) + except asyncio.CancelledError: + pass + except Exception: + logger.exception("session task ended with error") return {"status": "stopped"} @app.post("/stream/params", summary="Update params on the active stream")