Sync reading position from Moon Reader app to Bookhive atproto records
atproto bookhive ereader moonreader
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

Initial waggle scaffolding

WebDAV shim that impersonates Moon+ Reader's sync backend and translates
PROPFIND/GET/PUT into ATProto reads/writes against buzz.bookhive.book
records. Replaces the moon2hive cron script with a bidirectional service.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Brad Wenner dfc6836d

+2560
+13
.env.example
··· 1 + # ATProto — the identity waggle writes to 2 + PDS=hermitary.brad.quest 3 + BSKY_HANDLE=bradwenner.photo 4 + BSKY_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx 5 + 6 + # WebDAV access to waggle itself — what Moon+ Reader sends in Basic auth 7 + DAV_USER=brad 8 + DAV_PASSWORD=change-me 9 + 10 + # Local-disk fallback for non-.po DAV paths (Settings/, etc.) 11 + PASSTHROUGH_ROOT=/data/passthrough 12 + 13 + LOG_LEVEL=INFO
+54
.forgejo/workflows/ci.yml
··· 1 + name: CI 2 + 3 + on: 4 + push: 5 + branches: [main] 6 + pull_request: 7 + 8 + jobs: 9 + test: 10 + runs-on: docker 11 + container: ghcr.io/astral-sh/uv:python3.11-bookworm-slim 12 + steps: 13 + # Forgejo runner's slim image has no Node.js, so actions/checkout fails. 14 + # Clone manually instead (see global memory: feedback_ci_checkout). 15 + - name: Clone repo 16 + run: | 17 + apt-get update && apt-get install -y --no-install-recommends git ca-certificates 18 + git clone --depth 1 "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" . 19 + git fetch --depth 1 origin "${GITHUB_SHA}" && git checkout "${GITHUB_SHA}" 20 + 21 + - name: Install deps 22 + run: uv sync --extra dev 23 + 24 + - name: Lint 25 + run: uv run ruff check src tests 26 + 27 + - name: Test 28 + run: uv run pytest -q 29 + 30 + image: 31 + needs: test 32 + if: github.ref == 'refs/heads/main' 33 + runs-on: docker 34 + container: docker:27-cli 35 + steps: 36 + - name: Clone repo 37 + run: | 38 + apk add --no-cache git 39 + git clone --depth 1 "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" . 40 + git fetch --depth 1 origin "${GITHUB_SHA}" && git checkout "${GITHUB_SHA}" 41 + 42 + - name: Login to Forgejo registry 43 + run: | 44 + echo "${FORGEJO_TOKEN}" | docker login git.brads.house -u "${FORGEJO_USER}" --password-stdin 45 + env: 46 + FORGEJO_USER: ${{ secrets.FORGEJO_USER }} 47 + FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }} 48 + 49 + - name: Build and push 50 + run: | 51 + IMAGE=git.brads.house/brad/waggle 52 + docker build -t "${IMAGE}:latest" -t "${IMAGE}:${GITHUB_SHA::8}" . 53 + docker push "${IMAGE}:latest" 54 + docker push "${IMAGE}:${GITHUB_SHA::8}"
+10
.gitignore
··· 1 + .env 2 + .venv/ 3 + __pycache__/ 4 + *.pyc 5 + .pytest_cache/ 6 + .ruff_cache/ 7 + dist/ 8 + build/ 9 + *.egg-info/ 10 + data/
+74
CLAUDE.md
··· 1 + # waggle 2 + 3 + WebDAV shim that impersonates Moon+ Reader's sync backend and translates 4 + requests into ATProto reads/writes on `buzz.bookhive.book` records. Protocol 5 + adapter, not a poller — every inbound DAV request is a round-trip to the PDS. 6 + 7 + See `/Users/personal/.claude/plans/declarative-skipping-sunset.md` for the 8 + full architecture plan. 9 + 10 + ## Shape 11 + 12 + ``` 13 + MoonReader ─[WebDAV: PROPFIND/GET/PUT/HEAD/OPTIONS]──→ waggle ──→ ATProto PDS 14 + Passthrough (Settings/, etc.) ─────────────────→ local disk at $PASSTHROUGH_ROOT 15 + ``` 16 + 17 + - `/Books/.Moon+/Cache/**` is **virtual** — served from bookhive records. No 18 + disk backing. Synthesizing the listing comes from `listRecords` on 19 + `buzz.bookhive.book`; each record with a `bookProgress.moonReader.file` 20 + appears as a `.po`. 21 + - Every other path hits `Passthrough` over a local filesystem. Future: 22 + annotation files route to `margin.at`, but out of scope for v1. 23 + 24 + ## Design constraints (don't re-derive) 25 + 26 + - **Namespace**: all our extensions live under `bookProgress.moonReader.*` — 27 + never flatten into top-level lexicon fields. Same pattern survives adding 28 + `bookProgress.koReader.*` later. 29 + - **`moonReader.position` is stored verbatim**. On GET we return it unchanged 30 + so Moon+ Reader's internal `timestamp_ms` (the ebook import mtime, see 31 + `../moon2hive/CLAUDE.md` for the forensics) stays stable. Re-synthesizing 32 + would trip Moon+ into thinking the position changed. 33 + - **PUT is idempotent**: if the incoming position + filename match what's on 34 + the record, skip the write. Moon+ Reader PUTs on every pause event and most 35 + are no-ops. 36 + - **Finished books don't flip back to reading**: if `status` is 37 + `buzz.bookhive.defs#finished`, a PUT updates the bookProgress but keeps the 38 + status. 39 + - **Filename = book identity for Moon+**. `moonReader.file` is the key we 40 + look up by. If `resolve_record()` misses, fall back to fuzzy 41 + title/author match, then catalog-search-and-create. 42 + 43 + ## Single-user 44 + 45 + One atproto identity in env. `DAV_USER` / `DAV_PASSWORD` are a single shared 46 + credential — just a gate on the service, not a multi-tenant mapping. 47 + 48 + ## Running 49 + 50 + ```sh 51 + cp .env.example .env # fill in creds 52 + uv sync --extra dev 53 + uv run uvicorn waggle.main:app --reload --port 8080 54 + uv run pytest -q 55 + uv run ruff check src tests 56 + ``` 57 + 58 + ## Deploy 59 + 60 + Docker image at `git.brads.house/brad/waggle`. `docker-compose.yml` is 61 + CasaOS-shaped; mount `./data:/data` for the passthrough scratch area. Put 62 + behind HTTPS reverse proxy — Moon+ Reader requires TLS for WebDAV. 63 + 64 + CI: `.forgejo/workflows/ci.yml` (test on PR; build+push image on `main`). 65 + Slim runners don't have Node.js; we `git clone` manually instead of using 66 + `actions/checkout`. 67 + 68 + ## Related 69 + 70 + - `../moon2hive/` — the previous one-way cron script waggle replaces. The 71 + translation core in `src/waggle/atproto/bookhive.py` is lifted (with light 72 + edits) from `moon2hive.py`. 73 + - `bookhive.buzz` — the AT Protocol book tracker that renders the records 74 + waggle writes.
+17
Dockerfile
··· 1 + FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim 2 + 3 + WORKDIR /app 4 + 5 + COPY pyproject.toml uv.lock* ./ 6 + RUN uv sync --frozen --no-install-project --no-dev 2>/dev/null || uv sync --no-install-project --no-dev 7 + 8 + COPY src ./src 9 + RUN uv sync --no-dev 10 + 11 + ENV PATH="/app/.venv/bin:$PATH" \ 12 + PASSTHROUGH_ROOT=/data/passthrough \ 13 + LOG_LEVEL=INFO 14 + 15 + EXPOSE 8080 16 + 17 + CMD ["uvicorn", "waggle.main:app", "--host", "0.0.0.0", "--port", "8080"]
+53
README.md
··· 1 + # waggle 2 + 3 + A WebDAV shim that impersonates a cloud sync backend for [Moon+ Reader] and 4 + translates its `.po` position files into ATProto reads/writes on your 5 + [bookhive.buzz] book records. 6 + 7 + Point Moon+ Reader at `https://waggle.yourdomain/` (Basic auth). Open a book, 8 + read for a while, pause the app — your `bookProgress` on your PDS updates. Flip 9 + to `bookhive.buzz` and the progress is already there. Flip between devices and 10 + waggle hands each one the current state. 11 + 12 + ## How it works 13 + 14 + ``` 15 + MoonReader ─[WebDAV: PROPFIND / GET / PUT]──→ waggle ──→ ATProto PDS 16 + (buzz.bookhive.book) 17 + ``` 18 + 19 + - `PROPFIND /Books/.Moon+/Cache/` synthesizes a directory listing from your 20 + `buzz.bookhive.book` records that have a `bookProgress.moonReader.file` field. 21 + - `GET /Books/.Moon+/Cache/{file}.po` returns the stored position string 22 + verbatim (preserves Moon+ Reader's internal chapter/offset encoding). 23 + - `PUT /Books/.Moon+/Cache/{file}.po` parses the `.po` body, finds the matching 24 + bookhive record (or catalog-searches and creates one), and updates 25 + `bookProgress.{percent,currentChapter,moonReader}` on your PDS. 26 + 27 + Non-position WebDAV paths (`Books/.Moon+/Settings/`, etc.) fall through to a 28 + local-disk scratch area rooted at `$PASSTHROUGH_ROOT`. Those files are not 29 + synced anywhere — they just keep Moon+ Reader happy. 30 + 31 + ## Running locally 32 + 33 + ```sh 34 + cp .env.example .env # fill in PDS, handle, app password, DAV creds 35 + uv run uvicorn waggle.main:app --reload --port 8080 36 + ``` 37 + 38 + Point a test device at `http://<your-laptop>:8080/` as the WebDAV target. 39 + 40 + ## Deploying 41 + 42 + Single Docker container. `docker-compose.yml` is CasaOS-shaped; put waggle 43 + behind a reverse proxy with HTTPS (Moon+ Reader requires TLS for WebDAV). 44 + 45 + ## Related 46 + 47 + - [`../moon2hive`](../moon2hive) — the previous one-way cron script this 48 + replaces. 49 + - [`bookhive.buzz`](https://bookhive.buzz) — the AT Protocol book tracker 50 + whose records waggle reads and writes. 51 + 52 + [Moon+ Reader]: https://www.moondownload.com/ 53 + [bookhive.buzz]: https://bookhive.buzz/
+22
docker-compose.yml
··· 1 + services: 2 + waggle: 3 + image: git.brads.house/brad/waggle:latest 4 + container_name: waggle 5 + restart: unless-stopped 6 + ports: 7 + - "8080:8080" 8 + volumes: 9 + - ./data:/data 10 + environment: 11 + PDS: ${PDS} 12 + BSKY_HANDLE: ${BSKY_HANDLE} 13 + BSKY_APP_PASSWORD: ${BSKY_APP_PASSWORD} 14 + DAV_USER: ${DAV_USER} 15 + DAV_PASSWORD: ${DAV_PASSWORD} 16 + PASSTHROUGH_ROOT: /data/passthrough 17 + LOG_LEVEL: ${LOG_LEVEL:-INFO} 18 + healthcheck: 19 + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/healthz')"] 20 + interval: 30s 21 + timeout: 5s 22 + retries: 3
+37
pyproject.toml
··· 1 + [project] 2 + name = "waggle" 3 + version = "0.1.0" 4 + description = "WebDAV/KOSync shim that translates ereader sync requests into ATProto bookhive records" 5 + requires-python = ">=3.11" 6 + dependencies = [ 7 + "fastapi>=0.115", 8 + "uvicorn[standard]>=0.30", 9 + "httpx>=0.27", 10 + "python-dotenv>=1.0", 11 + ] 12 + 13 + [project.optional-dependencies] 14 + dev = [ 15 + "pytest>=8.0", 16 + "pytest-asyncio>=0.23", 17 + "respx>=0.21", 18 + "ruff>=0.6", 19 + ] 20 + 21 + [build-system] 22 + requires = ["hatchling"] 23 + build-backend = "hatchling.build" 24 + 25 + [tool.hatch.build.targets.wheel] 26 + packages = ["src/waggle"] 27 + 28 + [tool.ruff] 29 + line-length = 100 30 + target-version = "py311" 31 + 32 + [tool.ruff.lint] 33 + select = ["E", "F", "I", "UP", "B", "SIM"] 34 + 35 + [tool.pytest.ini_options] 36 + testpaths = ["tests"] 37 + asyncio_mode = "auto"
src/waggle/__init__.py

This is a binary file and will not be displayed.

src/waggle/adapters/__init__.py

This is a binary file and will not be displayed.

src/waggle/adapters/webdav/__init__.py

This is a binary file and will not be displayed.

+203
src/waggle/adapters/webdav/moonreader.py
··· 1 + """Moon+ Reader `.po` virtual directory served from bookhive records. 2 + 3 + The `/Books/.Moon+/Cache/` prefix is NOT backed by disk — each PROPFIND/GET/PUT 4 + is translated to an ATProto round-trip against `buzz.bookhive.book`. See the 5 + project plan and `atproto/bookhive.py` for the translation contract. 6 + """ 7 + 8 + from __future__ import annotations 9 + 10 + import hashlib 11 + import logging 12 + from datetime import UTC, datetime 13 + from email.utils import format_datetime 14 + from urllib.parse import quote 15 + 16 + from waggle.atproto import bookhive 17 + from waggle.atproto.client import ATProtoClient 18 + 19 + log = logging.getLogger(__name__) 20 + 21 + VIRTUAL_PREFIX = "/Books/.Moon+/Cache/" 22 + 23 + 24 + # --------------------------------------------------------------------------- 25 + # Helpers 26 + # --------------------------------------------------------------------------- 27 + 28 + def under_prefix(path: str) -> bool: 29 + """Is this path the cache dir itself or something inside it?""" 30 + return path == VIRTUAL_PREFIX.rstrip("/") or path.startswith(VIRTUAL_PREFIX) 31 + 32 + 33 + def filename_from_path(path: str) -> str | None: 34 + """Return the `.po` filename from a path, or None if path is the dir itself.""" 35 + if not under_prefix(path): 36 + return None 37 + tail = path[len(VIRTUAL_PREFIX):] if path.startswith(VIRTUAL_PREFIX) else "" 38 + return tail or None 39 + 40 + 41 + def _http_date(iso: str | None) -> str: 42 + if not iso: 43 + return format_datetime(datetime.now(UTC), usegmt=True) 44 + try: 45 + dt = datetime.fromisoformat(iso.replace("Z", "+00:00")) 46 + except ValueError: 47 + return format_datetime(datetime.now(UTC), usegmt=True) 48 + return format_datetime(dt, usegmt=True) 49 + 50 + 51 + def _etag(content: str) -> str: 52 + return '"' + hashlib.sha1(content.encode("utf-8")).hexdigest()[:16] + '"' 53 + 54 + 55 + # --------------------------------------------------------------------------- 56 + # PROPFIND XML synthesis 57 + # --------------------------------------------------------------------------- 58 + 59 + def _response_xml( 60 + *, 61 + href: str, 62 + is_collection: bool, 63 + last_modified_http: str, 64 + content_length: int = 0, 65 + etag: str | None = None, 66 + display_name: str | None = None, 67 + ) -> str: 68 + resourcetype = "<D:collection/>" if is_collection else "" 69 + content_type = "" if is_collection else "<D:getcontenttype>text/plain</D:getcontenttype>" 70 + length = "" if is_collection else f"<D:getcontentlength>{content_length}</D:getcontentlength>" 71 + etag_xml = f"<D:getetag>{etag}</D:getetag>" if etag else "" 72 + dn = display_name or href.rsplit("/", 1)[-1] or href 73 + return ( 74 + "<D:response>" 75 + f"<D:href>{quote(href)}</D:href>" 76 + "<D:propstat>" 77 + "<D:prop>" 78 + f"<D:resourcetype>{resourcetype}</D:resourcetype>" 79 + f"<D:displayname>{dn}</D:displayname>" 80 + f"<D:getlastmodified>{last_modified_http}</D:getlastmodified>" 81 + f"{length}" 82 + f"{content_type}" 83 + f"{etag_xml}" 84 + "</D:prop>" 85 + "<D:status>HTTP/1.1 200 OK</D:status>" 86 + "</D:propstat>" 87 + "</D:response>" 88 + ) 89 + 90 + 91 + def _multistatus(responses: list[str]) -> bytes: 92 + body = ( 93 + '<?xml version="1.0" encoding="utf-8"?>' 94 + '<D:multistatus xmlns:D="DAV:">' 95 + + "".join(responses) 96 + + "</D:multistatus>" 97 + ) 98 + return body.encode("utf-8") 99 + 100 + 101 + async def propfind( 102 + client: ATProtoClient, path: str, depth: str 103 + ) -> tuple[int, bytes, dict[str, str]]: 104 + """Synthesize a WebDAV 207 Multi-Status response from bookhive records.""" 105 + filename = filename_from_path(path) 106 + 107 + if filename is None: 108 + # PROPFIND on the cache dir itself. 109 + records = await bookhive.list_records(client) 110 + responses = [ 111 + _response_xml( 112 + href=VIRTUAL_PREFIX, 113 + is_collection=True, 114 + last_modified_http=format_datetime(datetime.now(UTC), usegmt=True), 115 + display_name="Cache", 116 + ) 117 + ] 118 + if depth != "0": 119 + for r in records: 120 + moon = (r["value"].get("bookProgress") or {}).get("moonReader") or {} 121 + fn = moon.get("file") 122 + if not fn: 123 + continue 124 + pos = moon.get("position") or "" 125 + updated = (r["value"].get("bookProgress") or {}).get("updatedAt") 126 + responses.append( 127 + _response_xml( 128 + href=f"{VIRTUAL_PREFIX}{fn}", 129 + is_collection=False, 130 + last_modified_http=_http_date(updated), 131 + content_length=len(pos.encode("utf-8")), 132 + etag=_etag(pos), 133 + display_name=fn, 134 + ) 135 + ) 136 + return 207, _multistatus(responses), {"Content-Type": "application/xml; charset=utf-8"} 137 + 138 + # PROPFIND on a specific .po file. 139 + record = await bookhive.resolve_record(client, filename) 140 + if not record: 141 + return 404, b"", {} 142 + content = bookhive.serialize_po(record["value"]) 143 + updated = (record["value"].get("bookProgress") or {}).get("updatedAt") 144 + response = _response_xml( 145 + href=f"{VIRTUAL_PREFIX}{filename}", 146 + is_collection=False, 147 + last_modified_http=_http_date(updated), 148 + content_length=len(content.encode("utf-8")), 149 + etag=_etag(content), 150 + display_name=filename, 151 + ) 152 + return 207, _multistatus([response]), {"Content-Type": "application/xml; charset=utf-8"} 153 + 154 + 155 + # --------------------------------------------------------------------------- 156 + # GET / HEAD 157 + # --------------------------------------------------------------------------- 158 + 159 + async def get( 160 + client: ATProtoClient, path: str, *, head: bool = False 161 + ) -> tuple[int, bytes, dict[str, str]]: 162 + filename = filename_from_path(path) 163 + if not filename: 164 + return 400, b"", {} 165 + record = await bookhive.resolve_record(client, filename) 166 + if not record: 167 + return 404, b"", {} 168 + content = bookhive.serialize_po(record["value"]) 169 + updated = (record["value"].get("bookProgress") or {}).get("updatedAt") 170 + body = content.encode("utf-8") 171 + headers = { 172 + "Content-Type": "text/plain; charset=utf-8", 173 + "Content-Length": str(len(body)), 174 + "Last-Modified": _http_date(updated), 175 + "ETag": _etag(content), 176 + } 177 + return 200, (b"" if head else body), headers 178 + 179 + 180 + # --------------------------------------------------------------------------- 181 + # PUT / DELETE 182 + # --------------------------------------------------------------------------- 183 + 184 + async def put( 185 + client: ATProtoClient, path: str, body: bytes 186 + ) -> tuple[int, bytes, dict[str, str]]: 187 + filename = filename_from_path(path) 188 + if not filename: 189 + return 400, b"", {} 190 + status_msg = await bookhive.apply_po_put(client, filename, body) 191 + log.info("PUT %s -> %s", filename, status_msg) 192 + return 204, b"", {} 193 + 194 + 195 + async def delete( 196 + client: ATProtoClient, path: str 197 + ) -> tuple[int, bytes, dict[str, str]]: 198 + filename = filename_from_path(path) 199 + if not filename: 200 + return 400, b"", {} 201 + status_msg = await bookhive.apply_po_delete(client, filename) 202 + log.info("DELETE %s -> %s", filename, status_msg) 203 + return 204, b"", {}
+149
src/waggle/adapters/webdav/passthrough.py
··· 1 + """Local-disk WebDAV fallback for paths outside the Moon+ cache virtual tree. 2 + 3 + Moon+ Reader syncs non-position files (settings, bookmark lists, theme data) 4 + alongside `.po` files. waggle stores those on disk under `PASSTHROUGH_ROOT` and 5 + serves them back over DAV. Eventually, annotation-bearing files here may be 6 + routed to `margin.at` records — out of scope for v1. 7 + """ 8 + 9 + from __future__ import annotations 10 + 11 + import logging 12 + from datetime import UTC, datetime 13 + from email.utils import format_datetime 14 + from pathlib import Path 15 + from urllib.parse import quote 16 + 17 + log = logging.getLogger(__name__) 18 + 19 + 20 + class Passthrough: 21 + def __init__(self, root: str) -> None: 22 + self._root = Path(root).resolve() 23 + self._root.mkdir(parents=True, exist_ok=True) 24 + # Skeleton dirs Moon+ Reader expects so PROPFIND on ancestors succeeds. 25 + # `Cache` is a placeholder — real requests under it route to bookhive 26 + # (see moonreader.under_prefix in router.py); this empty dir only 27 + # exists so it appears when Moon+ Reader lists `/Books/.Moon+/`. 28 + for sub in ("Books", "Books/.Moon+", "Books/.Moon+/Settings", "Books/.Moon+/Cache"): 29 + (self._root / sub).mkdir(parents=True, exist_ok=True) 30 + 31 + def _local(self, path: str) -> Path: 32 + """Resolve a WebDAV path to a local Path, refusing escapes.""" 33 + rel = path.lstrip("/") 34 + candidate = (self._root / rel).resolve() 35 + # Directory traversal guard 36 + if not str(candidate).startswith(str(self._root)): 37 + raise PermissionError(f"path escapes passthrough root: {path!r}") 38 + return candidate 39 + 40 + # -- verbs ------------------------------------------------------------- 41 + 42 + async def propfind( 43 + self, path: str, depth: str 44 + ) -> tuple[int, bytes, dict[str, str]]: 45 + target = self._local(path) 46 + if not target.exists(): 47 + return 404, b"", {} 48 + 49 + responses: list[str] = [_entry_xml(target, path, self._root)] 50 + if target.is_dir() and depth != "0": 51 + for child in sorted(target.iterdir()): 52 + child_href = path.rstrip("/") + "/" + child.name 53 + if child.is_dir(): 54 + child_href += "/" 55 + responses.append(_entry_xml(child, child_href, self._root)) 56 + 57 + body = _multistatus(responses) 58 + return 207, body, {"Content-Type": "application/xml; charset=utf-8"} 59 + 60 + async def get( 61 + self, path: str, *, head: bool = False 62 + ) -> tuple[int, bytes, dict[str, str]]: 63 + target = self._local(path) 64 + if not target.is_file(): 65 + return 404, b"", {} 66 + data = target.read_bytes() 67 + mtime = datetime.fromtimestamp(target.stat().st_mtime, tz=UTC) 68 + headers = { 69 + "Content-Type": "application/octet-stream", 70 + "Content-Length": str(len(data)), 71 + "Last-Modified": format_datetime(mtime, usegmt=True), 72 + } 73 + return 200, (b"" if head else data), headers 74 + 75 + async def put( 76 + self, path: str, body: bytes 77 + ) -> tuple[int, bytes, dict[str, str]]: 78 + target = self._local(path) 79 + target.parent.mkdir(parents=True, exist_ok=True) 80 + new = not target.exists() 81 + target.write_bytes(body) 82 + return (201 if new else 204), b"", {} 83 + 84 + async def delete(self, path: str) -> tuple[int, bytes, dict[str, str]]: 85 + target = self._local(path) 86 + if not target.exists(): 87 + return 404, b"", {} 88 + if target.is_dir(): 89 + _rmtree(target) 90 + else: 91 + target.unlink() 92 + return 204, b"", {} 93 + 94 + async def mkcol(self, path: str) -> tuple[int, bytes, dict[str, str]]: 95 + target = self._local(path) 96 + if target.exists(): 97 + return 405, b"", {} 98 + target.mkdir(parents=True) 99 + return 201, b"", {} 100 + 101 + 102 + # --------------------------------------------------------------------------- 103 + # XML helpers 104 + # --------------------------------------------------------------------------- 105 + 106 + def _entry_xml(local: Path, href: str, root: Path) -> str: 107 + is_dir = local.is_dir() 108 + resourcetype = "<D:collection/>" if is_dir else "" 109 + mtime = datetime.fromtimestamp(local.stat().st_mtime, tz=UTC) 110 + lastmod = format_datetime(mtime, usegmt=True) 111 + size_xml = "" 112 + ct_xml = "" 113 + if not is_dir: 114 + size = local.stat().st_size 115 + size_xml = f"<D:getcontentlength>{size}</D:getcontentlength>" 116 + ct_xml = "<D:getcontenttype>application/octet-stream</D:getcontenttype>" 117 + return ( 118 + "<D:response>" 119 + f"<D:href>{quote(href)}</D:href>" 120 + "<D:propstat>" 121 + "<D:prop>" 122 + f"<D:resourcetype>{resourcetype}</D:resourcetype>" 123 + f"<D:displayname>{local.name or 'root'}</D:displayname>" 124 + f"<D:getlastmodified>{lastmod}</D:getlastmodified>" 125 + f"{size_xml}{ct_xml}" 126 + "</D:prop>" 127 + "<D:status>HTTP/1.1 200 OK</D:status>" 128 + "</D:propstat>" 129 + "</D:response>" 130 + ) 131 + 132 + 133 + def _multistatus(responses: list[str]) -> bytes: 134 + body = ( 135 + '<?xml version="1.0" encoding="utf-8"?>' 136 + '<D:multistatus xmlns:D="DAV:">' 137 + + "".join(responses) 138 + + "</D:multistatus>" 139 + ) 140 + return body.encode("utf-8") 141 + 142 + 143 + def _rmtree(p: Path) -> None: 144 + for child in p.iterdir(): 145 + if child.is_dir(): 146 + _rmtree(child) 147 + else: 148 + child.unlink() 149 + p.rmdir()
+142
src/waggle/adapters/webdav/router.py
··· 1 + """WebDAV method dispatch. 2 + 3 + Paths under `/Books/.Moon+/Cache/` are virtual — served from bookhive records. 4 + Everything else hits the local-disk passthrough. FastAPI's `api_route` with 5 + custom `methods=[...]` lets us register verbs like PROPFIND/LOCK that aren't 6 + in the default HTTP verb list. 7 + """ 8 + 9 + from __future__ import annotations 10 + 11 + import logging 12 + import uuid 13 + from dataclasses import dataclass 14 + 15 + from fastapi import APIRouter, Request, Response 16 + 17 + from waggle.atproto.client import ATProtoClient 18 + 19 + from . import moonreader 20 + from .passthrough import Passthrough 21 + 22 + log = logging.getLogger(__name__) 23 + 24 + 25 + @dataclass 26 + class DAVContext: 27 + client: ATProtoClient 28 + passthrough: Passthrough 29 + 30 + 31 + def make_router(ctx: DAVContext) -> APIRouter: 32 + router = APIRouter() 33 + 34 + def use_moonreader(path: str) -> bool: 35 + return moonreader.under_prefix(path) 36 + 37 + # ---- OPTIONS -------------------------------------------------------- 38 + @router.api_route("/{path:path}", methods=["OPTIONS"], include_in_schema=False) 39 + async def options(path: str) -> Response: 40 + return Response( 41 + status_code=200, 42 + headers={ 43 + "DAV": "1, 2", 44 + "Allow": "OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, MKCOL, LOCK, UNLOCK", 45 + "MS-Author-Via": "DAV", 46 + }, 47 + ) 48 + 49 + # ---- PROPFIND ------------------------------------------------------- 50 + @router.api_route("/{path:path}", methods=["PROPFIND"], include_in_schema=False) 51 + async def propfind(path: str, request: Request) -> Response: 52 + dav_path = "/" + path 53 + depth = request.headers.get("Depth", "1") 54 + if use_moonreader(dav_path): 55 + status, body, headers = await moonreader.propfind(ctx.client, dav_path, depth) 56 + else: 57 + status, body, headers = await ctx.passthrough.propfind(dav_path, depth) 58 + return Response(status_code=status, content=body, headers=headers) 59 + 60 + # ---- GET / HEAD ----------------------------------------------------- 61 + @router.api_route("/{path:path}", methods=["GET"], include_in_schema=False) 62 + async def get(path: str) -> Response: 63 + dav_path = "/" + path 64 + if use_moonreader(dav_path): 65 + status, body, headers = await moonreader.get(ctx.client, dav_path) 66 + else: 67 + status, body, headers = await ctx.passthrough.get(dav_path) 68 + return Response(status_code=status, content=body, headers=headers) 69 + 70 + @router.api_route("/{path:path}", methods=["HEAD"], include_in_schema=False) 71 + async def head(path: str) -> Response: 72 + dav_path = "/" + path 73 + if use_moonreader(dav_path): 74 + status, body, headers = await moonreader.get(ctx.client, dav_path, head=True) 75 + else: 76 + status, body, headers = await ctx.passthrough.get(dav_path, head=True) 77 + return Response(status_code=status, content=body, headers=headers) 78 + 79 + # ---- PUT ------------------------------------------------------------ 80 + @router.api_route("/{path:path}", methods=["PUT"], include_in_schema=False) 81 + async def put(path: str, request: Request) -> Response: 82 + dav_path = "/" + path 83 + body = await request.body() 84 + if use_moonreader(dav_path): 85 + status, resp_body, headers = await moonreader.put(ctx.client, dav_path, body) 86 + else: 87 + status, resp_body, headers = await ctx.passthrough.put(dav_path, body) 88 + return Response(status_code=status, content=resp_body, headers=headers) 89 + 90 + # ---- DELETE --------------------------------------------------------- 91 + @router.api_route("/{path:path}", methods=["DELETE"], include_in_schema=False) 92 + async def delete(path: str) -> Response: 93 + dav_path = "/" + path 94 + if use_moonreader(dav_path): 95 + status, body, headers = await moonreader.delete(ctx.client, dav_path) 96 + else: 97 + status, body, headers = await ctx.passthrough.delete(dav_path) 98 + return Response(status_code=status, content=body, headers=headers) 99 + 100 + # ---- MKCOL ---------------------------------------------------------- 101 + @router.api_route("/{path:path}", methods=["MKCOL"], include_in_schema=False) 102 + async def mkcol(path: str) -> Response: 103 + dav_path = "/" + path 104 + if use_moonreader(dav_path): 105 + # The Moon+ cache dir is virtual; creating subdirs is meaningless. 106 + return Response(status_code=405) 107 + status, body, headers = await ctx.passthrough.mkcol(dav_path) 108 + return Response(status_code=status, content=body, headers=headers) 109 + 110 + # ---- LOCK / UNLOCK -------------------------------------------------- 111 + # WebDAV Class 2. Some clients fail hard without LOCK support; we return 112 + # a synthetic lock token that's never actually enforced (no concurrent 113 + # writers matter for a single-user homelab sync). 114 + @router.api_route("/{path:path}", methods=["LOCK"], include_in_schema=False) 115 + async def lock(path: str) -> Response: 116 + token = f"opaquelocktoken:{uuid.uuid4()}" 117 + body = ( 118 + '<?xml version="1.0" encoding="utf-8"?>' 119 + '<D:prop xmlns:D="DAV:">' 120 + "<D:lockdiscovery><D:activelock>" 121 + "<D:locktype><D:write/></D:locktype>" 122 + "<D:lockscope><D:exclusive/></D:lockscope>" 123 + "<D:depth>infinity</D:depth>" 124 + "<D:timeout>Second-3600</D:timeout>" 125 + f"<D:locktoken><D:href>{token}</D:href></D:locktoken>" 126 + "</D:activelock></D:lockdiscovery>" 127 + "</D:prop>" 128 + ).encode() 129 + return Response( 130 + status_code=200, 131 + content=body, 132 + headers={ 133 + "Lock-Token": f"<{token}>", 134 + "Content-Type": "application/xml; charset=utf-8", 135 + }, 136 + ) 137 + 138 + @router.api_route("/{path:path}", methods=["UNLOCK"], include_in_schema=False) 139 + async def unlock(path: str) -> Response: 140 + return Response(status_code=204) 141 + 142 + return router
src/waggle/atproto/__init__.py

This is a binary file and will not be displayed.

+457
src/waggle/atproto/bookhive.py
··· 1 + """Translation layer between Moon+ Reader `.po` files and buzz.bookhive.book records. 2 + 3 + The parsing / matching helpers are lifted (with light edits) from 4 + `../moon2hive/moon2hive.py`. The ATProto calls are rewritten to go through 5 + `ATProtoClient` instead of the old sync `requests` path. 6 + 7 + Namespace contract: 8 + 9 + bookProgress.moonReader.{position, file, syncedAt} 10 + 11 + `moonReader.position` is the raw `.po` content byte-for-byte. We preserve it on 12 + write so GETs (which return this verbatim) continue to produce a file Moon+ 13 + Reader recognizes — including its internal `timestamp_ms` which is the ebook 14 + file's import mtime, not a real timestamp. See the moon2hive CLAUDE.md for the 15 + full story. 16 + """ 17 + 18 + from __future__ import annotations 19 + 20 + import logging 21 + import re 22 + import time 23 + from dataclasses import dataclass 24 + from datetime import UTC, datetime 25 + 26 + import httpx 27 + 28 + from .client import ATProtoClient 29 + 30 + log = logging.getLogger(__name__) 31 + 32 + BOOKHIVE_COLLECTION = "buzz.bookhive.book" 33 + BOOKHIVE_CATALOG_URL = "https://bookhive.buzz/xrpc/buzz.bookhive.searchBooks" 34 + 35 + # Records cache: single-user waggle, so a module-level dict suffices. 36 + _RECORDS_CACHE: dict[str, tuple[float, list[dict]]] = {} 37 + RECORDS_TTL = 30.0 38 + 39 + 40 + # --------------------------------------------------------------------------- 41 + # `.po` parse / serialize 42 + # --------------------------------------------------------------------------- 43 + 44 + PO_PATTERN = re.compile(r"^(\d+)\*(\d+)@(\d+)#(\d+):([\d.]+)%$") 45 + 46 + 47 + @dataclass 48 + class ReadingProgress: 49 + timestamp_ms: int 50 + chapter: int # 0-indexed (Moon+ convention) 51 + volume: int 52 + char_offset: int 53 + percentage: float 54 + raw: str 55 + 56 + 57 + def parse_po(content: str) -> ReadingProgress | None: 58 + m = PO_PATTERN.match(content.strip()) 59 + if not m: 60 + log.warning("Could not parse .po content: %r", content) 61 + return None 62 + return ReadingProgress( 63 + timestamp_ms=int(m.group(1)), 64 + chapter=int(m.group(2)), 65 + volume=int(m.group(3)), 66 + char_offset=int(m.group(4)), 67 + percentage=float(m.group(5)), 68 + raw=content.strip(), 69 + ) 70 + 71 + 72 + def serialize_po(record_value: dict) -> str: 73 + """Produce the .po bytes for a GET response. 74 + 75 + Prefers the stored raw `moonReader.position` (so the internal timestamp and 76 + char offset Moon+ Reader wrote last time round-trip perfectly). Falls back 77 + to a minimal synthesis for records that were never touched by Moon+ Reader 78 + but have bookhive progress (e.g. edited in the bookhive UI). 79 + """ 80 + bp = record_value.get("bookProgress") or {} 81 + moon = bp.get("moonReader") or {} 82 + raw = moon.get("position") 83 + if raw: 84 + return raw 85 + 86 + # Synthesize. Moon+ Reader tolerates timestamp_ms=0 and char_offset=0. 87 + percent = float(bp.get("percent", 0)) 88 + chapter_1idx = int(bp.get("currentChapter", 1)) 89 + chapter = max(0, chapter_1idx - 1) 90 + return f"0*{chapter}@0#0:{percent:.1f}%" 91 + 92 + 93 + # --------------------------------------------------------------------------- 94 + # Filename parsing and fuzzy matching 95 + # --------------------------------------------------------------------------- 96 + 97 + FORMAT_EXTS = { 98 + ".epub", ".mobi", ".azw3", ".azw", ".pdf", 99 + ".fb2", ".djvu", ".cbz", ".cbr", ".txt", 100 + } 101 + 102 + 103 + def parse_filename(filename: str) -> tuple[str, str]: 104 + """Extract (part_a, part_b) — usually (title, author) — from a Moon+ filename. 105 + 106 + Ordering isn't guaranteed (both `Title - Author.epub.po` and 107 + `Author - Title.epub.po` appear in the wild), so callers should try both. 108 + """ 109 + name = filename 110 + if name.endswith(".po"): 111 + name = name[:-3] 112 + for ext in FORMAT_EXTS: 113 + if name.endswith(ext): 114 + name = name[: -len(ext)] 115 + break 116 + 117 + # Strip series prefix like "(Dungeon Crawler Carl 1) " 118 + name = re.sub(r"^\([^)]+\)\s*", "", name) 119 + 120 + parts = name.split(" - ") 121 + if len(parts) >= 2: 122 + part_b = parts[-1].strip() 123 + part_a = " - ".join(parts[:-1]).strip() 124 + else: 125 + part_a = name.strip() 126 + part_b = "" 127 + 128 + # Moon+ Reader replaces ":" with "_" in filenames 129 + part_a = part_a.replace("_", ": ").replace(": ", ": ") 130 + part_b = part_b.replace("_", ": ").replace(": ", ": ") 131 + 132 + # "Last, First" → "First Last" 133 + if "," in part_b and part_b.count(",") == 1: 134 + last, first = part_b.split(",", 1) 135 + part_b = f"{first.strip()} {last.strip()}" 136 + 137 + # "Thing, The" → "The Thing" 138 + if part_a.endswith(", The"): 139 + part_a = "The " + part_a[:-5] 140 + elif part_a.endswith(", A"): 141 + part_a = "A " + part_a[:-3] 142 + 143 + # Strip trailing digits that are truncation artifacts ("Esthe13") 144 + part_a = re.sub(r"\d+$", "", part_a).strip() 145 + part_b = re.sub(r"\d+$", "", part_b).strip() 146 + 147 + return part_a, part_b 148 + 149 + 150 + def _normalize(text: str) -> str: 151 + text = text.lower() 152 + text = re.sub(r"[^\w\s]", " ", text) 153 + text = re.sub(r"\s+", " ", text).strip() 154 + for article in ("the ", "a ", "an "): 155 + if text.startswith(article): 156 + text = text[len(article):] 157 + break 158 + return text 159 + 160 + 161 + def _word_set(text: str) -> set[str]: 162 + return {w for w in _normalize(text).split() if len(w) > 2} 163 + 164 + 165 + def _title_similarity(a: str, b: str) -> float: 166 + wa, wb = _word_set(a), _word_set(b) 167 + if not wa or not wb: 168 + return 0.0 169 + return len(wa & wb) / min(len(wa), len(wb)) 170 + 171 + 172 + def _author_matches(a: str, b: str) -> bool: 173 + if not a or not b: 174 + return False 175 + na, nb = _normalize(a), _normalize(b) 176 + sa = na.split()[-1] if na.split() else "" 177 + sb = nb.split()[-1] if nb.split() else "" 178 + return bool(sa) and len(sa) > 2 and (sa in nb or sb in na) 179 + 180 + 181 + def match_record( 182 + part_a: str, part_b: str, records: list[dict] 183 + ) -> dict | None: 184 + """Best-effort fuzzy match. Tries both (title,author) orderings.""" 185 + best: dict | None = None 186 + best_score = 0.0 187 + for record in records: 188 + v = record["value"] 189 + rec_title = v.get("title", "") 190 + rec_authors = v.get("authors", "") 191 + candidates = [ 192 + (_title_similarity(part_a, rec_title), _author_matches(part_b, rec_authors)), 193 + (_title_similarity(part_b, rec_title), _author_matches(part_a, rec_authors)), 194 + ] 195 + for score, auth_ok in candidates: 196 + if score > best_score and ((auth_ok and score > 0.6) or score > 0.85): 197 + best_score = score 198 + best = record 199 + if best: 200 + log.info( 201 + "Fuzzy-matched %r → %r (score=%.2f)", 202 + f"{part_a} / {part_b}", best["value"].get("title"), best_score, 203 + ) 204 + return best 205 + 206 + 207 + # --------------------------------------------------------------------------- 208 + # ATProto record operations 209 + # --------------------------------------------------------------------------- 210 + 211 + async def list_records(client: ATProtoClient, *, use_cache: bool = True) -> list[dict]: 212 + """Fetch all buzz.bookhive.book records for the authed user. 30s cached.""" 213 + did = await client.did() 214 + now = time.time() 215 + if use_cache: 216 + cached = _RECORDS_CACHE.get(did) 217 + if cached and now - cached[0] < RECORDS_TTL: 218 + return cached[1] 219 + 220 + records: list[dict] = [] 221 + cursor: str | None = None 222 + while True: 223 + params: dict = { 224 + "repo": did, 225 + "collection": BOOKHIVE_COLLECTION, 226 + "limit": 100, 227 + } 228 + if cursor: 229 + params["cursor"] = cursor 230 + resp = await client.request("GET", "com.atproto.repo.listRecords", params=params) 231 + resp.raise_for_status() 232 + data = resp.json() 233 + records.extend(data.get("records", [])) 234 + cursor = data.get("cursor") 235 + if not cursor: 236 + break 237 + 238 + _RECORDS_CACHE[did] = (now, records) 239 + log.debug("Cached %d bookhive records for %s", len(records), did) 240 + return records 241 + 242 + 243 + def invalidate_cache(did: str | None = None) -> None: 244 + if did is None: 245 + _RECORDS_CACHE.clear() 246 + else: 247 + _RECORDS_CACHE.pop(did, None) 248 + 249 + 250 + def find_by_moon_filename(records: list[dict], filename: str) -> dict | None: 251 + """Direct lookup: stored moonReader.file == filename.""" 252 + for r in records: 253 + moon = (r["value"].get("bookProgress") or {}).get("moonReader") or {} 254 + if moon.get("file") == filename: 255 + return r 256 + return None 257 + 258 + 259 + async def resolve_record( 260 + client: ATProtoClient, filename: str 261 + ) -> dict | None: 262 + """Find the bookhive record that corresponds to a Moon+ filename. 263 + 264 + First: exact match on stored `moonReader.file` (fast path, survives renames 265 + of the bookhive record's title). Fallback: fuzzy match on title/author 266 + parsed from filename. 267 + """ 268 + records = await list_records(client) 269 + direct = find_by_moon_filename(records, filename) 270 + if direct: 271 + return direct 272 + part_a, part_b = parse_filename(filename) 273 + return match_record(part_a, part_b, records) 274 + 275 + 276 + async def search_catalog(client: ATProtoClient, query: str) -> dict | None: 277 + """Search bookhive.buzz's catalog for a book by title. Returns top hit.""" 278 + resp = await client.http.get(BOOKHIVE_CATALOG_URL, params={"q": query, "limit": 5}) 279 + resp.raise_for_status() 280 + books = resp.json().get("books", []) 281 + return books[0] if books else None 282 + 283 + 284 + async def upload_cover(client: ATProtoClient, cover_url: str) -> dict | None: 285 + """Download a cover image URL and upload as a PDS blob. Returns blob ref.""" 286 + try: 287 + img = await client.http.get(cover_url) 288 + img.raise_for_status() 289 + except httpx.HTTPError as e: 290 + log.warning("Failed to download cover %s: %s", cover_url, e) 291 + return None 292 + content_type = img.headers.get("Content-Type", "image/jpeg") 293 + if "image" not in content_type: 294 + content_type = "image/jpeg" 295 + resp = await client.request( 296 + "POST", "com.atproto.repo.uploadBlob", 297 + content=img.content, headers={"Content-Type": content_type}, 298 + ) 299 + if resp.status_code >= 400: 300 + log.warning("Failed to upload cover blob: %s", resp.text) 301 + return None 302 + return resp.json().get("blob") 303 + 304 + 305 + # --------------------------------------------------------------------------- 306 + # Write path (PUT /...po → updated record on PDS) 307 + # --------------------------------------------------------------------------- 308 + 309 + def _now_iso() -> str: 310 + return datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S.000Z") 311 + 312 + 313 + def _build_moon_reader(progress: ReadingProgress, filename: str, now: str) -> dict: 314 + return {"position": progress.raw, "file": filename, "syncedAt": now} 315 + 316 + 317 + def _merge_progress( 318 + existing_value: dict, progress: ReadingProgress, filename: str 319 + ) -> tuple[dict, bool]: 320 + """Return (new_value, changed). Skips the write if nothing moved.""" 321 + value = dict(existing_value) 322 + now = _now_iso() 323 + 324 + existing_bp = value.get("bookProgress") or {} 325 + existing_moon = existing_bp.get("moonReader") or {} 326 + 327 + unchanged = ( 328 + existing_moon.get("position") == progress.raw 329 + and existing_moon.get("file") == filename 330 + and existing_bp.get("percent") == int(progress.percentage) 331 + ) 332 + if unchanged and value.get("status") == "buzz.bookhive.defs#reading": 333 + return value, False 334 + 335 + # Respect "finished" — don't flip completed books back to reading. 336 + if value.get("status") != "buzz.bookhive.defs#finished": 337 + value["status"] = "buzz.bookhive.defs#reading" 338 + if not value.get("startedAt"): 339 + value["startedAt"] = now 340 + 341 + value["bookProgress"] = { 342 + "percent": int(progress.percentage), 343 + "currentChapter": progress.chapter + 1, 344 + "updatedAt": now, 345 + "moonReader": _build_moon_reader(progress, filename, now), 346 + } 347 + return value, True 348 + 349 + 350 + async def put_record(client: ATProtoClient, rkey: str, value: dict) -> None: 351 + did = await client.did() 352 + resp = await client.request( 353 + "POST", "com.atproto.repo.putRecord", 354 + json={ 355 + "repo": did, 356 + "collection": BOOKHIVE_COLLECTION, 357 + "rkey": rkey, 358 + "record": value, 359 + }, 360 + ) 361 + resp.raise_for_status() 362 + invalidate_cache(did) 363 + 364 + 365 + async def create_record(client: ATProtoClient, value: dict) -> str: 366 + did = await client.did() 367 + resp = await client.request( 368 + "POST", "com.atproto.repo.createRecord", 369 + json={"repo": did, "collection": BOOKHIVE_COLLECTION, "record": value}, 370 + ) 371 + resp.raise_for_status() 372 + invalidate_cache(did) 373 + uri = resp.json().get("uri", "") 374 + return uri.rsplit("/", 1)[-1] 375 + 376 + 377 + async def apply_po_put( 378 + client: ATProtoClient, filename: str, body: bytes 379 + ) -> str: 380 + """Full inbound PUT flow: parse → resolve/create record → update bookProgress. 381 + 382 + Returns a short human status string for logging. 383 + """ 384 + content = body.decode("utf-8", errors="replace").strip() 385 + progress = parse_po(content) 386 + if not progress: 387 + return f"ignored unparsable .po ({len(body)} bytes)" 388 + 389 + record = await resolve_record(client, filename) 390 + if record: 391 + rkey = record["uri"].rsplit("/", 1)[-1] 392 + new_value, changed = _merge_progress(record["value"], progress, filename) 393 + if not changed: 394 + return f"no-op (already at {int(progress.percentage)}%)" 395 + await put_record(client, rkey, new_value) 396 + return f"updated {record['value'].get('title')!r} → {int(progress.percentage)}%" 397 + 398 + # Unknown book — try the bookhive catalog. 399 + part_a, part_b = parse_filename(filename) 400 + catalog = await search_catalog(client, part_a) 401 + if not catalog and part_b: 402 + catalog = await search_catalog(client, part_b) 403 + if not catalog: 404 + log.warning("No bookhive catalog match for %r; dropping PUT", filename) 405 + return f"dropped (no catalog match for {part_a!r})" 406 + 407 + now = _now_iso() 408 + hive_id = catalog.get("id", "") 409 + value: dict = { 410 + "$type": BOOKHIVE_COLLECTION, 411 + "title": catalog.get("title", part_a or "Unknown"), 412 + "authors": catalog.get("authors", part_b or "Unknown"), 413 + "hiveId": hive_id, 414 + "createdAt": now, 415 + "startedAt": now, 416 + "status": "buzz.bookhive.defs#reading", 417 + "owned": True, 418 + "bookProgress": { 419 + "percent": int(progress.percentage), 420 + "currentChapter": progress.chapter + 1, 421 + "updatedAt": now, 422 + "moonReader": _build_moon_reader(progress, filename, now), 423 + }, 424 + } 425 + if catalog.get("identifiers"): 426 + value["identifiers"] = catalog["identifiers"] 427 + if hive_id: 428 + value["hiveBookUri"] = ( 429 + f"at://did:plc:enu2j5xjlqsjaylv3du4myh4/buzz.bookhive.catalogBook/{hive_id}" 430 + ) 431 + cover_url = catalog.get("cover") 432 + if cover_url: 433 + blob = await upload_cover(client, cover_url) 434 + if blob: 435 + value["cover"] = blob 436 + 437 + rkey = await create_record(client, value) 438 + return f"created {value['title']!r} at {int(progress.percentage)}% (rkey={rkey})" 439 + 440 + 441 + async def apply_po_delete(client: ATProtoClient, filename: str) -> str: 442 + """DELETE /...po — clear only the moonReader sub-object; leave record intact.""" 443 + record = await resolve_record(client, filename) 444 + if not record: 445 + return "no-op (no matching record)" 446 + value = dict(record["value"]) 447 + bp = dict(value.get("bookProgress") or {}) 448 + if "moonReader" not in bp: 449 + return "no-op (no moonReader data)" 450 + bp.pop("moonReader", None) 451 + if bp: 452 + value["bookProgress"] = bp 453 + else: 454 + value.pop("bookProgress", None) 455 + rkey = record["uri"].rsplit("/", 1)[-1] 456 + await put_record(client, rkey, value) 457 + return f"cleared moonReader on {value.get('title')!r}"
+120
src/waggle/atproto/client.py
··· 1 + """ATProto session + XRPC client. 2 + 3 + One lazy, module-scoped session. Refresh on 401; createSession if refresh also 4 + fails. No persistence — a restart re-auths on first request. 5 + """ 6 + 7 + from __future__ import annotations 8 + 9 + import asyncio 10 + import logging 11 + from dataclasses import dataclass 12 + 13 + import httpx 14 + 15 + log = logging.getLogger(__name__) 16 + 17 + TIMEOUT = 15.0 18 + 19 + 20 + @dataclass 21 + class Session: 22 + access_jwt: str 23 + refresh_jwt: str 24 + did: str 25 + 26 + 27 + class ATProtoClient: 28 + def __init__(self, pds: str, handle: str, app_password: str) -> None: 29 + self._pds = pds.rstrip("/") 30 + self._handle = handle 31 + self._app_password = app_password 32 + self._session: Session | None = None 33 + self._lock = asyncio.Lock() 34 + self._http = httpx.AsyncClient(timeout=TIMEOUT) 35 + 36 + @property 37 + def pds_url(self) -> str: 38 + return f"https://{self._pds}" 39 + 40 + @property 41 + def http(self) -> httpx.AsyncClient: 42 + """Shared unauthenticated HTTP client (e.g. for bookhive catalog, blob downloads).""" 43 + return self._http 44 + 45 + async def close(self) -> None: 46 + await self._http.aclose() 47 + 48 + async def did(self) -> str: 49 + sess = await self._ensure_session() 50 + return sess.did 51 + 52 + async def _ensure_session(self) -> Session: 53 + if self._session is not None: 54 + return self._session 55 + async with self._lock: 56 + if self._session is None: 57 + self._session = await self._create_session() 58 + return self._session 59 + 60 + async def _create_session(self) -> Session: 61 + log.info("Authenticating to %s as %s", self._pds, self._handle) 62 + resp = await self._http.post( 63 + f"{self.pds_url}/xrpc/com.atproto.server.createSession", 64 + json={"identifier": self._handle, "password": self._app_password}, 65 + ) 66 + resp.raise_for_status() 67 + data = resp.json() 68 + return Session( 69 + access_jwt=data["accessJwt"], 70 + refresh_jwt=data["refreshJwt"], 71 + did=data["did"], 72 + ) 73 + 74 + async def _refresh_session(self) -> None: 75 + assert self._session is not None 76 + log.debug("Refreshing ATProto session") 77 + resp = await self._http.post( 78 + f"{self.pds_url}/xrpc/com.atproto.server.refreshSession", 79 + headers={"Authorization": f"Bearer {self._session.refresh_jwt}"}, 80 + ) 81 + if resp.status_code >= 400: 82 + log.info("refreshSession failed (%s); re-creating", resp.status_code) 83 + self._session = await self._create_session() 84 + return 85 + data = resp.json() 86 + self._session = Session( 87 + access_jwt=data["accessJwt"], 88 + refresh_jwt=data["refreshJwt"], 89 + did=data["did"], 90 + ) 91 + 92 + async def request( 93 + self, 94 + method: str, 95 + nsid: str, 96 + *, 97 + params: dict | None = None, 98 + json: dict | None = None, 99 + content: bytes | None = None, 100 + headers: dict | None = None, 101 + ) -> httpx.Response: 102 + """XRPC call with auto-refresh on 401.""" 103 + sess = await self._ensure_session() 104 + url = f"{self.pds_url}/xrpc/{nsid}" 105 + hdrs = {"Authorization": f"Bearer {sess.access_jwt}"} 106 + if headers: 107 + hdrs.update(headers) 108 + 109 + resp = await self._http.request( 110 + method, url, params=params, json=json, content=content, headers=hdrs 111 + ) 112 + if resp.status_code == 401: 113 + async with self._lock: 114 + await self._refresh_session() 115 + assert self._session is not None 116 + hdrs["Authorization"] = f"Bearer {self._session.access_jwt}" 117 + resp = await self._http.request( 118 + method, url, params=params, json=json, content=content, headers=hdrs 119 + ) 120 + return resp
+30
src/waggle/auth.py
··· 1 + """HTTP Basic gate on all DAV requests. 2 + 3 + Single shared credential — waggle only serves one ATProto identity, so the 4 + Basic auth here just lets Moon+ Reader say "I'm allowed to talk to this box." 5 + It does not map to different atproto accounts. 6 + """ 7 + 8 + from __future__ import annotations 9 + 10 + import base64 11 + import hmac 12 + 13 + from fastapi import Request, Response 14 + 15 + 16 + def check(request: Request, dav_user: str, dav_password: str) -> bool: 17 + """Return True if the request's Basic credentials match config.""" 18 + header = request.headers.get("Authorization", "") 19 + if not header.lower().startswith("basic "): 20 + return False 21 + try: 22 + raw = base64.b64decode(header.split(" ", 1)[1]).decode("utf-8") 23 + except Exception: 24 + return False 25 + user, _, pw = raw.partition(":") 26 + return hmac.compare_digest(user, dav_user) and hmac.compare_digest(pw, dav_password) 27 + 28 + 29 + def challenge() -> Response: 30 + return Response(status_code=401, headers={"WWW-Authenticate": 'Basic realm="waggle"'})
+49
src/waggle/config.py
··· 1 + """Env-only config. Loaded once at startup; immutable thereafter.""" 2 + 3 + from __future__ import annotations 4 + 5 + import os 6 + from dataclasses import dataclass 7 + 8 + from dotenv import load_dotenv 9 + 10 + 11 + @dataclass(frozen=True) 12 + class Config: 13 + # ATProto identity waggle writes to 14 + pds: str 15 + bsky_handle: str 16 + bsky_app_password: str 17 + 18 + # Shared DAV credential — Moon+ Reader sends this in Basic auth 19 + dav_user: str 20 + dav_password: str 21 + 22 + # Local-disk fallback for non-.po DAV paths 23 + passthrough_root: str 24 + 25 + log_level: str 26 + 27 + 28 + def load() -> Config: 29 + load_dotenv() 30 + required = [ 31 + "PDS", 32 + "BSKY_HANDLE", 33 + "BSKY_APP_PASSWORD", 34 + "DAV_USER", 35 + "DAV_PASSWORD", 36 + ] 37 + missing = [k for k in required if not os.environ.get(k)] 38 + if missing: 39 + raise RuntimeError(f"Missing required env vars: {', '.join(missing)}") 40 + 41 + return Config( 42 + pds=os.environ["PDS"], 43 + bsky_handle=os.environ["BSKY_HANDLE"], 44 + bsky_app_password=os.environ["BSKY_APP_PASSWORD"], 45 + dav_user=os.environ["DAV_USER"], 46 + dav_password=os.environ["DAV_PASSWORD"], 47 + passthrough_root=os.environ.get("PASSTHROUGH_ROOT", "/data/passthrough"), 48 + log_level=os.environ.get("LOG_LEVEL", "INFO"), 49 + )
+59
src/waggle/main.py
··· 1 + """FastAPI entrypoint: wires config, auth middleware, and the WebDAV router.""" 2 + 3 + from __future__ import annotations 4 + 5 + import logging 6 + from contextlib import asynccontextmanager 7 + 8 + from fastapi import FastAPI, Request 9 + from starlette.middleware.base import BaseHTTPMiddleware 10 + 11 + from waggle import auth, config 12 + from waggle.adapters.webdav.passthrough import Passthrough 13 + from waggle.adapters.webdav.router import DAVContext, make_router 14 + from waggle.atproto.client import ATProtoClient 15 + 16 + 17 + def create_app() -> FastAPI: 18 + cfg = config.load() 19 + logging.basicConfig( 20 + level=getattr(logging, cfg.log_level.upper(), logging.INFO), 21 + format="%(asctime)s %(levelname)s %(name)s: %(message)s", 22 + datefmt="%H:%M:%S", 23 + ) 24 + log = logging.getLogger("waggle") 25 + 26 + client = ATProtoClient(cfg.pds, cfg.bsky_handle, cfg.bsky_app_password) 27 + passthrough = Passthrough(cfg.passthrough_root) 28 + ctx = DAVContext(client=client, passthrough=passthrough) 29 + 30 + @asynccontextmanager 31 + async def lifespan(_: FastAPI): 32 + log.info("waggle starting (pds=%s handle=%s)", cfg.pds, cfg.bsky_handle) 33 + try: 34 + yield 35 + finally: 36 + await client.close() 37 + log.info("waggle stopped") 38 + 39 + app = FastAPI(lifespan=lifespan, openapi_url=None, docs_url=None, redoc_url=None) 40 + 41 + class BasicAuthMiddleware(BaseHTTPMiddleware): 42 + async def dispatch(self, request: Request, call_next): 43 + if request.url.path == "/healthz": 44 + return await call_next(request) 45 + if not auth.check(request, cfg.dav_user, cfg.dav_password): 46 + return auth.challenge() 47 + return await call_next(request) 48 + 49 + app.add_middleware(BasicAuthMiddleware) 50 + 51 + @app.get("/healthz", include_in_schema=False) 52 + async def healthz() -> dict: 53 + return {"ok": True} 54 + 55 + app.include_router(make_router(ctx)) 56 + return app 57 + 58 + 59 + app = create_app()
tests/__init__.py

This is a binary file and will not be displayed.

+97
tests/test_bookhive_parsing.py
··· 1 + """Pure-function tests for `.po` parsing and filename heuristics.""" 2 + 3 + from waggle.atproto import bookhive 4 + 5 + 6 + class TestParsePo: 7 + def test_round_trip(self) -> None: 8 + raw = "1703297605115*24@0#0:30.0%" 9 + p = bookhive.parse_po(raw) 10 + assert p is not None 11 + assert p.timestamp_ms == 1703297605115 12 + assert p.chapter == 24 13 + assert p.volume == 0 14 + assert p.char_offset == 0 15 + assert p.percentage == 30.0 16 + assert p.raw == raw 17 + 18 + def test_percent_with_fraction(self) -> None: 19 + p = bookhive.parse_po("1000*5@0#123:79.7%") 20 + assert p is not None 21 + assert p.percentage == 79.7 22 + 23 + def test_rejects_garbage(self) -> None: 24 + assert bookhive.parse_po("not a po file") is None 25 + 26 + 27 + class TestSerializePo: 28 + def test_prefers_stored_raw(self) -> None: 29 + """When moonReader.position exists, return it byte-for-byte. 30 + 31 + This matters: the internal timestamp is the ebook's import mtime and 32 + Moon+ Reader recognizes the file by that value. Re-synthesizing would 33 + trip false "position changed" events. 34 + """ 35 + record = { 36 + "bookProgress": { 37 + "percent": 30, 38 + "currentChapter": 25, 39 + "moonReader": {"position": "1703297605115*24@0#0:30.0%"}, 40 + } 41 + } 42 + assert bookhive.serialize_po(record) == "1703297605115*24@0#0:30.0%" 43 + 44 + def test_synthesizes_when_moonreader_missing(self) -> None: 45 + record = {"bookProgress": {"percent": 42, "currentChapter": 5}} 46 + # Chapter goes 1-indexed → 0-indexed on the way out. 47 + assert bookhive.serialize_po(record) == "0*4@0#0:42.0%" 48 + 49 + def test_synthesizes_zero_progress(self) -> None: 50 + assert bookhive.serialize_po({}) == "0*0@0#0:0.0%" 51 + 52 + 53 + class TestParseFilename: 54 + def test_title_dash_author(self) -> None: 55 + assert bookhive.parse_filename( 56 + "The Lesser Dead - Christopher Buehlman.epub.po" 57 + ) == ("The Lesser Dead", "Christopher Buehlman") 58 + 59 + def test_series_prefix_stripped(self) -> None: 60 + assert bookhive.parse_filename( 61 + "(Dungeon Crawler Carl 1) Matt Dinniman - Dungeon Crawler Carl.epub.po" 62 + ) == ("Matt Dinniman", "Dungeon Crawler Carl") 63 + 64 + def test_dashes_in_title(self) -> None: 65 + assert bookhive.parse_filename( 66 + "Cultish - The Language of Fanaticism - Amanda Montell.epub.po" 67 + ) == ("Cultish - The Language of Fanaticism", "Amanda Montell") 68 + 69 + def test_comma_last_first_author(self) -> None: 70 + a, b = bookhive.parse_filename("Book Title - Doe, Jane.epub.po") 71 + assert a == "Book Title" 72 + assert b == "Jane Doe" 73 + 74 + def test_article_suffix(self) -> None: 75 + a, _ = bookhive.parse_filename("Lesser Dead, The - Christopher Buehlman.epub.po") 76 + assert a == "The Lesser Dead" 77 + 78 + 79 + class TestMatchRecord: 80 + RECORDS = [ 81 + {"value": {"title": "The Lesser Dead", "authors": "Christopher Buehlman"}}, 82 + {"value": {"title": "Dungeon Crawler Carl", "authors": "Matt Dinniman"}}, 83 + {"value": {"title": "Cultish", "authors": "Amanda Montell"}}, 84 + ] 85 + 86 + def test_matches_title_first_order(self) -> None: 87 + m = bookhive.match_record("The Lesser Dead", "Christopher Buehlman", self.RECORDS) 88 + assert m is not None 89 + assert m["value"]["title"] == "The Lesser Dead" 90 + 91 + def test_matches_reversed_order(self) -> None: 92 + m = bookhive.match_record("Matt Dinniman", "Dungeon Crawler Carl", self.RECORDS) 93 + assert m is not None 94 + assert m["value"]["title"] == "Dungeon Crawler Carl" 95 + 96 + def test_no_match_bogus_input(self) -> None: 97 + assert bookhive.match_record("Zz", "Qq", self.RECORDS) is None
+255
tests/test_webdav_e2e.py
··· 1 + """End-to-end tests for the WebDAV adapter with ATProto mocked via respx.""" 2 + 3 + from __future__ import annotations 4 + 5 + import tempfile 6 + 7 + import httpx 8 + import pytest 9 + import respx 10 + from fastapi.testclient import TestClient 11 + 12 + from waggle.atproto import bookhive 13 + 14 + 15 + @pytest.fixture 16 + def app(monkeypatch): 17 + # Clear module-level record cache between tests. 18 + bookhive.invalidate_cache() 19 + 20 + tmp = tempfile.mkdtemp(prefix="waggle-test-") 21 + monkeypatch.setenv("PDS", "pds.example") 22 + monkeypatch.setenv("BSKY_HANDLE", "tester.example") 23 + monkeypatch.setenv("BSKY_APP_PASSWORD", "app-pw") 24 + monkeypatch.setenv("DAV_USER", "u") 25 + monkeypatch.setenv("DAV_PASSWORD", "p") 26 + monkeypatch.setenv("PASSTHROUGH_ROOT", tmp) 27 + 28 + # Reload the app factory to pick up env. 29 + from waggle import main as main_mod 30 + 31 + return main_mod.create_app() 32 + 33 + 34 + @pytest.fixture 35 + def client(app): 36 + return TestClient(app) 37 + 38 + 39 + AUTH = ("u", "p") 40 + DID = "did:plc:tester" 41 + 42 + 43 + def _mock_create_session(respx_mock) -> None: 44 + respx_mock.post("https://pds.example/xrpc/com.atproto.server.createSession").mock( 45 + return_value=httpx.Response( 46 + 200, 47 + json={ 48 + "accessJwt": "access.jwt", 49 + "refreshJwt": "refresh.jwt", 50 + "did": DID, 51 + "handle": "tester.example", 52 + }, 53 + ) 54 + ) 55 + 56 + 57 + def _records(books: list[dict]) -> dict: 58 + return {"records": books} 59 + 60 + 61 + def _book_record( 62 + rkey: str, 63 + title: str, 64 + authors: str, 65 + *, 66 + moon_file: str | None = None, 67 + position: str | None = None, 68 + ) -> dict: 69 + value: dict = { 70 + "$type": "buzz.bookhive.book", 71 + "title": title, 72 + "authors": authors, 73 + "status": "buzz.bookhive.defs#reading", 74 + } 75 + if moon_file and position: 76 + value["bookProgress"] = { 77 + "percent": 30, 78 + "currentChapter": 25, 79 + "updatedAt": "2026-04-13T19:00:18.000Z", 80 + "moonReader": { 81 + "position": position, 82 + "file": moon_file, 83 + "syncedAt": "2026-04-13T19:00:18.000Z", 84 + }, 85 + } 86 + return {"uri": f"at://{DID}/buzz.bookhive.book/{rkey}", "cid": "cid", "value": value} 87 + 88 + 89 + # --------------------------------------------------------------------------- 90 + # Auth 91 + # --------------------------------------------------------------------------- 92 + 93 + def test_healthz_requires_no_auth(client): 94 + assert client.get("/healthz").status_code == 200 95 + 96 + 97 + def test_options_unauthenticated_rejected(client): 98 + assert client.request("OPTIONS", "/").status_code == 401 99 + 100 + 101 + def test_options_authenticated_ok(client): 102 + r = client.request("OPTIONS", "/", auth=AUTH) 103 + assert r.status_code == 200 104 + assert "1" in r.headers["DAV"] 105 + assert "PROPFIND" in r.headers["Allow"] 106 + 107 + 108 + # --------------------------------------------------------------------------- 109 + # Moon+ cache PROPFIND / GET / PUT 110 + # --------------------------------------------------------------------------- 111 + 112 + @respx.mock 113 + def test_propfind_cache_synthesizes_from_records(client): 114 + _mock_create_session(respx.mock) 115 + respx.get("https://pds.example/xrpc/com.atproto.repo.listRecords").mock( 116 + return_value=httpx.Response( 117 + 200, 118 + json=_records([ 119 + _book_record( 120 + "rk1", "The Lesser Dead", "Christopher Buehlman", 121 + moon_file="The Lesser Dead - Christopher Buehlman.epub.po", 122 + position="1703297605115*24@0#0:30.0%", 123 + ), 124 + # No moonReader data — should NOT appear in listing. 125 + _book_record("rk2", "Other Book", "Nobody"), 126 + ]), 127 + ) 128 + ) 129 + 130 + r = client.request( 131 + "PROPFIND", "/Books/.Moon+/Cache/", 132 + auth=AUTH, headers={"Depth": "1"}, 133 + ) 134 + assert r.status_code == 207 135 + body = r.text 136 + assert "The Lesser Dead - Christopher Buehlman.epub.po" in body 137 + assert "Other Book" not in body 138 + 139 + 140 + @respx.mock 141 + def test_get_returns_stored_position_verbatim(client): 142 + _mock_create_session(respx.mock) 143 + raw = "1703297605115*24@0#0:30.0%" 144 + respx.get("https://pds.example/xrpc/com.atproto.repo.listRecords").mock( 145 + return_value=httpx.Response( 146 + 200, 147 + json=_records([ 148 + _book_record( 149 + "rk1", "The Lesser Dead", "Christopher Buehlman", 150 + moon_file="The Lesser Dead - Christopher Buehlman.epub.po", 151 + position=raw, 152 + ), 153 + ]), 154 + ) 155 + ) 156 + 157 + r = client.get( 158 + "/Books/.Moon+/Cache/The Lesser Dead - Christopher Buehlman.epub.po", 159 + auth=AUTH, 160 + ) 161 + assert r.status_code == 200 162 + assert r.text == raw 163 + assert r.headers["Content-Type"].startswith("text/plain") 164 + 165 + 166 + @respx.mock 167 + def test_put_updates_existing_record(client): 168 + _mock_create_session(respx.mock) 169 + bookhive.invalidate_cache() 170 + 171 + filename = "The Lesser Dead - Christopher Buehlman.epub.po" 172 + respx.get("https://pds.example/xrpc/com.atproto.repo.listRecords").mock( 173 + return_value=httpx.Response( 174 + 200, 175 + json=_records([ 176 + _book_record( 177 + "rk1", "The Lesser Dead", "Christopher Buehlman", 178 + moon_file=filename, 179 + position="1703297605115*24@0#0:30.0%", 180 + ), 181 + ]), 182 + ) 183 + ) 184 + put_route = respx.post("https://pds.example/xrpc/com.atproto.repo.putRecord").mock( 185 + return_value=httpx.Response(200, json={"uri": "...", "cid": "..."}), 186 + ) 187 + 188 + new_po = "1703297605115*30@0#0:42.5%" 189 + r = client.put( 190 + f"/Books/.Moon+/Cache/{filename}", 191 + auth=AUTH, 192 + content=new_po.encode(), 193 + ) 194 + assert r.status_code == 204 195 + assert put_route.called 196 + body = put_route.calls[0].request.read() 197 + # The new bookProgress should have hit percent=42 + moonReader.position=new_po. 198 + import json 199 + payload = json.loads(body) 200 + assert payload["collection"] == "buzz.bookhive.book" 201 + assert payload["record"]["bookProgress"]["percent"] == 42 202 + assert payload["record"]["bookProgress"]["moonReader"]["position"] == new_po 203 + 204 + 205 + @respx.mock 206 + def test_put_is_noop_when_unchanged(client): 207 + _mock_create_session(respx.mock) 208 + bookhive.invalidate_cache() 209 + 210 + filename = "The Lesser Dead - Christopher Buehlman.epub.po" 211 + raw = "1703297605115*24@0#0:30.0%" 212 + respx.get("https://pds.example/xrpc/com.atproto.repo.listRecords").mock( 213 + return_value=httpx.Response( 214 + 200, 215 + json=_records([ 216 + _book_record( 217 + "rk1", "The Lesser Dead", "Christopher Buehlman", 218 + moon_file=filename, position=raw, 219 + ), 220 + ]), 221 + ) 222 + ) 223 + put_route = respx.post("https://pds.example/xrpc/com.atproto.repo.putRecord").mock( 224 + return_value=httpx.Response(200, json={"uri": "...", "cid": "..."}), 225 + ) 226 + 227 + r = client.put(f"/Books/.Moon+/Cache/{filename}", auth=AUTH, content=raw.encode()) 228 + assert r.status_code == 204 229 + # Crucial: we must NOT hit putRecord when the position hasn't moved. 230 + # Moon+ Reader uploads on every pause event; most are duplicates. 231 + assert not put_route.called 232 + 233 + 234 + # --------------------------------------------------------------------------- 235 + # Passthrough 236 + # --------------------------------------------------------------------------- 237 + 238 + def test_passthrough_put_then_get(client): 239 + r = client.put( 240 + "/Books/.Moon+/Settings/theme.json", auth=AUTH, content=b'{"dark": true}' 241 + ) 242 + assert r.status_code in (201, 204) 243 + r = client.get("/Books/.Moon+/Settings/theme.json", auth=AUTH) 244 + assert r.status_code == 200 245 + assert r.content == b'{"dark": true}' 246 + 247 + 248 + def test_passthrough_propfind_lists_skeleton(client): 249 + r = client.request( 250 + "PROPFIND", "/Books/.Moon+/", auth=AUTH, headers={"Depth": "1"} 251 + ) 252 + assert r.status_code == 207 253 + # The virtual Cache/ dir should be visible alongside real Settings/. 254 + assert "Cache" in r.text 255 + assert "Settings" in r.text
+719
uv.lock
··· 1 + version = 1 2 + revision = 3 3 + requires-python = ">=3.11" 4 + 5 + [[package]] 6 + name = "annotated-doc" 7 + version = "0.0.4" 8 + source = { registry = "https://pypi.org/simple" } 9 + 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" } 10 + wheels = [ 11 + { 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" }, 12 + ] 13 + 14 + [[package]] 15 + name = "annotated-types" 16 + version = "0.7.0" 17 + source = { registry = "https://pypi.org/simple" } 18 + 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" } 19 + wheels = [ 20 + { 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" }, 21 + ] 22 + 23 + [[package]] 24 + name = "anyio" 25 + version = "4.13.0" 26 + source = { registry = "https://pypi.org/simple" } 27 + dependencies = [ 28 + { name = "idna" }, 29 + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 30 + ] 31 + 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" } 32 + wheels = [ 33 + { 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" }, 34 + ] 35 + 36 + [[package]] 37 + name = "certifi" 38 + version = "2026.2.25" 39 + source = { registry = "https://pypi.org/simple" } 40 + sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } 41 + wheels = [ 42 + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, 43 + ] 44 + 45 + [[package]] 46 + name = "click" 47 + version = "8.3.2" 48 + source = { registry = "https://pypi.org/simple" } 49 + dependencies = [ 50 + { name = "colorama", marker = "sys_platform == 'win32'" }, 51 + ] 52 + sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } 53 + wheels = [ 54 + { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, 55 + ] 56 + 57 + [[package]] 58 + name = "colorama" 59 + version = "0.4.6" 60 + source = { registry = "https://pypi.org/simple" } 61 + 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" } 62 + wheels = [ 63 + { 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" }, 64 + ] 65 + 66 + [[package]] 67 + name = "fastapi" 68 + version = "0.135.3" 69 + source = { registry = "https://pypi.org/simple" } 70 + dependencies = [ 71 + { name = "annotated-doc" }, 72 + { name = "pydantic" }, 73 + { name = "starlette" }, 74 + { name = "typing-extensions" }, 75 + { name = "typing-inspection" }, 76 + ] 77 + sdist = { url = "https://files.pythonhosted.org/packages/f7/e6/7adb4c5fa231e82c35b8f5741a9f2d055f520c29af5546fd70d3e8e1cd2e/fastapi-0.135.3.tar.gz", hash = "sha256:bd6d7caf1a2bdd8d676843cdcd2287729572a1ef524fc4d65c17ae002a1be654", size = 396524, upload-time = "2026-04-01T16:23:58.188Z" } 78 + wheels = [ 79 + { url = "https://files.pythonhosted.org/packages/84/a4/5caa2de7f917a04ada20018eccf60d6cc6145b0199d55ca3711b0fc08312/fastapi-0.135.3-py3-none-any.whl", hash = "sha256:9b0f590c813acd13d0ab43dd8494138eb58e484bfac405db1f3187cfc5810d98", size = 117734, upload-time = "2026-04-01T16:23:59.328Z" }, 80 + ] 81 + 82 + [[package]] 83 + name = "h11" 84 + version = "0.16.0" 85 + source = { registry = "https://pypi.org/simple" } 86 + 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" } 87 + wheels = [ 88 + { 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" }, 89 + ] 90 + 91 + [[package]] 92 + name = "httpcore" 93 + version = "1.0.9" 94 + source = { registry = "https://pypi.org/simple" } 95 + dependencies = [ 96 + { name = "certifi" }, 97 + { name = "h11" }, 98 + ] 99 + sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } 100 + wheels = [ 101 + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, 102 + ] 103 + 104 + [[package]] 105 + name = "httptools" 106 + version = "0.7.1" 107 + source = { registry = "https://pypi.org/simple" } 108 + 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" } 109 + wheels = [ 110 + { 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" }, 111 + { 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" }, 112 + { 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" }, 113 + { 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" }, 114 + { 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" }, 115 + { 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" }, 116 + { 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" }, 117 + { 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" }, 118 + { 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" }, 119 + { 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" }, 120 + { 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" }, 121 + { 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" }, 122 + { 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" }, 123 + { 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" }, 124 + { 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" }, 125 + { 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" }, 126 + { 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" }, 127 + { 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" }, 128 + { 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" }, 129 + { 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" }, 130 + { 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" }, 131 + { 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" }, 132 + { 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" }, 133 + { 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" }, 134 + { 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" }, 135 + { 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" }, 136 + { 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" }, 137 + { 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" }, 138 + ] 139 + 140 + [[package]] 141 + name = "httpx" 142 + version = "0.28.1" 143 + source = { registry = "https://pypi.org/simple" } 144 + dependencies = [ 145 + { name = "anyio" }, 146 + { name = "certifi" }, 147 + { name = "httpcore" }, 148 + { name = "idna" }, 149 + ] 150 + sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } 151 + wheels = [ 152 + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, 153 + ] 154 + 155 + [[package]] 156 + name = "idna" 157 + version = "3.11" 158 + source = { registry = "https://pypi.org/simple" } 159 + sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } 160 + wheels = [ 161 + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, 162 + ] 163 + 164 + [[package]] 165 + name = "iniconfig" 166 + version = "2.3.0" 167 + source = { registry = "https://pypi.org/simple" } 168 + sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } 169 + wheels = [ 170 + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, 171 + ] 172 + 173 + [[package]] 174 + name = "packaging" 175 + version = "26.0" 176 + source = { registry = "https://pypi.org/simple" } 177 + sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } 178 + wheels = [ 179 + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, 180 + ] 181 + 182 + [[package]] 183 + name = "pluggy" 184 + version = "1.6.0" 185 + source = { registry = "https://pypi.org/simple" } 186 + sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } 187 + wheels = [ 188 + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, 189 + ] 190 + 191 + [[package]] 192 + name = "pydantic" 193 + version = "2.13.0" 194 + source = { registry = "https://pypi.org/simple" } 195 + dependencies = [ 196 + { name = "annotated-types" }, 197 + { name = "pydantic-core" }, 198 + { name = "typing-extensions" }, 199 + { name = "typing-inspection" }, 200 + ] 201 + sdist = { url = "https://files.pythonhosted.org/packages/84/6b/69fd5c7194b21ebde0f8637e2a4ddc766ada29d472bfa6a5ca533d79549a/pydantic-2.13.0.tar.gz", hash = "sha256:b89b575b6e670ebf6e7448c01b41b244f471edd276cd0b6fe02e7e7aca320070", size = 843468, upload-time = "2026-04-13T10:51:35.571Z" } 202 + wheels = [ 203 + { url = "https://files.pythonhosted.org/packages/01/d7/c3a52c61f5b7be648e919005820fbac33028c6149994cd64453f49951c17/pydantic-2.13.0-py3-none-any.whl", hash = "sha256:ab0078b90da5f3e2fd2e71e3d9b457ddcb35d0350854fbda93b451e28d56baaf", size = 471872, upload-time = "2026-04-13T10:51:33.343Z" }, 204 + ] 205 + 206 + [[package]] 207 + name = "pydantic-core" 208 + version = "2.46.0" 209 + source = { registry = "https://pypi.org/simple" } 210 + dependencies = [ 211 + { name = "typing-extensions" }, 212 + ] 213 + sdist = { url = "https://files.pythonhosted.org/packages/6f/0a/9414cddf82eda3976b14048cc0fa8f5b5d1aecb0b22e1dcd2dbfe0e139b1/pydantic_core-2.46.0.tar.gz", hash = "sha256:82d2498c96be47b47e903e1378d1d0f770097ec56ea953322f39936a7cf34977", size = 471441, upload-time = "2026-04-13T09:06:33.813Z" } 214 + wheels = [ 215 + { url = "https://files.pythonhosted.org/packages/ce/43/9bc38d43a6a48794209e4eb6d61e9c68395f69b7949f66842854b0cd1344/pydantic_core-2.46.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0027da787ae711f7fbd5a76cb0bb8df526acba6c10c1e44581de1b838db10b7b", size = 2121004, upload-time = "2026-04-13T09:05:17.531Z" }, 216 + { url = "https://files.pythonhosted.org/packages/8c/1d/f43342b7107939b305b5e4efeef7d54e267a5ef51515570a5c1d77726efb/pydantic_core-2.46.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:63e288fc18d7eaeef5f16c73e65c4fd0ad95b25e7e21d8a5da144977b35eb997", size = 1947505, upload-time = "2026-04-13T09:04:48.975Z" }, 217 + { url = "https://files.pythonhosted.org/packages/4a/cd/ccf48cbbcaf0d99ba65969459ebfbf7037600b2cfdcca3062084dd83a008/pydantic_core-2.46.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:080a3bdc6807089a1fe1fbc076519cea287f1a964725731d80b49d8ecffaa217", size = 1973301, upload-time = "2026-04-13T09:05:42.149Z" }, 218 + { url = "https://files.pythonhosted.org/packages/c2/ff/a7bb1e7a762fb1f40ad5ef4e6a92c012864a017b7b1fdfb71cf91faa8b73/pydantic_core-2.46.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c065f1c3e54c3e79d909927a8cb48ccbc17b68733552161eba3e0628c38e5d19", size = 2042208, upload-time = "2026-04-13T09:05:32.591Z" }, 219 + { url = "https://files.pythonhosted.org/packages/ea/64/d3f11c6f6ace71526f3b03646df95eaab3f21edd13e00daae3f20f4e5a09/pydantic_core-2.46.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7e2db58ab46cfe602d4255381cce515585998c3b6699d5b1f909f519bc44a5aa", size = 2229046, upload-time = "2026-04-13T09:04:18.59Z" }, 220 + { url = "https://files.pythonhosted.org/packages/d0/64/93db9a63cce71630c58b376d63de498aa93cb341c72cd5f189b5c08f5c28/pydantic_core-2.46.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c660974890ec1e4c65cff93f5670a5f451039f65463e9f9c03ad49746b49fc78", size = 2292138, upload-time = "2026-04-13T09:04:13.816Z" }, 221 + { url = "https://files.pythonhosted.org/packages/e9/96/936fccce22f1f2ae8b2b694de651c2c929847be5f701c927a0bb3b1eb679/pydantic_core-2.46.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3be91482a8db77377c902cca87697388a4fb68addeb3e943ac74f425201a099", size = 2093333, upload-time = "2026-04-13T09:05:15.729Z" }, 222 + { url = "https://files.pythonhosted.org/packages/75/76/c325e7fda69d589e26e772272044fe704c7e525c47d0d32a74f8345ac657/pydantic_core-2.46.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:1c72de82115233112d70d07f26a48cf6996eb86f7e143423ec1a182148455a9d", size = 2138802, upload-time = "2026-04-13T09:03:51.142Z" }, 223 + { url = "https://files.pythonhosted.org/packages/c0/6f/ccaa2ff7d53a017b66841e2d38edd1f38d19ae1a2d0c5efee17f2d432229/pydantic_core-2.46.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7904e58768cd79304b992868d7710bfc85dc6c7ed6163f0f68dbc1dcd72dc231", size = 2181358, upload-time = "2026-04-13T09:04:30.737Z" }, 224 + { url = "https://files.pythonhosted.org/packages/6c/71/0c4b6303e92d63edcb81f5301695cdf70bb351775b4733eea65acdac8384/pydantic_core-2.46.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1af8d88718005f57bb4768f92f4ff16bf31a747d39dfc919b22211b84e72c053", size = 2183985, upload-time = "2026-04-13T09:04:06.792Z" }, 225 + { url = "https://files.pythonhosted.org/packages/71/eb/f6bf255de38a4393aaa10bff224e882b630576bc26ebfb401e42bb965092/pydantic_core-2.46.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:a5b891301b02770a5852253f4b97f8bd192e5710067bc129e20d43db5403ede2", size = 2328559, upload-time = "2026-04-13T09:06:14.143Z" }, 226 + { url = "https://files.pythonhosted.org/packages/f2/71/93895a1545f50823a24b21d7761c2bd1b1afea7a6ddc019787caec237361/pydantic_core-2.46.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:48b671fe59031fd9754c7384ac05b3ed47a0cccb7d4db0ec56121f0e6a541b90", size = 2367466, upload-time = "2026-04-13T09:05:59.613Z" }, 227 + { url = "https://files.pythonhosted.org/packages/78/39/62331b3e71f41fb13d486621e2aec49900ba56567fb3a0ae5999fded0005/pydantic_core-2.46.0-cp311-cp311-win32.whl", hash = "sha256:0a52b7262b6cc67033823e9549a41bb77580ac299dc964baae4e9c182b2e335c", size = 1981367, upload-time = "2026-04-13T09:07:37.563Z" }, 228 + { url = "https://files.pythonhosted.org/packages/9f/51/caac70958420e2d6115962f550676df59647c11f96a44c2fcb61662fcd16/pydantic_core-2.46.0-cp311-cp311-win_amd64.whl", hash = "sha256:4103fea1beeef6b3a9fed8515f27d4fa30c929a1973655adf8f454dc49ee0662", size = 2065942, upload-time = "2026-04-13T09:06:37.873Z" }, 229 + { url = "https://files.pythonhosted.org/packages/b2/cf/576b2a4eb5500a1a5da485613b1ea8bc0d7279b27e0426801574b284ae65/pydantic_core-2.46.0-cp311-cp311-win_arm64.whl", hash = "sha256:3137cd88938adb8e567c5e938e486adc7e518ffc96b4ae1ec268e6a4275704d7", size = 2052532, upload-time = "2026-04-13T09:06:03.697Z" }, 230 + { url = "https://files.pythonhosted.org/packages/a7/d2/206c72ad47071559142a35f71efc29eb16448a4a5ae9487230ab8e4e292b/pydantic_core-2.46.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:66ccedb02c934622612448489824955838a221b3a35875458970521ef17b2f9c", size = 2117060, upload-time = "2026-04-13T09:04:47.443Z" }, 231 + { url = "https://files.pythonhosted.org/packages/17/2c/7a53b33f91c8b77e696b1a6aa3bed609bf9374bdc0f8dcda681bc7d922b8/pydantic_core-2.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a44f27f4d2788ef9876ec47a43739b118c5904d74f418f53398f6ced3bbcacf2", size = 1951802, upload-time = "2026-04-13T09:05:34.591Z" }, 232 + { url = "https://files.pythonhosted.org/packages/fc/20/90e548c1f6d38800ef11c915881525770ce270d8e5e887563ff046a08674/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26a1032bcce6ca4b4670eb3f7d8195bd0a8b8f255f1307823e217ca3cfa7c27", size = 1976621, upload-time = "2026-04-13T09:04:03.909Z" }, 233 + { url = "https://files.pythonhosted.org/packages/20/3c/9c5810ca70b60c623488cdd80f7e9ee1a0812df81e97098b64788719860f/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b8d1412f725060527e56675904b17a2d421dddcf861eecf7c75b9dda47921a4", size = 2056721, upload-time = "2026-04-13T09:04:40.992Z" }, 234 + { url = "https://files.pythonhosted.org/packages/1a/a3/d6e5f4cdec84278431c75540f90838c9d0a4dfe9402a8f3902073660ff28/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc3d1569edd859cabaa476cabce9eecd05049a7966af7b4a33b541bfd4ca1104", size = 2239634, upload-time = "2026-04-13T09:03:52.478Z" }, 235 + { url = "https://files.pythonhosted.org/packages/46/42/ef58aacf330d8de6e309d62469aa1f80e945eaf665929b4037ac1bfcebc1/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:38108976f2d8afaa8f5067fd1390a8c9f5cc580175407cda636e76bc76e88054", size = 2315739, upload-time = "2026-04-13T09:05:04.971Z" }, 236 + { url = "https://files.pythonhosted.org/packages/8b/86/c63b12fafa2d86a515bfd1840b39c23a49302f02b653161bf9c3a0566c50/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5a06d8ed01dad5575056b5187e5959b336793c6047920a3441ee5b03533836", size = 2098169, upload-time = "2026-04-13T09:07:27.151Z" }, 237 + { url = "https://files.pythonhosted.org/packages/76/19/b5b33a2f6be4755b21a20434293c4364be255f4c1a108f125d101d4cc4ee/pydantic_core-2.46.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:04017ace142da9ce27cafd423a480872571b5c7e80382aec22f7d715ca8eb870", size = 2170830, upload-time = "2026-04-13T09:04:39.448Z" }, 238 + { url = "https://files.pythonhosted.org/packages/99/ae/7559f99a29b7d440012ddb4da897359304988a881efaca912fd2f655652e/pydantic_core-2.46.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2629ad992ed1b1c012e6067f5ffafd3336fcb9b54569449fabb85621f1444ed3", size = 2203901, upload-time = "2026-04-13T09:04:01.048Z" }, 239 + { url = "https://files.pythonhosted.org/packages/dd/0e/b0ef945a39aeb4ac58da316813e1106b7fbdfbf20ac141c1c27904355ac5/pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3068b1e7bd986aebc88f6859f8353e72072538dcf92a7fb9cf511a0f61c5e729", size = 2191789, upload-time = "2026-04-13T09:06:39.915Z" }, 240 + { url = "https://files.pythonhosted.org/packages/90/f4/830484e07188c1236b013995818888ab93bab8fd88aa9689b1d8fd22220d/pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:1e366916ff69ff700aa9326601634e688581bc24c5b6b4f8738d809ec7d72611", size = 2344423, upload-time = "2026-04-13T09:05:12.252Z" }, 241 + { url = "https://files.pythonhosted.org/packages/fd/ba/e455c18cbdc333177af754e740be4fe9d1de173d65bbe534daf88da02ac0/pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:485a23e8f4618a1b8e23ac744180acde283fffe617f96923d25507d5cade62ec", size = 2384037, upload-time = "2026-04-13T09:06:24.503Z" }, 242 + { url = "https://files.pythonhosted.org/packages/78/1f/b35d20d73144a41e78de0ae398e60fdd8bed91667daa1a5a92ab958551ba/pydantic_core-2.46.0-cp312-cp312-win32.whl", hash = "sha256:520940e1b702fe3b33525d0351777f25e9924f1818ca7956447dabacf2d339fd", size = 1967068, upload-time = "2026-04-13T09:05:23.374Z" }, 243 + { url = "https://files.pythonhosted.org/packages/d1/84/4b6252e9606e8295647b848233cc4137ee0a04ebba8f0f9fb2977655b38c/pydantic_core-2.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:90d2048e0339fa365e5a66aefe760ddd3b3d0a45501e088bc5bc7f4ed9ff9571", size = 2071008, upload-time = "2026-04-13T09:05:21.392Z" }, 244 + { url = "https://files.pythonhosted.org/packages/39/95/d08eb508d4d5560ccbd226ee5971e5ef9b749aba9b413c0c4ed6e406d4f6/pydantic_core-2.46.0-cp312-cp312-win_arm64.whl", hash = "sha256:a70247649b7dffe36648e8f34be5ce8c5fa0a27ff07b071ea780c20a738c05ce", size = 2036634, upload-time = "2026-04-13T09:05:48.299Z" }, 245 + { url = "https://files.pythonhosted.org/packages/df/05/ab3b0742bad1d51822f1af0c4232208408902bdcfc47601f3b812e09e6c2/pydantic_core-2.46.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a05900c37264c070c683c650cbca8f83d7cbb549719e645fcd81a24592eac788", size = 2116814, upload-time = "2026-04-13T09:04:12.41Z" }, 246 + { url = "https://files.pythonhosted.org/packages/98/08/30b43d9569d69094a0899a199711c43aa58fce6ce80f6a8f7693673eb995/pydantic_core-2.46.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de8e482fd4f1e3f36c50c6aac46d044462615d8f12cfafc6bebeaa0909eea22", size = 1951867, upload-time = "2026-04-13T09:04:02.364Z" }, 247 + { url = "https://files.pythonhosted.org/packages/db/a0/bf9a1ba34537c2ed3872a48195291138fdec8fe26c4009776f00d63cf0c8/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c525ecf8a4cdf198327b65030a7d081867ad8e60acb01a7214fff95cf9832d47", size = 1977040, upload-time = "2026-04-13T09:06:16.088Z" }, 248 + { url = "https://files.pythonhosted.org/packages/71/70/0ba03c20e1e118219fc18c5417b008b7e880f0e3fb38560ec4465984d471/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f14581aeb12e61542ce73b9bfef2bca5439d65d9ab3efe1a4d8e346b61838f9b", size = 2055284, upload-time = "2026-04-13T09:05:25.125Z" }, 249 + { url = "https://files.pythonhosted.org/packages/58/cf/1e320acefbde7fb7158a9e5def55e0adf9a4634636098ce28dc6b978e0d3/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c108067f2f7e190d0dbd81247d789ec41f9ea50ccd9265a3a46710796ac60530", size = 2238896, upload-time = "2026-04-13T09:05:01.345Z" }, 250 + { url = "https://files.pythonhosted.org/packages/df/f5/ea8ba209756abe9eba891bb0ef3772b4c59a894eb9ad86cd5bd0dd4e3e52/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ac10967e9a7bb1b96697374513f9a1a90a59e2fb41566b5e00ee45392beac59", size = 2314353, upload-time = "2026-04-13T09:06:07.942Z" }, 251 + { url = "https://files.pythonhosted.org/packages/e8/f8/5885350203b72e96438eee7f94de0d8f0442f4627237ca8ef75de34db1cd/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7897078fe8a13b73623c0955dfb2b3d2c9acb7177aac25144758c9e5a5265aaa", size = 2098522, upload-time = "2026-04-13T09:04:23.239Z" }, 252 + { url = "https://files.pythonhosted.org/packages/bf/88/5930b0e828e371db5a556dd3189565417ddc3d8316bb001058168aadcf5f/pydantic_core-2.46.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:e69ce405510a419a082a78faed65bb4249cfb51232293cc675645c12f7379bf7", size = 2168757, upload-time = "2026-04-13T09:07:12.46Z" }, 253 + { url = "https://files.pythonhosted.org/packages/da/75/63d563d3035a0548e721c38b5b69fd5626fdd51da0f09ff4467503915b82/pydantic_core-2.46.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fd28d13eea0d8cf351dc1fe274b5070cc8e1cca2644381dee5f99de629e77cf3", size = 2202518, upload-time = "2026-04-13T09:05:44.418Z" }, 254 + { url = "https://files.pythonhosted.org/packages/a7/53/1958eacbfddc41aadf5ae86dd85041bf054b675f34a2fa76385935f96070/pydantic_core-2.46.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:ee1547a6b8243e73dd10f585555e5a263395e55ce6dea618a078570a1e889aef", size = 2190148, upload-time = "2026-04-13T09:06:56.151Z" }, 255 + { url = "https://files.pythonhosted.org/packages/c7/17/098cc6d3595e4623186f2bc6604a6195eb182e126702a90517236391e9ce/pydantic_core-2.46.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c3dc68dcf62db22a18ddfc3ad4960038f72b75908edc48ae014d7ac8b391d57a", size = 2342925, upload-time = "2026-04-13T09:04:17.286Z" }, 256 + { url = "https://files.pythonhosted.org/packages/71/a7/abdb924620b1ac535c690b36ad5b8871f376104090f8842c08625cecf1d3/pydantic_core-2.46.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:004a2081c881abfcc6854a4623da6a09090a0d7c1398a6ae7133ca1256cee70b", size = 2383167, upload-time = "2026-04-13T09:04:52.643Z" }, 257 + { url = "https://files.pythonhosted.org/packages/d7/c9/2ddd10f50e4b7350d2574629a0f53d8d4eb6573f9c19a6b43e6b1487a31d/pydantic_core-2.46.0-cp313-cp313-win32.whl", hash = "sha256:59d24ec8d5eaabad93097525a69d0f00f2667cb353eb6cda578b1cfff203ceef", size = 1965660, upload-time = "2026-04-13T09:06:05.877Z" }, 258 + { url = "https://files.pythonhosted.org/packages/b5/e7/1efc38ed6f2680c032bcefa0e3ebd496a8c77e92dfdb86b07d0f2fc632b1/pydantic_core-2.46.0-cp313-cp313-win_amd64.whl", hash = "sha256:71186dad5ac325c64d68fe0e654e15fd79802e7cc42bc6f0ff822d5ad8b1ab25", size = 2069563, upload-time = "2026-04-13T09:07:14.738Z" }, 259 + { url = "https://files.pythonhosted.org/packages/c3/1e/a325b4989e742bf7e72ed35fa124bc611fd76539c9f8cd2a9a7854473533/pydantic_core-2.46.0-cp313-cp313-win_arm64.whl", hash = "sha256:8e4503f3213f723842c9a3b53955c88a9cfbd0b288cbd1c1ae933aebeec4a1b4", size = 2034966, upload-time = "2026-04-13T09:04:21.629Z" }, 260 + { url = "https://files.pythonhosted.org/packages/36/3b/914891d384cdbf9a6f464eb13713baa22ea1e453d4da80fb7da522079370/pydantic_core-2.46.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:4fc801c290342350ffc82d77872054a934b2e24163727263362170c1db5416ca", size = 2113349, upload-time = "2026-04-13T09:04:59.407Z" }, 261 + { url = "https://files.pythonhosted.org/packages/35/95/3a0c6f65e231709fb3463e32943c69d10285cb50203a2130a4732053a06d/pydantic_core-2.46.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0a36f2cc88170cc177930afcc633a8c15907ea68b59ac16bd180c2999d714940", size = 1949170, upload-time = "2026-04-13T09:06:09.935Z" }, 262 + { url = "https://files.pythonhosted.org/packages/d1/63/d845c36a608469fe7bee226edeff0984c33dbfe7aecd755b0e7ab5a275c4/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a3912e0c568a1f99d4d6d3e41def40179d61424c0ca1c8c87c4877d7f6fd7fb", size = 1977914, upload-time = "2026-04-13T09:04:56.16Z" }, 263 + { url = "https://files.pythonhosted.org/packages/08/6f/f2e7a7f85931fb31671f5378d1c7fc70606e4b36d59b1b48e1bd1ef5d916/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3534c3415ed1a19ab23096b628916a827f7858ec8db49ad5d7d1e44dc13c0d7b", size = 2050538, upload-time = "2026-04-13T09:05:06.789Z" }, 264 + { url = "https://files.pythonhosted.org/packages/8c/97/f4aa7181dd9a16dd9059a99fc48fdab0c2aab68307283a5c04cf56de68c4/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21067396fc285609323a4db2f63a87570044abe0acddfcca8b135fc7948e3db7", size = 2236294, upload-time = "2026-04-13T09:07:03.2Z" }, 265 + { url = "https://files.pythonhosted.org/packages/24/c1/6a5042fc32765c87101b500f394702890af04239c318b6002cfd627b710d/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2afd85b7be186e2fe7cdbb09a3d964bcc2042f65bbcc64ad800b3c7915032655", size = 2312954, upload-time = "2026-04-13T09:06:11.919Z" }, 266 + { url = "https://files.pythonhosted.org/packages/cb/e4/566101a561492ce8454f0844ca29c3b675a6b3a7b3ff577db85ed05c8c50/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67e2c2e171b78db8154da602de72ffdc473c6ee51de8a9d80c0f1cd4051abfc7", size = 2102533, upload-time = "2026-04-13T09:06:58.664Z" }, 267 + { url = "https://files.pythonhosted.org/packages/3e/ac/adc11ee1646a5c4dd9abb09a00e7909e6dc25beddc0b1310ca734bb9b48e/pydantic_core-2.46.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c16ae1f3170267b1a37e16dba5c297bdf60c8b5657b147909ca8774ce7366644", size = 2169447, upload-time = "2026-04-13T09:04:11.143Z" }, 268 + { url = "https://files.pythonhosted.org/packages/26/73/408e686b45b82d28ac19e8229e07282254dbee6a5d24c5c7cf3cf3716613/pydantic_core-2.46.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:133b69e1c1ba34d3702eed73f19f7f966928f9aa16663b55c2ebce0893cca42e", size = 2200672, upload-time = "2026-04-13T09:03:54.056Z" }, 269 + { url = "https://files.pythonhosted.org/packages/0a/3b/807d5b035ec891b57b9079ce881f48263936c37bd0d154a056e7fd152afb/pydantic_core-2.46.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:15ed8e5bde505133d96b41702f31f06829c46b05488211a5b1c7877e11de5eb5", size = 2188293, upload-time = "2026-04-13T09:07:07.614Z" }, 270 + { url = "https://files.pythonhosted.org/packages/f1/ed/719b307516285099d1196c52769fdbe676fd677da007b9c349ae70b7226d/pydantic_core-2.46.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:8cfc29a1c66a7f0fcb36262e92f353dd0b9c4061d558fceb022e698a801cb8ae", size = 2335023, upload-time = "2026-04-13T09:04:05.176Z" }, 271 + { url = "https://files.pythonhosted.org/packages/8d/90/8718e4ae98c4e8a7325afdc079be82be1e131d7a47cb6c098844a9531ffe/pydantic_core-2.46.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e1155708540f13845bf68d5ac511a55c76cfe2e057ed12b4bf3adac1581fc5c2", size = 2377155, upload-time = "2026-04-13T09:06:18.081Z" }, 272 + { url = "https://files.pythonhosted.org/packages/dd/dc/7172789283b963f81da2fc92b186e22de55687019079f71c4d570822502b/pydantic_core-2.46.0-cp314-cp314-win32.whl", hash = "sha256:de5635a48df6b2eef161d10ea1bc2626153197333662ba4cd700ee7ec1aba7f5", size = 1963078, upload-time = "2026-04-13T09:05:30.615Z" }, 273 + { url = "https://files.pythonhosted.org/packages/e0/69/03a7ea4b6264def3a44eabf577528bcec2f49468c5698b2044dea54dc07e/pydantic_core-2.46.0-cp314-cp314-win_amd64.whl", hash = "sha256:f07a5af60c5e7cf53dd1ff734228bd72d0dc9938e64a75b5bb308ca350d9681e", size = 2068439, upload-time = "2026-04-13T09:04:57.729Z" }, 274 + { url = "https://files.pythonhosted.org/packages/f5/eb/1c3afcfdee2ab6634b802ab0a0f1966df4c8b630028ec56a1cb0a710dc58/pydantic_core-2.46.0-cp314-cp314-win_arm64.whl", hash = "sha256:e7a77eca3c7d5108ff509db20aae6f80d47c7ed7516d8b96c387aacc42f3ce0f", size = 2026470, upload-time = "2026-04-13T09:05:08.654Z" }, 275 + { url = "https://files.pythonhosted.org/packages/5c/30/1177dde61b200785c4739665e3aa03a9d4b2c25d2d0408b07d585e633965/pydantic_core-2.46.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5e7cdd4398bee1aaeafe049ac366b0f887451d9ae418fd8785219c13fea2f928", size = 2107447, upload-time = "2026-04-13T09:05:46.314Z" }, 276 + { url = "https://files.pythonhosted.org/packages/b1/60/4e0f61f99bdabbbc309d364a2791e1ba31e778a4935bc43391a7bdec0744/pydantic_core-2.46.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5c2c92d82808e27cef3f7ab3ed63d657d0c755e0dbe5b8a58342e37bdf09bd2e", size = 1926927, upload-time = "2026-04-13T09:06:20.371Z" }, 277 + { url = "https://files.pythonhosted.org/packages/1d/d0/67f89a8269152c1d6eaa81f04e75a507372ebd8ca7382855a065222caa80/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bab80af91cd7014b45d1089303b5f844a9d91d7da60eabf3d5f9694b32a6655", size = 1966613, upload-time = "2026-04-13T09:07:05.389Z" }, 278 + { url = "https://files.pythonhosted.org/packages/cd/07/8dfdc3edc78f29a80fb31f366c50203ec904cff6a4c923599bf50ac0d0ff/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e49ffdb714bc990f00b39d1ad1d683033875b5af15582f60c1f34ad3eeccfaa", size = 2032902, upload-time = "2026-04-13T09:06:42.47Z" }, 279 + { url = "https://files.pythonhosted.org/packages/b0/2a/111c5e8fe24f99c46bcad7d3a82a8f6dbc738066e2c72c04c71f827d8c78/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca877240e8dbdeef3a66f751dc41e5a74893767d510c22a22fc5c0199844f0ce", size = 2244456, upload-time = "2026-04-13T09:05:36.484Z" }, 280 + { url = "https://files.pythonhosted.org/packages/6b/7c/cfc5d11c15a63ece26e148572c77cfbb2c7f08d315a7b63ef0fe0711d753/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87e6843f89ecd2f596d7294e33196c61343186255b9880c4f1b725fde8b0e20d", size = 2294535, upload-time = "2026-04-13T09:06:01.689Z" }, 281 + { url = "https://files.pythonhosted.org/packages/c4/2c/f0d744e3dab7bd026a3f4670a97a295157cff923a2666d30a15a70a7e3d0/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e20bc5add1dd9bc3b9a3600d40632e679376569098345500799a6ad7c5d46c72", size = 2104621, upload-time = "2026-04-13T09:04:34.388Z" }, 282 + { url = "https://files.pythonhosted.org/packages/a7/64/e7cc4698dc024264d214b51d5a47a2404221b12060dd537d76f831b2120a/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:ee6ff79a5f0289d64a9d6696a3ce1f98f925b803dd538335a118231e26d6d827", size = 2130718, upload-time = "2026-04-13T09:04:26.23Z" }, 283 + { url = "https://files.pythonhosted.org/packages/0b/a8/224e655fec21f7d4441438ad2ecaccb33b5a3876ce7bb2098c74a49efc14/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:52d35cfb58c26323101c7065508d7bb69bb56338cda9ea47a7b32be581af055d", size = 2180738, upload-time = "2026-04-13T09:05:50.253Z" }, 284 + { url = "https://files.pythonhosted.org/packages/32/7b/b3025618ed4c4e4cbaa9882731c19625db6669896b621760ea95bc1125ef/pydantic_core-2.46.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d14cc5a6f260fa78e124061eebc5769af6534fc837e9a62a47f09a2c341fa4ea", size = 2171222, upload-time = "2026-04-13T09:07:29.929Z" }, 285 + { url = "https://files.pythonhosted.org/packages/7b/e3/68170aa1d891920af09c1f2f34df61dc5ff3a746400027155523e3400e89/pydantic_core-2.46.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:4f7ff859d663b6635f6307a10803d07f0d09487e16c3d36b1744af51dbf948b2", size = 2320040, upload-time = "2026-04-13T09:06:35.732Z" }, 286 + { url = "https://files.pythonhosted.org/packages/67/1b/5e65807001b84972476300c1f49aea2b4971b7e9fffb5c2654877dadd274/pydantic_core-2.46.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:8ef749be6ed0d69dba31902aaa8255a9bb269ae50c93888c4df242d8bb7acd9e", size = 2377062, upload-time = "2026-04-13T09:07:39.945Z" }, 287 + { url = "https://files.pythonhosted.org/packages/75/03/48caa9dd5f28f7662bd52bff454d9a451f6b7e5e4af95e289e5e170749c9/pydantic_core-2.46.0-cp314-cp314t-win32.whl", hash = "sha256:d93ca72870133f86360e4bb0c78cd4e6ba2a0f9f3738a6486909ffc031463b32", size = 1951028, upload-time = "2026-04-13T09:04:20.224Z" }, 288 + { url = "https://files.pythonhosted.org/packages/87/ed/e97ff55fe28c0e6e3cba641d622b15e071370b70e5f07c496b07b65db7c9/pydantic_core-2.46.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6ebb2668afd657e2127cb40f2ceb627dd78e74e9dfde14d9bf6cdd532a29ff59", size = 2048519, upload-time = "2026-04-13T09:05:10.464Z" }, 289 + { url = "https://files.pythonhosted.org/packages/b6/51/e0db8267a287994546925f252e329eeae4121b1e77e76353418da5a3adf0/pydantic_core-2.46.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4864f5bbb7993845baf9209bae1669a8a76769296a018cb569ebda9dcb4241f5", size = 2026791, upload-time = "2026-04-13T09:04:37.724Z" }, 290 + { url = "https://files.pythonhosted.org/packages/2d/f1/6731c2d6caf03efe822101edb4783eb3f212f34b7b005a34f039f67e76e1/pydantic_core-2.46.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:ce2e38e27de73ff6a0312a9e3304c398577c418d90bbde97f0ba1ee3ab7ac39f", size = 2121259, upload-time = "2026-04-13T09:07:34.845Z" }, 291 + { url = "https://files.pythonhosted.org/packages/72/fd/ac34d4c92e739e37a040be9e7ea84d116afec5f983a7db856c27135fba77/pydantic_core-2.46.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:f0d34ba062396de0be7421e6e69c9a6821bf6dc73a0ab9959a48a5a6a1e24754", size = 1945798, upload-time = "2026-04-13T09:04:24.729Z" }, 292 + { url = "https://files.pythonhosted.org/packages/b6/a4/f413a522c4047c46b109be6805a3095d35e5a4882fd5b4fdc0909693dfc0/pydantic_core-2.46.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4c0a12147b4026dd68789fb9f22f1a8769e457f9562783c181880848bbd6412", size = 1986062, upload-time = "2026-04-13T09:05:57.177Z" }, 293 + { url = "https://files.pythonhosted.org/packages/91/2e/9760025ea8b0f49903c0ceebdfc2d8ef839da872426f2b03cae9de036a7c/pydantic_core-2.46.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a99896d9db56df901ab4a63cd6a36348a569cff8e05f049db35f4016a817a3d9", size = 2145344, upload-time = "2026-04-13T09:03:56.924Z" }, 294 + { url = "https://files.pythonhosted.org/packages/74/0c/106ed5cc50393d90523f09adcc50d05e42e748eb107dc06aea971137f02d/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:bc0e2fefe384152d7da85b5c2fe8ce2bf24752f68a58e3f3ea42e28a29dfdeb2", size = 2104968, upload-time = "2026-04-13T09:06:26.967Z" }, 295 + { url = "https://files.pythonhosted.org/packages/f5/71/b494cef3165e3413ee9bbbb5a9eedc9af0ea7b88d8638beef6c2061b110e/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:a2ab0e785548be1b4362a62c4004f9217598b7ee465f1f420fc2123e2a5b5b02", size = 1940442, upload-time = "2026-04-13T09:06:29.332Z" }, 296 + { url = "https://files.pythonhosted.org/packages/7e/3e/a4d578c8216c443e26a1124f8c1e07c0654264ce5651143d3883d85ff140/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16d45aecb18b8cba1c68eeb17c2bb2d38627ceed04c5b30b882fc9134e01f187", size = 1999672, upload-time = "2026-04-13T09:04:42.798Z" }, 297 + { url = "https://files.pythonhosted.org/packages/cd/c1/9114560468685525a21770138382fd0cb849aaf351ff2c7b97f760d121e0/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5078f6c377b002428e984259ac327ef8902aacae6c14b7de740dd4869a491501", size = 2154533, upload-time = "2026-04-13T09:04:50.868Z" }, 298 + { url = "https://files.pythonhosted.org/packages/09/ed/fbd8127e4a19c4fdbb2f4983cf72c7b3534086df640c813c5c0ec4218177/pydantic_core-2.46.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:be3e04979ba4d68183f247202c7f4f483f35df57690b3f875c06340a1579b47c", size = 2119951, upload-time = "2026-04-13T09:04:35.923Z" }, 299 + { url = "https://files.pythonhosted.org/packages/ec/77/df8711ebb45910412f90d75198430fa1120f5618336b71fa00303601c5a4/pydantic_core-2.46.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b1eae8d7d9b8c2a90b34d3d9014804dca534f7f40180197062634499412ea14e", size = 1953812, upload-time = "2026-04-13T09:05:40.293Z" }, 300 + { url = "https://files.pythonhosted.org/packages/12/fe/14b35df69112bd812d6818a395eeab22eeaa2befc6f85bc54ed648430186/pydantic_core-2.46.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a95a2773680dd4b6b999d4eccdd1b577fd71c31739fb4849f6ada47eabb9c56", size = 2139585, upload-time = "2026-04-13T09:06:46.94Z" }, 301 + { url = "https://files.pythonhosted.org/packages/1f/f0/4fea4c14ebbdeb87e5f6edd2620735fcbd384865f06707fe229c021ce041/pydantic_core-2.46.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25988c3159bb097e06abfdf7b21b1fcaf90f187c74ca6c7bb842c1f72ce74fa8", size = 2179154, upload-time = "2026-04-13T09:04:15.639Z" }, 302 + { url = "https://files.pythonhosted.org/packages/5c/36/6329aa79ba32b73560e6e453164fb29702b115fd3b2b650e796e1dc27862/pydantic_core-2.46.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:747d89bd691854c719a3381ba46b6124ef916ae85364c79e11db9c84995d8d03", size = 2182917, upload-time = "2026-04-13T09:07:24.483Z" }, 303 + { url = "https://files.pythonhosted.org/packages/92/61/edbf7aea71052d410347846a2ea43394f74651bf6822b8fad8703ca00575/pydantic_core-2.46.0-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:909a7327b83ca93b372f7d48df0ebc7a975a5191eb0b6e024f503f4902c24124", size = 2327716, upload-time = "2026-04-13T09:06:31.681Z" }, 304 + { url = "https://files.pythonhosted.org/packages/a4/11/aa5089b941e85294b1d5d526840b18f0d4464f842d43d8999ce50ef881c1/pydantic_core-2.46.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:2f7e6a3752378a69fadf3f5ee8bc5fa082f623703eec0f4e854b12c548322de0", size = 2365925, upload-time = "2026-04-13T09:05:38.338Z" }, 305 + { url = "https://files.pythonhosted.org/packages/0c/75/e187b0ea247f71f2009d156df88b7d8449c52a38810c9a1bd55dd4871206/pydantic_core-2.46.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ef47ee0a3ac4c2bb25a083b3acafb171f65be4a0ac1e84edef79dd0016e25eaa", size = 2193856, upload-time = "2026-04-13T09:05:03.114Z" }, 306 + ] 307 + 308 + [[package]] 309 + name = "pygments" 310 + version = "2.20.0" 311 + source = { registry = "https://pypi.org/simple" } 312 + sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } 313 + wheels = [ 314 + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, 315 + ] 316 + 317 + [[package]] 318 + name = "pytest" 319 + version = "9.0.3" 320 + source = { registry = "https://pypi.org/simple" } 321 + dependencies = [ 322 + { name = "colorama", marker = "sys_platform == 'win32'" }, 323 + { name = "iniconfig" }, 324 + { name = "packaging" }, 325 + { name = "pluggy" }, 326 + { name = "pygments" }, 327 + ] 328 + sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } 329 + wheels = [ 330 + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, 331 + ] 332 + 333 + [[package]] 334 + name = "pytest-asyncio" 335 + version = "1.3.0" 336 + source = { registry = "https://pypi.org/simple" } 337 + dependencies = [ 338 + { name = "pytest" }, 339 + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 340 + ] 341 + sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } 342 + wheels = [ 343 + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, 344 + ] 345 + 346 + [[package]] 347 + name = "python-dotenv" 348 + version = "1.2.2" 349 + source = { registry = "https://pypi.org/simple" } 350 + 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" } 351 + wheels = [ 352 + { 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" }, 353 + ] 354 + 355 + [[package]] 356 + name = "pyyaml" 357 + version = "6.0.3" 358 + source = { registry = "https://pypi.org/simple" } 359 + 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" } 360 + wheels = [ 361 + { 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" }, 362 + { 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" }, 363 + { 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" }, 364 + { 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" }, 365 + { 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" }, 366 + { 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" }, 367 + { 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" }, 368 + { 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" }, 369 + { 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" }, 370 + { 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" }, 371 + { 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" }, 372 + { 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" }, 373 + { 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" }, 374 + { 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" }, 375 + { 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" }, 376 + { 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" }, 377 + { 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" }, 378 + { 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" }, 379 + { 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" }, 380 + { 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" }, 381 + { 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" }, 382 + { 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" }, 383 + { 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" }, 384 + { 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" }, 385 + { 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" }, 386 + { 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" }, 387 + { 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" }, 388 + { 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" }, 389 + { 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" }, 390 + { 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" }, 391 + { 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" }, 392 + { 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" }, 393 + { 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" }, 394 + { 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" }, 395 + { 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" }, 396 + { 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" }, 397 + { 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" }, 398 + { 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" }, 399 + { 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" }, 400 + { 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" }, 401 + { 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" }, 402 + { 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" }, 403 + { 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" }, 404 + { 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" }, 405 + { 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" }, 406 + { 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" }, 407 + { 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" }, 408 + ] 409 + 410 + [[package]] 411 + name = "respx" 412 + version = "0.23.1" 413 + source = { registry = "https://pypi.org/simple" } 414 + dependencies = [ 415 + { name = "httpx" }, 416 + ] 417 + sdist = { url = "https://files.pythonhosted.org/packages/43/98/4e55c9c486404ec12373708d015ebce157966965a5ebe7f28ff2c784d41b/respx-0.23.1.tar.gz", hash = "sha256:242dcc6ce6b5b9bf621f5870c82a63997e8e82bc7c947f9ffe272b8f3dd5a780", size = 29243, upload-time = "2026-04-08T14:37:16.008Z" } 418 + wheels = [ 419 + { url = "https://files.pythonhosted.org/packages/1d/4a/221da6ca167db45693d8d26c7dc79ccfc978a440251bf6721c9aaf251ac0/respx-0.23.1-py2.py3-none-any.whl", hash = "sha256:b18004b029935384bccfa6d7d9d74b4ec9af73a081cc28600fffc0447f4b8c1a", size = 25557, upload-time = "2026-04-08T14:37:14.613Z" }, 420 + ] 421 + 422 + [[package]] 423 + name = "ruff" 424 + version = "0.15.10" 425 + source = { registry = "https://pypi.org/simple" } 426 + sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" } 427 + wheels = [ 428 + { url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" }, 429 + { url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" }, 430 + { url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" }, 431 + { url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" }, 432 + { url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" }, 433 + { url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" }, 434 + { url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" }, 435 + { url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" }, 436 + { url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" }, 437 + { url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" }, 438 + { url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" }, 439 + { url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" }, 440 + { url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" }, 441 + { url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" }, 442 + { url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" }, 443 + { url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" }, 444 + { url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" }, 445 + ] 446 + 447 + [[package]] 448 + name = "starlette" 449 + version = "1.0.0" 450 + source = { registry = "https://pypi.org/simple" } 451 + dependencies = [ 452 + { name = "anyio" }, 453 + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 454 + ] 455 + 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" } 456 + wheels = [ 457 + { 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" }, 458 + ] 459 + 460 + [[package]] 461 + name = "typing-extensions" 462 + version = "4.15.0" 463 + source = { registry = "https://pypi.org/simple" } 464 + sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } 465 + wheels = [ 466 + { 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" }, 467 + ] 468 + 469 + [[package]] 470 + name = "typing-inspection" 471 + version = "0.4.2" 472 + source = { registry = "https://pypi.org/simple" } 473 + dependencies = [ 474 + { name = "typing-extensions" }, 475 + ] 476 + 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" } 477 + wheels = [ 478 + { 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" }, 479 + ] 480 + 481 + [[package]] 482 + name = "uvicorn" 483 + version = "0.44.0" 484 + source = { registry = "https://pypi.org/simple" } 485 + dependencies = [ 486 + { name = "click" }, 487 + { name = "h11" }, 488 + ] 489 + sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" } 490 + wheels = [ 491 + { url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" }, 492 + ] 493 + 494 + [package.optional-dependencies] 495 + standard = [ 496 + { name = "colorama", marker = "sys_platform == 'win32'" }, 497 + { name = "httptools" }, 498 + { name = "python-dotenv" }, 499 + { name = "pyyaml" }, 500 + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, 501 + { name = "watchfiles" }, 502 + { name = "websockets" }, 503 + ] 504 + 505 + [[package]] 506 + name = "uvloop" 507 + version = "0.22.1" 508 + source = { registry = "https://pypi.org/simple" } 509 + 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" } 510 + wheels = [ 511 + { 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" }, 512 + { 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" }, 513 + { 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" }, 514 + { 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" }, 515 + { 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" }, 516 + { 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" }, 517 + { 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" }, 518 + { 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" }, 519 + { 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" }, 520 + { 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" }, 521 + { 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" }, 522 + { 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" }, 523 + { 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" }, 524 + { 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" }, 525 + { 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" }, 526 + { 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" }, 527 + { 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" }, 528 + { 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" }, 529 + { 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" }, 530 + { 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" }, 531 + { 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" }, 532 + { 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" }, 533 + { 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" }, 534 + { 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" }, 535 + { 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" }, 536 + { 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" }, 537 + { 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" }, 538 + { 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" }, 539 + { 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" }, 540 + { 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" }, 541 + ] 542 + 543 + [[package]] 544 + name = "waggle" 545 + version = "0.1.0" 546 + source = { editable = "." } 547 + dependencies = [ 548 + { name = "fastapi" }, 549 + { name = "httpx" }, 550 + { name = "python-dotenv" }, 551 + { name = "uvicorn", extra = ["standard"] }, 552 + ] 553 + 554 + [package.optional-dependencies] 555 + dev = [ 556 + { name = "pytest" }, 557 + { name = "pytest-asyncio" }, 558 + { name = "respx" }, 559 + { name = "ruff" }, 560 + ] 561 + 562 + [package.metadata] 563 + requires-dist = [ 564 + { name = "fastapi", specifier = ">=0.115" }, 565 + { name = "httpx", specifier = ">=0.27" }, 566 + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, 567 + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23" }, 568 + { name = "python-dotenv", specifier = ">=1.0" }, 569 + { name = "respx", marker = "extra == 'dev'", specifier = ">=0.21" }, 570 + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6" }, 571 + { name = "uvicorn", extras = ["standard"], specifier = ">=0.30" }, 572 + ] 573 + provides-extras = ["dev"] 574 + 575 + [[package]] 576 + name = "watchfiles" 577 + version = "1.1.1" 578 + source = { registry = "https://pypi.org/simple" } 579 + dependencies = [ 580 + { name = "anyio" }, 581 + ] 582 + 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" } 583 + wheels = [ 584 + { 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" }, 585 + { 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" }, 586 + { 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" }, 587 + { 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" }, 588 + { 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" }, 589 + { 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" }, 590 + { 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" }, 591 + { 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" }, 592 + { 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" }, 593 + { 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" }, 594 + { 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" }, 595 + { 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" }, 596 + { 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" }, 597 + { 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" }, 598 + { 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" }, 599 + { 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" }, 600 + { 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" }, 601 + { 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" }, 602 + { 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" }, 603 + { 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" }, 604 + { 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" }, 605 + { 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" }, 606 + { 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" }, 607 + { 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" }, 608 + { 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" }, 609 + { 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" }, 610 + { 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" }, 611 + { 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" }, 612 + { 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" }, 613 + { 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" }, 614 + { 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" }, 615 + { 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" }, 616 + { 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" }, 617 + { 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" }, 618 + { 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" }, 619 + { 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" }, 620 + { 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" }, 621 + { 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" }, 622 + { 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" }, 623 + { 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" }, 624 + { 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" }, 625 + { 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" }, 626 + { 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" }, 627 + { 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" }, 628 + { 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" }, 629 + { 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" }, 630 + { 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" }, 631 + { 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" }, 632 + { 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" }, 633 + { 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" }, 634 + { 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" }, 635 + { 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" }, 636 + { 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" }, 637 + { 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" }, 638 + { 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" }, 639 + { 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" }, 640 + { 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" }, 641 + { 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" }, 642 + { 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" }, 643 + { 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" }, 644 + { 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" }, 645 + { 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" }, 646 + { 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" }, 647 + { 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" }, 648 + { 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" }, 649 + { 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" }, 650 + { 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" }, 651 + { 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" }, 652 + { 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" }, 653 + { 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" }, 654 + { 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" }, 655 + { 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" }, 656 + { 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" }, 657 + { 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" }, 658 + { 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" }, 659 + { 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" }, 660 + ] 661 + 662 + [[package]] 663 + name = "websockets" 664 + version = "16.0" 665 + source = { registry = "https://pypi.org/simple" } 666 + 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" } 667 + wheels = [ 668 + { 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" }, 669 + { 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" }, 670 + { 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" }, 671 + { 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" }, 672 + { 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" }, 673 + { 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" }, 674 + { 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" }, 675 + { 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" }, 676 + { 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" }, 677 + { 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" }, 678 + { 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" }, 679 + { 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" }, 680 + { 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" }, 681 + { 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" }, 682 + { 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" }, 683 + { 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" }, 684 + { 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" }, 685 + { 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" }, 686 + { 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" }, 687 + { 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" }, 688 + { 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" }, 689 + { 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" }, 690 + { 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" }, 691 + { 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" }, 692 + { 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" }, 693 + { 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" }, 694 + { 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" }, 695 + { 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" }, 696 + { 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" }, 697 + { 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" }, 698 + { 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" }, 699 + { 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" }, 700 + { 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" }, 701 + { 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" }, 702 + { 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" }, 703 + { 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" }, 704 + { 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" }, 705 + { 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" }, 706 + { 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" }, 707 + { 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" }, 708 + { 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" }, 709 + { 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" }, 710 + { 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" }, 711 + { 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" }, 712 + { 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" }, 713 + { 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" }, 714 + { 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" }, 715 + { 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" }, 716 + { 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" }, 717 + { 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" }, 718 + { 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" }, 719 + ]