From 84ebc608dd49daa047b7f6a83fad27164fcd36a0 Mon Sep 17 00:00:00 2001 From: ildyria Date: Sat, 21 Mar 2026 15:12:34 +0100 Subject: [PATCH 01/14] Implement python --- .github/workflows/CICD.yml | 2 + .github/workflows/python_ai_vision.yml | 154 ++ ai-vision-service/Dockerfile | 47 + ai-vision-service/README.md | 120 ++ ai-vision-service/app/__init__.py | 1 + .../app/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 206 bytes .../app/__pycache__/config.cpython-313.pyc | Bin 0 -> 2518 bytes .../app/__pycache__/main.cpython-313.pyc | Bin 0 -> 3594 bytes ai-vision-service/app/api/__init__.py | 1 + .../api/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 227 bytes .../__pycache__/dependencies.cpython-313.pyc | Bin 0 -> 2007 bytes .../api/__pycache__/routes.cpython-313.pyc | Bin 0 -> 10860 bytes .../api/__pycache__/schemas.cpython-313.pyc | Bin 0 -> 4630 bytes ai-vision-service/app/api/dependencies.py | 42 + ai-vision-service/app/api/routes.py | 247 +++ ai-vision-service/app/api/schemas.py | 160 ++ ai-vision-service/app/clustering/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 227 bytes .../__pycache__/clusterer.cpython-313.pyc | Bin 0 -> 3367 bytes ai-vision-service/app/clustering/clusterer.py | 69 + ai-vision-service/app/config.py | 104 + ai-vision-service/app/detection/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 226 bytes .../__pycache__/cropper.cpython-313.pyc | Bin 0 -> 4496 bytes .../__pycache__/detector.cpython-313.pyc | Bin 0 -> 6952 bytes ai-vision-service/app/detection/cropper.py | 97 + ai-vision-service/app/detection/detector.py | 170 ++ ai-vision-service/app/embeddings/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 216 bytes .../__pycache__/sqlite_store.cpython-313.pyc | Bin 0 -> 7847 bytes .../__pycache__/store.cpython-313.pyc | Bin 0 -> 2474 bytes ai-vision-service/app/embeddings/factory.py | 42 + .../app/embeddings/pgvector_store.py | 148 ++ .../app/embeddings/sqlite_store.py | 169 ++ ai-vision-service/app/embeddings/store.py | 63 + ai-vision-service/app/main.py | 92 + ai-vision-service/app/matching/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 225 bytes .../__pycache__/matcher.cpython-313.pyc | Bin 0 -> 2765 bytes ai-vision-service/app/matching/matcher.py | 62 + ai-vision-service/pyproject.toml | 91 + ai-vision-service/tests/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 220 bytes .../conftest.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 5910 bytes .../test_api.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 18415 bytes ...st_clustering.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 11128 bytes .../test_cropper.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 10435 bytes ...est_detection.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 19853 bytes ...st_embeddings.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 15020 bytes ...test_matching.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 9622 bytes ai-vision-service/tests/conftest.py | 152 ++ ai-vision-service/tests/test_api.py | 181 ++ ai-vision-service/tests/test_clustering.py | 111 ++ ai-vision-service/tests/test_cropper.py | 114 ++ ai-vision-service/tests/test_detection.py | 182 ++ ai-vision-service/tests/test_embeddings.py | 125 ++ ai-vision-service/tests/test_matching.py | 102 + ai-vision-service/tests/test_smoke.py | 113 ++ ai-vision-service/uv.lock | 1767 +++++++++++++++++ .../features/030-ai-vision-service/tasks.md | 36 + 60 files changed, 4769 insertions(+) create mode 100644 .github/workflows/python_ai_vision.yml create mode 100644 ai-vision-service/Dockerfile create mode 100644 ai-vision-service/README.md create mode 100644 ai-vision-service/app/__init__.py create mode 100644 ai-vision-service/app/__pycache__/__init__.cpython-313.pyc create mode 100644 ai-vision-service/app/__pycache__/config.cpython-313.pyc create mode 100644 ai-vision-service/app/__pycache__/main.cpython-313.pyc create mode 100644 ai-vision-service/app/api/__init__.py create mode 100644 ai-vision-service/app/api/__pycache__/__init__.cpython-313.pyc create mode 100644 ai-vision-service/app/api/__pycache__/dependencies.cpython-313.pyc create mode 100644 ai-vision-service/app/api/__pycache__/routes.cpython-313.pyc create mode 100644 ai-vision-service/app/api/__pycache__/schemas.cpython-313.pyc create mode 100644 ai-vision-service/app/api/dependencies.py create mode 100644 ai-vision-service/app/api/routes.py create mode 100644 ai-vision-service/app/api/schemas.py create mode 100644 ai-vision-service/app/clustering/__init__.py create mode 100644 ai-vision-service/app/clustering/__pycache__/__init__.cpython-313.pyc create mode 100644 ai-vision-service/app/clustering/__pycache__/clusterer.cpython-313.pyc create mode 100644 ai-vision-service/app/clustering/clusterer.py create mode 100644 ai-vision-service/app/config.py create mode 100644 ai-vision-service/app/detection/__init__.py create mode 100644 ai-vision-service/app/detection/__pycache__/__init__.cpython-313.pyc create mode 100644 ai-vision-service/app/detection/__pycache__/cropper.cpython-313.pyc create mode 100644 ai-vision-service/app/detection/__pycache__/detector.cpython-313.pyc create mode 100644 ai-vision-service/app/detection/cropper.py create mode 100644 ai-vision-service/app/detection/detector.py create mode 100644 ai-vision-service/app/embeddings/__init__.py create mode 100644 ai-vision-service/app/embeddings/__pycache__/__init__.cpython-313.pyc create mode 100644 ai-vision-service/app/embeddings/__pycache__/sqlite_store.cpython-313.pyc create mode 100644 ai-vision-service/app/embeddings/__pycache__/store.cpython-313.pyc create mode 100644 ai-vision-service/app/embeddings/factory.py create mode 100644 ai-vision-service/app/embeddings/pgvector_store.py create mode 100644 ai-vision-service/app/embeddings/sqlite_store.py create mode 100644 ai-vision-service/app/embeddings/store.py create mode 100644 ai-vision-service/app/main.py create mode 100644 ai-vision-service/app/matching/__init__.py create mode 100644 ai-vision-service/app/matching/__pycache__/__init__.cpython-313.pyc create mode 100644 ai-vision-service/app/matching/__pycache__/matcher.cpython-313.pyc create mode 100644 ai-vision-service/app/matching/matcher.py create mode 100644 ai-vision-service/pyproject.toml create mode 100644 ai-vision-service/tests/__init__.py create mode 100644 ai-vision-service/tests/__pycache__/__init__.cpython-313.pyc create mode 100644 ai-vision-service/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc create mode 100644 ai-vision-service/tests/__pycache__/test_api.cpython-313-pytest-9.0.2.pyc create mode 100644 ai-vision-service/tests/__pycache__/test_clustering.cpython-313-pytest-9.0.2.pyc create mode 100644 ai-vision-service/tests/__pycache__/test_cropper.cpython-313-pytest-9.0.2.pyc create mode 100644 ai-vision-service/tests/__pycache__/test_detection.cpython-313-pytest-9.0.2.pyc create mode 100644 ai-vision-service/tests/__pycache__/test_embeddings.cpython-313-pytest-9.0.2.pyc create mode 100644 ai-vision-service/tests/__pycache__/test_matching.cpython-313-pytest-9.0.2.pyc create mode 100644 ai-vision-service/tests/conftest.py create mode 100644 ai-vision-service/tests/test_api.py create mode 100644 ai-vision-service/tests/test_clustering.py create mode 100644 ai-vision-service/tests/test_cropper.py create mode 100644 ai-vision-service/tests/test_detection.py create mode 100644 ai-vision-service/tests/test_embeddings.py create mode 100644 ai-vision-service/tests/test_matching.py create mode 100644 ai-vision-service/tests/test_smoke.py create mode 100644 ai-vision-service/uv.lock diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index aa258dd32b4..4e391e3dcba 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -11,11 +11,13 @@ on: - '**/*.md' - 'public/dist/*.js' - 'public/dist/**/*.js' + - 'ai-vision-service/**' pull_request: paths-ignore: - '**/*.md' - 'public/dist/*.js' - 'public/dist/**/*.js' + - 'ai-vision-service/**' # Allow manually triggering the workflow. workflow_dispatch: diff --git a/.github/workflows/python_ai_vision.yml b/.github/workflows/python_ai_vision.yml new file mode 100644 index 00000000000..3610caeca51 --- /dev/null +++ b/.github/workflows/python_ai_vision.yml @@ -0,0 +1,154 @@ +name: Python AI Vision Service + +on: + push: + branches: + - master + - assisted-vision + paths: + - 'ai-vision-service/**' + pull_request: + paths: + - 'ai-vision-service/**' + workflow_dispatch: + +# Declare default permissions as read only. +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ !contains(github.ref, 'master') && !startsWith(github.ref, 'refs/tags/') }} + +defaults: + run: + working-directory: ai-vision-service + +jobs: + # --------------------------------------------------------------------------- + # Lint – formatting and style + # --------------------------------------------------------------------------- + lint: + name: Lint (ruff) + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + with: + egress-policy: audit + + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up uv + uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd9f5de38368cc08bae2cc2fb # v5.4.1 + with: + enable-cache: true + + - name: Install dev dependencies + run: uv sync --frozen + + - name: Check formatting + run: uv run ruff format --check app/ tests/ + + - name: Lint + run: uv run ruff check app/ tests/ + + # --------------------------------------------------------------------------- + # Type check + # --------------------------------------------------------------------------- + typecheck: + name: Type check (ty) + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + with: + egress-policy: audit + + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up uv + uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd9f5de38368cc08bae2cc2fb # v5.4.1 + with: + enable-cache: true + + - name: Install dev dependencies + run: uv sync --frozen + + - name: Type check + run: uv run ty check app/ + + # --------------------------------------------------------------------------- + # Test matrix – Python 3.13 and 3.14 + # --------------------------------------------------------------------------- + test: + name: Tests (Python ${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - "3.13" + - "3.14" + + steps: + - name: Harden Runner + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + with: + egress-policy: audit + + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up uv + uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd9f5de38368cc08bae2cc2fb # v5.4.1 + with: + enable-cache: true + python-version: ${{ matrix.python-version }} + + - name: Install dev dependencies + run: uv sync --frozen + + - name: Run tests + run: uv run pytest --cov=app --cov-report=xml -v + + - name: Upload coverage + if: matrix.python-version == '3.13' + uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 + with: + files: ai-vision-service/coverage.xml + flags: ai-vision-service + continue-on-error: true + + # --------------------------------------------------------------------------- + # Docker build verification + # --------------------------------------------------------------------------- + docker-build: + name: Docker build + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + with: + egress-policy: audit + + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@b5ca514318bd2bf3c1fb87b4f2a0afaa4d2a7c4e # v3.10.0 + + - name: Build Docker image (no model bake in CI to save time) + uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0 + with: + context: ai-vision-service + push: false + load: true + tags: lychee-ai-vision:ci + # Override the model-bake step by targeting the builder stage + # to avoid downloading 300MB of model weights in CI. + target: builder + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/ai-vision-service/Dockerfile b/ai-vision-service/Dockerfile new file mode 100644 index 00000000000..1ecfb6a4cc7 --- /dev/null +++ b/ai-vision-service/Dockerfile @@ -0,0 +1,47 @@ +# Multi-stage build: keep the runtime image lean. + +# --------------------------------------------------------------------------- +# Stage 1 – builder: install dependencies and bake the model weights. +# --------------------------------------------------------------------------- +FROM python:3.13-slim AS builder + +# Install uv from the official image. +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv + +WORKDIR /app + +# Install dependencies only (no source code) so layer cache is reused when +# only application code changes. +COPY pyproject.toml uv.lock ./ +RUN uv sync --frozen --no-dev + +# Bake buffalo_l model weights into the image at build time (~300 MB download). +# The resulting image is ~1 GB larger but starts instantly and works in +# airgapped environments. Model updates require an image rebuild. +RUN uv run python -c \ + "from insightface.app import FaceAnalysis; \ + a = FaceAnalysis(name='buffalo_l', root='/root/.insightface', providers=['CPUExecutionProvider']); \ + a.prepare(ctx_id=-1); \ + print('buffalo_l model downloaded.')" + +# --------------------------------------------------------------------------- +# Stage 2 – runtime: copy only what's needed to run. +# --------------------------------------------------------------------------- +FROM python:3.13-slim AS runtime + +WORKDIR /app + +# Copy the pre-built virtualenv and baked model weights from the builder stage. +COPY --from=builder /app/.venv /app/.venv +COPY --from=builder /root/.insightface /root/.insightface + +# Copy application source. +COPY app/ ./app/ + +ENV PATH="/app/.venv/bin:$PATH" + +EXPOSE 8000 + +# Use a shell-form CMD so that the ${VISION_FACE_WORKERS:-1} variable is +# expanded at container startup, not at image build time. +CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers ${VISION_FACE_WORKERS:-1}"] diff --git a/ai-vision-service/README.md b/ai-vision-service/README.md new file mode 100644 index 00000000000..49fd8899989 --- /dev/null +++ b/ai-vision-service/README.md @@ -0,0 +1,120 @@ +# Lychee AI Vision Service + +Facial recognition microservice for [Lychee](https://github.com/LycheeOrg/Lychee). + +Detects faces in photos, stores embeddings, and supports selfie-based person +claiming via a REST API consumed by the Lychee PHP backend. + +## Tech stack + +| Concern | Library | +|---------|---------| +| Web framework | FastAPI + Uvicorn | +| Face detection & recognition | InsightFace (`buffalo_l`) + ONNX Runtime | +| Face crop generation | Pillow | +| Embedding clustering | scikit-learn (DBSCAN) | +| Embedding storage | SQLite + sqlite-vec (default) / PostgreSQL + pgvector | +| HTTP client (callbacks) | httpx | +| Config | Pydantic BaseSettings | + +## Directory layout + +``` +ai-vision-service/ +├── app/ +│ ├── __init__.py +│ ├── config.py # AppSettings (Pydantic BaseSettings) +│ ├── main.py # FastAPI app factory & lifespan handler +│ ├── api/ +│ │ ├── __init__.py +│ │ ├── dependencies.py # API key auth dependency +│ │ ├── routes.py # /detect, /match, /health +│ │ └── schemas.py # Pydantic request/response models +│ ├── detection/ +│ │ ├── __init__.py +│ │ ├── detector.py # InsightFace wrapper +│ │ └── cropper.py # 150×150 JPEG crop generator +│ ├── embeddings/ +│ │ ├── __init__.py +│ │ ├── store.py # Abstract EmbeddingStore protocol +│ │ ├── sqlite_store.py # SQLite + sqlite-vec implementation +│ │ └── pgvector_store.py # PostgreSQL + pgvector implementation +│ ├── clustering/ +│ │ ├── __init__.py +│ │ └── clusterer.py # DBSCAN clustering +│ └── matching/ +│ ├── __init__.py +│ └── matcher.py # Selfie similarity matching +├── tests/ +│ └── __init__.py +├── Dockerfile +├── pyproject.toml +└── README.md +``` + +## Environment variables + +All variables are prefixed `VISION_FACE_`. + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `VISION_FACE_LYCHEE_API_URL` | Yes | — | Lychee base URL for callbacks | +| `VISION_FACE_API_KEY` | Yes | — | API key expected on inbound requests from Lychee | +| `VISION_FACE_LYCHEE_API_KEY` | Yes | — | API key sent on outbound callbacks to Lychee | +| `VISION_FACE_MODEL_NAME` | No | `buffalo_l` | InsightFace model pack | +| `VISION_FACE_DETECTION_THRESHOLD` | No | `0.5` | Confidence filter for detected faces | +| `VISION_FACE_MATCH_THRESHOLD` | No | `0.5` | Cosine-similarity cutoff for selfie matching | +| `VISION_FACE_RESCAN_IOU_THRESHOLD` | No | `0.5` | IoU threshold for bounding-box matching on re-scan | +| `VISION_FACE_MAX_FACES_PER_PHOTO` | No | `10` | Maximum faces included in a callback payload | +| `VISION_FACE_THREAD_POOL_SIZE` | No | `1` | Inference thread-pool size | +| `VISION_FACE_STORAGE_BACKEND` | No | `sqlite` | `sqlite` or `pgvector` | +| `VISION_FACE_STORAGE_PATH` | No | `/data/embeddings` | SQLite DB directory | +| `VISION_FACE_PG_HOST` | No* | `localhost` | PostgreSQL host (*required with pgvector) | +| `VISION_FACE_PG_PORT` | No | `5432` | PostgreSQL port | +| `VISION_FACE_PG_DATABASE` | No* | `ai_vision` | PostgreSQL database (*required with pgvector) | +| `VISION_FACE_PG_USER` | No* | `ai_vision` | PostgreSQL user (*required with pgvector) | +| `VISION_FACE_PG_PASSWORD` | No* | `` | PostgreSQL password (*required with pgvector) | +| `VISION_FACE_PHOTOS_PATH` | No | `/data/photos` | Shared volume mount for photo files | +| `VISION_FACE_WORKERS` | No | `1` | Number of Uvicorn worker processes | +| `VISION_FACE_LOG_LEVEL` | No | `info` | Log level | + +## Development + +```bash +# Install uv (https://docs.astral.sh/uv/getting-started/installation/) +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Install all dependencies (including dev) +uv sync + +# Lint and format +uv run ruff format --check +uv run ruff check + +# Type check +uv run ty check + +# Run tests +uv run pytest +``` + +## Docker + +```bash +# Build (bakes buffalo_l model into the image – ~1 GB download on first build) +docker build -t lychee-ai-vision . + +# Run +docker run --rm \ + -e VISION_FACE_LYCHEE_API_URL=http://lychee \ + -e VISION_FACE_API_KEY=changeme \ + -e VISION_FACE_LYCHEE_API_KEY=changeme \ + -v /path/to/photos:/data/photos:ro \ + -v ai-vision-embeddings:/data/embeddings \ + -p 8000:8000 \ + lychee-ai-vision +``` + +--- + +*Last updated: 2026-03-21* diff --git a/ai-vision-service/app/__init__.py b/ai-vision-service/app/__init__.py new file mode 100644 index 00000000000..6f6a628fe93 --- /dev/null +++ b/ai-vision-service/app/__init__.py @@ -0,0 +1 @@ +"""Lychee AI Vision Service.""" diff --git a/ai-vision-service/app/__pycache__/__init__.cpython-313.pyc b/ai-vision-service/app/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7e58c5ec2d62a8fb9f3a002aee3755033166cd13 GIT binary patch literal 206 zcmey&%ge<81j}ab%Txf;k3k$5V1zP0a{w7r8G;##7}6OvnW`jxDw8u(QxzON6~Z!$ zGxPHlf>VpiGLuvF{4^PFvB$@!sapm%O1Btdlzw7Cfqr~^W?p7Ve7s&k Vrxk;o^?{j@k?|ITd=U$f0|0w!IHmvq literal 0 HcmV?d00001 diff --git a/ai-vision-service/app/__pycache__/config.cpython-313.pyc b/ai-vision-service/app/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..91cfca1bf2b779ea4991ed6595e6a04e7af5cd18 GIT binary patch literal 2518 zcma)8&2Jk;6kpq8J8_(^+NqN?NjFIYHKO(f5Z@^DOG&G?bVU)W(Ri{uwuh{Dmzl9? zA`X!_fW(1ZkT~!U@K5kJAX!M(oQyc2-cqI9c{95S=Hq}RzkR=XGwqhKe4x_E$cT8&VJcMIeiGqca~#u%Sk!uJE`3?Pup70+{x@Bj&`#=Yg>!fhUK7T z%gHv-tC@H$&o$Ccu915s?c|rOPtmK1OP+5}R3{IA*^VNgxkR!su-!0dv38HgsLzP~ z;J_n+WUl=_5!9hlvY;($g~GP)+aiHB;nem+;!)3T@vv*tpwD<1bZH>%KH-e)`Bc~u zr!Dpb=6ud%$8I(s?KnI4@8Mh9*KguxvsSqIgmfdHzG=%21v7~l-lqr6rrl!H_v|i- zge}9wI=*=O!4AH2^JAC^fpx%I;i=waHKitf-oq|&JG7cs#qm9;3un8zZZ>^`xzbIJ z#ys)~IDL@PN8hk`3XVVRq~U zCeLxf;KDhcGI%P%MP6)7!~E$4&+r+8XV3Ah!E*_o=W{0Bg#?%QyusxJFYpTnUrca? zm(2Rb1TXQju`i$F1%odoc!ggycs0RmykhX>1lxSk;Pn_cmVl5O&Zcw4*>YZZ1My|& zMSiJ$wOUnEK)kancv^&0JpAHe52)6q|Da(j-HHRB%uO(@p9>v3P@69WFt0EASNd%aeR_#yUHp&NSC#{ucmc1)Mid#dD7 zNnM>!SavuSozVBxY?ny4Gd55Li%SB`!rot_Pjw|wr7n4bTg0UTN0j5J6Uy)@kkC?d z+9C096ox()?2xMb=ON#xT&SG*%x99SSs_DC+7#~*cb^8{;auG#lGJH;k9wZYUR6As z5)s*f6bW78cS0dmK5ApFu-6wLm_|{^rJ5uR_sz+xDHzl4_MjS=2|Yo%vdnlygn+m4 z4vTR_^WFkX`u$-mIv8R>E7T#|*r$E!tKxVfAEFv$@L{$mTi0H{RYj@*5XW^xO=!Kv ze5&TpG)x0G)O%7hF1!#HG!RU(KK;?US6xvP*xU@ps(>-vRL`fN7cu^_iV9cRFTYN^}MYaVcPx4tkFsH=@J(^*GPZXwifGJY-*v4y(-3(iYu{Nz*y+uE+$fPw%3zD3e2l8^A!G; zx?b;uU0UB`{rZj2?dg&$>JNDMDKJpf?;1+i3A+~G-!-@`ojG+9MfEsDEjr+N@MMHe zL$L}}`^tKj&SWP}iq^v709A(QlhO3tmv@I~Z&a?Bf!&jwRoOV&JwKG8FA-V>}l`G%};N}p$H(Gq@=faVmQ}_*>&hgH`w5(~BU!vmWcrHNUgYdj&ev;q!-P zrA=(po;>JyV+9d<84KX%15@R$6X>?G!-U=H1*Wptx;HnQH)sSYF0Qtz1oFqtxoXzh zw}_C?7;PQ`XY39MJRizLt#H3jIcHwnVWXO7JU|{D;4ra>$K&okiQNJ3q-nn_(A0v$ z=kEXY0iZiN#DVtQb32fa|NR9}D^yc4*EE0QQ+y3HRU8k<-+-Ye{Do%Yk@bAeDptOE z2l!)F+^bIT8W_~1ISVL+g6q>(^HK7ta()sV!MMq)II4DRQfDmm#M^OUc$dEoR((}M zD<_#$D)lUlQs|GOm9l@g-hH0?AeG7;zx>?N&y!BtN>81n^Qood{Zk7brx_S{rU#7o FKLK@y=Bxk! literal 0 HcmV?d00001 diff --git a/ai-vision-service/app/__pycache__/main.cpython-313.pyc b/ai-vision-service/app/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cc96017c9730f1b3c174f763d9027f9cfae20ec3 GIT binary patch literal 3594 zcmai1&2JRR6|bJ@p6{{8HrNIeR2U2lS!PTG0W3grz+lfTws!_+S(&8W?yi|>Xm^jQ zdJML57^0O>vWF-KB&1#C6h)i=AgA0TMG6KYk6J+x?T55C6SI+`T=J^A+e6ltw&m%1 zUG?hKt5@}Y@8|7oHih8%_3!>{z5?kB{t-S=Uy!!H2H`OpLsdjkRjNkHQYk_sn9I>} zlt!VI%W_Gfis)0yu~MAIg&Zp^~>@9dz(cyxLpM9z=KLbAb*Ws^+SF z)gIn=SE9prhDXuP*nw*1&cSN>1nAbI`F`yP7`m=8cXECPYt5!*>Y8ghHm+&9>(I4A zDm7)hbPYEh({^#qq1bH@JUN4ZYBH`@C3Mx)Nde;Ib6^3@#V%nmLDy_-+INZWVh!sp<~l1loR-oZ z+a(V`mFqVtIo))cEelp>U>qDMFc&_+`hC-F;1x%|hn;0^i-Aq%63xI)4O@<8n07tI zw*|gic*bUCz2RQhbOMtMVii)~@W|#RFRj_O14dy7Y!<%zZ#7_wG4D9m)B~coAY#xv zps_Vu54>B^Y^_dcKH_C>eKbF%UY(x0TArD`;YBCyHBgQR!3Qx$ofd=uRMV5qW|g=u z_{F?TowzCszCf*KlAFIlY(h0y(2K%WX#yP7gFC^UMRE8=BS_l*Lj<9t2*ZtD+!+sV zgDow>sQ*;tQ0lg$Xp!3yPgiq6s_^B$^HkE`>4490o$lO9W)H0(^QzKC39=&sSr#uP zY{U7%TUb zdTkjzl(~mtHrmSf*p7p3++8b_7L>}-Q0}&&$LE6hfahc3DA+k3%H6h_xG34N+v9to zhP6M`yFI_>_~6d?(w^fxF|8)g%d6-ypx;8QaweSBZL6tjnxEwN#3B<$4WoMUe6%Lz zv+F;Zr;gFm1p?kdpu<24u16nwn)YB1QonC@DaOC>B{I7Uc&9Um2ds z*8p5Nph0q_L{h6Z0FF0ch?_u3(+3w{rBM&yxE7}9~zC-U3%4V-a zhkRc%-Sv@OFpF8E+%i!Iitn%K4MK{V zd3sfpv(wBkXGI84aYZxjLUYaQQw>tnT9&JZSCak^R^Zsd{tK^fP#e9Cp&#Twoo!Ep zpky}Xp=a{YXZ7FLKYy@UoZKi*ZWeEB6mL8)&iwWK-!A>-((}W&pUEG63%Y0j**nr1 z7}*>+zA3T4=}(Q#D<5oJ z`QU|I@eO2#pA0|GjJBr&olfFXCz0Ds;Ee?CB(lFr|0?}S_HA5NVsCp;ZuIlUX5q#L z{Pj+^r(Yj9@@VGa%nNz2GdRMfkLNl&U0=${*D`Eo|AxH($+@SzJ+kA+>A%Z`|1cmd z=$BKyQ+?9z>Vu&A{2ay~qXo1K$+&>#ltuXVAE-S6t_jWqsJ{Tb>Y{8%79tfT)atg2 zd@1pCabqEJ4CNJV1`69%!8-&jK;?xTXi*;(_8>PvY9?u&O{Dl83D0x_&xO=P)Ne>o zpI!tN9J>x~0n%tpfDK%bkShr@@0tQK^kIY8z(Qsn8wkjIc%s&_^@*ij7ikHbj9WDd zJ4WVK38kh%0678@1PccoNN^aDGJ?|pT?pxm@F`#`sS0sh)DHGH$v5K1!@v`|r767OIhphl;bG)ukz#_@cs-`ATP=gA zK0l&KxCQwr9f!ut^5E^{B@ndku`qtU;8^G>u9)f)Q+gIUuECF8hF2SH4Wc9Y4Se>u z(=R5 z=*TNH{&h6>QZBqs<~EZDpCu1Ix$r!BtR35mp~(L2$e2{zj)JtsrGL*$`=p^a<0yCV QlV7x>?GIm|5#huC0F17fQ~&?~ literal 0 HcmV?d00001 diff --git a/ai-vision-service/app/api/__init__.py b/ai-vision-service/app/api/__init__.py new file mode 100644 index 00000000000..0a5af4779e5 --- /dev/null +++ b/ai-vision-service/app/api/__init__.py @@ -0,0 +1 @@ +"""FastAPI routes, dependencies, and schemas.""" diff --git a/ai-vision-service/app/api/__pycache__/__init__.cpython-313.pyc b/ai-vision-service/app/api/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c16a732d677927b9ef2925bda7b305269ad0e8b9 GIT binary patch literal 227 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 literal 0 HcmV?d00001 diff --git a/ai-vision-service/app/api/__pycache__/schemas.cpython-313.pyc b/ai-vision-service/app/api/__pycache__/schemas.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..baa99be87bda084221c02f7eb470e06c70333533 GIT binary patch literal 4630 zcmb_f&2JmW72hR?A6k;4zAf2~yR{wLtS!=YV-)^KB3Y4Q`9l`GP5?IqmaE+%x#^0_ z%r5OxCjmYr=t(HxLOm4lt@S@q6glM>fk43SNk$KaZcfFb*S!W0oA=)CXEKTe*B^fOC;RsklJr;n)4gKhV7~){=aMEdNt3n6UHMLgMeamdRF$Ts z(~=fFE@`9~xf&Jo8R6O(Xybj_7}q91o9xrZxpoM&N}o2twJFf1`?N`}&46~ePdmi5 zSR6s?Fshatj?+~A zW}VI&wVLZ2KDBPBHCHW_=4;JTiJQdKhDWWuvWG#+MS`JQhDYz?O9ind^k<6b2^oe` z+P@6{pG!rF$q@dCCbQ_P5+_5Y;PEB z8(-dXttRZO20LzcclERi8@Ft)Zun-^u2ob+EgL4CwfK4j^HX;XKrEF^!*SLO^Ot&q zIj}F=Q|+1$&R`B$nyRseuCH?xyYMk;SRSnXcv)M1u%a)`FD&Z!9xvQpT+|^p`bQ7% zfu}Na*Hv13q9WLJ*YiDywbyXq4q*k1Et|n!oMv7LlJ%he3o*d7z!t zbq_*no4Voq%wB8w6x0ll>^}UY;C7+vuG7Mr{iN`LYc|#a0k5#i+)v?!y@Fs)!LVna zaAMAexD^7Gx1@n;YIjyeXZ zLd-tInqIhzDnmIX7||M)3Y<8=;vtX6o(gU^ z3iGN5L~-B%+UvGsFiscD{4b|%{%?HB{KR`P23L^J6aKf)}@cR9ikswl` zLByef0*A6}TeN0Uj`GUO6idTwmO+8guq=uZ6r(7{P!JS90ujW@j%)aA92FQQfz%|9 zj&+3!1MeJ&&!xYRYr7N2TIASkg!S9BG;`|3_#el2re`!at00~p%0|+lh5l+78G%WaMi>%qr4QkzJ z0{&K>9fy?+S z)}_Gr$^<_JKkO0;1ZH3!z^F15xB(BTf#F@<9Ug@&J9YvBw9q0a+Ntc8vXeUT4LKoz z#ubBsz73Wi1T@y}UMlFzyGfpRCl(oV*-^BM@R0u%?XqDz4Msz>V-%mm5V1hvW8H*) zBuK17M2t$9fOA7{K6wWTn8QBkEWHEX847%V?<|OZuxA9=Gi|JFZSqMwGxF?SK;99E z1GXfS(( zhbE7=EzofS_ZxK!8bZ~ER;HKcRL^&zsd;MqRdG~&W6hz8=oMxScvJ<;XsL9#Y+|DX zjV?9J>VdDk!g8<;HiH7o-3m)}hXO-h`L3f51@8GsZ!tav38zi2h%y+E_r*G#JsA6+ zfaOhW=jkIM`cgqmB71moH`;51#L-X~-E?sX ze|81@gjM1{aFyo-4g@a{gSh%GIzoYO=3$G0T)n#snIN^3I{6JbDX_sQhp@qIAO<*u@?_3@Fuw3xG~>txlHJhZO$oPhkH=*9vbkW4t$%;uA>7LK9^WCFSv0W+#Hl+ zQ4a-QRJ0uFl;%)}ViSJeX%Gm+qM%5dEdN{O>Q$pTn0pACC@ApS|G~7$bYT!|`87D% zINc?W%9(-(qEy1lqZas{OQk%dFWh~vYJ3+Z=D@G$83s{e0j((T@gB}VBrfe{Mz=2R zWKOlnseqIOD{z(xzupxMNBcKn@TL>f`-V$E9pK+^&;%x79K({V#o16LkxK@{x2fUy zND6_*Z^1YijSm+Aj4GcEXo&2d<{!U0c4pKHvb|IU`Mv?)z!?3Nobo;Ld_dM**9oFf zy$6=o55W^df$!HVt(SJw*=Kj#P;*0P-wgP`;_<6qp@j5EY zGJIvpR z%2&6qbtJfTrc&~pyp_X6bFe5XzqS3iBf+gRl|Tnz!Fy+d$cBth!h2_0k#BB2{)YrN H{?7jbPvT7P literal 0 HcmV?d00001 diff --git a/ai-vision-service/app/api/dependencies.py b/ai-vision-service/app/api/dependencies.py new file mode 100644 index 00000000000..a89ee2f8138 --- /dev/null +++ b/ai-vision-service/app/api/dependencies.py @@ -0,0 +1,42 @@ +"""FastAPI dependency providers. + +Centralises per-request dependency resolution. All functions follow +FastAPI's ``Depends()`` contract so they can be overridden in tests via +``app.dependency_overrides``. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from fastapi import Depends, Header, HTTPException, Request + +from app.config import AppSettings, get_settings + +if TYPE_CHECKING: + from app.detection.detector import FaceDetector + from app.embeddings.store import EmbeddingStore + + +async def require_api_key( + x_api_key: str = Header(..., alias="X-API-Key"), + settings: AppSettings = Depends(get_settings), +) -> None: + """FastAPI dependency that validates the ``X-API-Key`` request header. + + Raises: + HTTPException(401): If the header is missing or does not match + ``VISION_FACE_API_KEY``. + """ + if x_api_key != settings.api_key: + raise HTTPException(status_code=401, detail="Invalid or missing API key") + + +def get_detector(request: Request) -> FaceDetector: + """Return the :class:`FaceDetector` stored in ``app.state``.""" + return request.app.state.detector + + +def get_store(request: Request) -> EmbeddingStore: + """Return the :class:`EmbeddingStore` stored in ``app.state``.""" + return request.app.state.store diff --git a/ai-vision-service/app/api/routes.py b/ai-vision-service/app/api/routes.py new file mode 100644 index 00000000000..01fa6c43bee --- /dev/null +++ b/ai-vision-service/app/api/routes.py @@ -0,0 +1,247 @@ +"""FastAPI route handlers. + +Endpoints: + POST /detect - Accept a face-detection job; run async, callback to Lychee. + POST /match - Accept a selfie; return top-N similar stored faces. + GET /health - Return service health and embedding count. +""" + +from __future__ import annotations + +import asyncio +import logging +import uuid +from pathlib import Path +from typing import TYPE_CHECKING + +import httpx +from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request, UploadFile + +from app.api.dependencies import get_detector, get_store, require_api_key +from app.api.schemas import ( + DetectCallbackPayload, + DetectCallbackResponse, + DetectRequest, + ErrorCallbackPayload, + FaceResult, + HealthResponse, + MatchResponse, + MatchResult, + SuggestionResult, +) +from app.config import AppSettings, get_settings +from app.detection.cropper import generate_crop + +if TYPE_CHECKING: + from concurrent.futures import Executor + + from app.detection.detector import DetectedFace, FaceDetector + from app.embeddings.store import EmbeddingStore + +logger = logging.getLogger(__name__) + +router = APIRouter() + + +# --------------------------------------------------------------------------- +# POST /detect +# --------------------------------------------------------------------------- + + +@router.post("/detect", status_code=202) +async def detect( + body: DetectRequest, + background_tasks: BackgroundTasks, + request: Request, + settings: AppSettings = Depends(get_settings), + _: None = Depends(require_api_key), +) -> None: + """Accept a face-detection job. + + Validates the photo path (path-traversal protection), then immediately + returns **202 Accepted** and schedules detection as a background task. + Results are POSTed back to Lychee's results endpoint once detection + completes. + """ + resolved = Path(body.photo_path).resolve() + photos_root = Path(settings.photos_path).resolve() + + if not str(resolved).startswith(str(photos_root) + "/") and resolved != photos_root: + raise HTTPException(status_code=400, detail="photo_path is outside the allowed directory") + + if not resolved.is_file(): + raise HTTPException(status_code=400, detail="photo_path does not exist or is not a file") + + detector: FaceDetector = get_detector(request) + store: EmbeddingStore = get_store(request) + executor: Executor = request.app.state.executor + + background_tasks.add_task( + _run_detection_job, + body.photo_id, + resolved, + detector, + store, + executor, + settings, + ) + + +# --------------------------------------------------------------------------- +# POST /match +# --------------------------------------------------------------------------- + + +@router.post("/match") +async def match( + file: UploadFile, + request: Request, + settings: AppSettings = Depends(get_settings), + _: None = Depends(require_api_key), +) -> MatchResponse: + """Match a selfie against stored face embeddings. + + Accepts a multipart image upload, detects the face, embeds it, and returns + the closest matches from the embedding store above the configured threshold. + + Returns **422** if no face is detected in the selfie. + """ + image_bytes = await file.read() + + detector: FaceDetector = get_detector(request) + store: EmbeddingStore = get_store(request) + executor: Executor = request.app.state.executor + + loop = asyncio.get_running_loop() + raw_faces: list[DetectedFace] = await loop.run_in_executor(executor, detector.detect_bytes, image_bytes) + + if not raw_faces: + raise HTTPException(status_code=422, detail="No face detected in the uploaded image") + + best = raw_faces[0] # highest confidence (sorted descending) + matches = store.similarity_search(best.embedding, settings.match_threshold, limit=10) + + return MatchResponse(matches=[MatchResult(lychee_face_id=face_id, confidence=conf) for face_id, conf in matches]) + + +# --------------------------------------------------------------------------- +# GET /health +# --------------------------------------------------------------------------- + + +@router.get("/health") +async def health(request: Request) -> HealthResponse: + """Return service health, model-loaded status, and embedding count. + + This endpoint is intentionally unauthenticated so that load-balancers and + Docker health checks can probe it without an API key. + """ + detector: FaceDetector = request.app.state.detector + store: EmbeddingStore = request.app.state.store + + return HealthResponse( + status="ok" if detector.is_loaded else "degraded", + model_loaded=detector.is_loaded, + embedding_count=store.count(), + ) + + +# --------------------------------------------------------------------------- +# Background detection job +# --------------------------------------------------------------------------- + + +async def _run_detection_job( + photo_id: str, + image_path: Path, + detector: FaceDetector, + store: EmbeddingStore, + executor: Executor, + settings: AppSettings, +) -> None: + """Detect faces, build the callback payload, and notify Lychee. + + Runs entirely as an async background task after the ``/detect`` route has + returned 202. All CPU-bound work is offloaded to ``executor`` via + ``run_in_executor`` so the event loop remains responsive. + """ + try: + loop = asyncio.get_running_loop() + + # --- 1. Detect faces (CPU-bound, runs in thread pool) --- + raw_faces: list[DetectedFace] = await loop.run_in_executor(executor, detector.detect, image_path) + raw_faces = raw_faces[: settings.max_faces_per_photo] + + # --- 2. For each face: generate crop + search suggestions --- + face_data: list[tuple[str, list[float], FaceResult]] = [] + + for raw_face in raw_faces: + emp_id = str(uuid.uuid4()) + + crop_b64: str = await loop.run_in_executor( + executor, + generate_crop, + image_path, + raw_face.x, + raw_face.y, + raw_face.width, + raw_face.height, + ) + + suggestions = store.similarity_search(raw_face.embedding, settings.match_threshold, limit=10) + + result = FaceResult( + x=raw_face.x, + y=raw_face.y, + width=raw_face.width, + height=raw_face.height, + confidence=raw_face.confidence, + embedding_id=emp_id, + crop=crop_b64, + suggestions=[SuggestionResult(lychee_face_id=fid, confidence=conf) for fid, conf in suggestions], + ) + face_data.append((emp_id, raw_face.embedding, result)) + + # --- 3. POST success callback to Lychee --- + payload = DetectCallbackPayload( + photo_id=photo_id, + faces=[fd[2] for fd in face_data], + ) + callback_url = f"{settings.lychee_api_url}/api/v2/FaceDetection/results" + + async with httpx.AsyncClient() as client: + response = await client.post( + callback_url, + json=payload.model_dump(), + headers={"X-API-Key": settings.lychee_api_key}, + timeout=30.0, + ) + response.raise_for_status() + callback_resp = DetectCallbackResponse.model_validate(response.json()) + + # --- 4. Persist embeddings now that we have stable lychee_face_ids --- + id_to_vector: dict[str, list[float]] = {eid: vec for eid, vec, _ in face_data} + for mapping in callback_resp.faces: + vec = id_to_vector.get(mapping.embedding_id) + if vec is not None: + store.add(mapping.lychee_face_id, vec) + + except Exception: + logger.exception("Detection job failed for photo_id=%s; sending error callback", photo_id) + await _send_error_callback(photo_id, "internal_error", "Detection pipeline failed", settings) + + +async def _send_error_callback(photo_id: str, error_code: str, message: str, settings: AppSettings) -> None: + """Best-effort POST of an error callback to Lychee.""" + payload = ErrorCallbackPayload(photo_id=photo_id, error_code=error_code, message=message) + callback_url = f"{settings.lychee_api_url}/api/v2/FaceDetection/results" + try: + async with httpx.AsyncClient() as client: + await client.post( + callback_url, + json=payload.model_dump(), + headers={"X-API-Key": settings.lychee_api_key}, + timeout=10.0, + ) + except Exception: + logger.exception("Failed to send error callback for photo_id=%s", photo_id) diff --git a/ai-vision-service/app/api/schemas.py b/ai-vision-service/app/api/schemas.py new file mode 100644 index 00000000000..b00c9166167 --- /dev/null +++ b/ai-vision-service/app/api/schemas.py @@ -0,0 +1,160 @@ +"""Pydantic request/response schemas for the AI Vision Service API. + +These models define the contract between the Python service and Lychee (PHP). +All fields are fully type-annotated; no ``Any`` types are used. +""" + +from pydantic import BaseModel, Field + +# --------------------------------------------------------------------------- +# /detect - Lychee -> Python (request) & Python -> Lychee (callback) +# --------------------------------------------------------------------------- + + +class DetectRequest(BaseModel): + """Body sent by Lychee when dispatching a face-detection job. + + ``callback_url`` is intentionally absent: the service reads + ``VISION_FACE_LYCHEE_API_URL`` from env and posts results there directly. + """ + + photo_id: str + """Lychee-internal photo identifier (string PK).""" + + photo_path: str + """Absolute filesystem path on the shared Docker volume. + + The service validates that this path starts with ``VISION_FACE_PHOTOS_PATH`` + before opening the file (path-traversal protection). + """ + + +class SuggestionResult(BaseModel): + """A single similar face returned as a suggestion.""" + + lychee_face_id: str + """The Lychee ``Face.id`` of the suggested similar face. + + Stored as ``face_suggestions.suggested_face_id`` in the PHP layer; + Lychee JOINs to resolve the linked person at read time. + """ + + confidence: float = Field(ge=0.0, le=1.0) + """Cosine-similarity score (0.0-1.0) between the new embedding and this suggestion.""" + + +class FaceResult(BaseModel): + """One detected face included in the callback payload.""" + + x: float = Field(ge=0.0, le=1.0) + """Left edge of the bounding box as a fraction of image width.""" + + y: float = Field(ge=0.0, le=1.0) + """Top edge of the bounding box as a fraction of image height.""" + + width: float = Field(ge=0.0, le=1.0) + """Bounding-box width as a fraction of image width.""" + + height: float = Field(ge=0.0, le=1.0) + """Bounding-box height as a fraction of image height.""" + + confidence: float = Field(ge=0.0, le=1.0) + """Detection confidence score from the face detector.""" + + embedding_id: str + """Transient identifier generated by the service for this embedding. + + Echoed back in ``DetectCallbackResponse.faces`` so the service can update + its embedding store with the ``lychee_face_id`` assigned by Lychee. + This value is *not* persisted on the Lychee ``Face`` model. + """ + + crop: str + """Base64-encoded 150x150 JPEG face crop. + + Lychee stores it at ``uploads/faces/{token[0:2]}/{token[2:4]}/{token}.jpg`` + where ``token`` is a random high-entropy string generated by Lychee. + Only the top ``max_faces_per_photo`` faces (by confidence) are included. + """ + + suggestions: list[SuggestionResult] = [] + """Pre-computed similar faces from the embedding store (may be empty).""" + + +class DetectCallbackPayload(BaseModel): + """Payload POSTed by the service to Lychee's results endpoint on success.""" + + photo_id: str + status: str = "success" + faces: list[FaceResult] + + +class ErrorCallbackPayload(BaseModel): + """Payload POSTed by the service to Lychee's results endpoint on failure.""" + + photo_id: str + status: str = "error" + error_code: str + """Machine-readable error code (e.g. ``"corrupt_file"``, ``"no_faces"``, ``"oom"``).""" + message: str + """Human-readable description of the failure.""" + + +class FaceMapping(BaseModel): + """Mapping returned by Lychee after processing a successful callback. + + The service uses these to update its embedding store with the stable + Lychee-assigned ``lychee_face_id`` for each embedding. + """ + + embedding_id: str + """Echoed from ``FaceResult.embedding_id``.""" + + lychee_face_id: str + """Stable Lychee ``Face.id`` assigned to this embedding.""" + + +class DetectCallbackResponse(BaseModel): + """200 response body returned by Lychee after a successful callback.""" + + faces: list[FaceMapping] + + +# --------------------------------------------------------------------------- +# /match - Lychee -> Python (multipart file) & Python -> Lychee (response) +# --------------------------------------------------------------------------- + + +class MatchResult(BaseModel): + """A single candidate face returned from a selfie-match query.""" + + lychee_face_id: str + """Stable Lychee ``Face.id``; resolved to ``person_id`` by Lychee.""" + + confidence: float = Field(ge=0.0, le=1.0) + """Cosine-similarity score between the selfie embedding and this face.""" + + +class MatchResponse(BaseModel): + """Response body for ``POST /match``.""" + + matches: list[MatchResult] + """Top-N matches above ``VISION_FACE_MATCH_THRESHOLD``, ordered by descending confidence.""" + + +# --------------------------------------------------------------------------- +# /health +# --------------------------------------------------------------------------- + + +class HealthResponse(BaseModel): + """Response body for ``GET /health``.""" + + status: str + """``"ok"`` when fully operational; ``"degraded"`` if the model is not loaded.""" + + model_loaded: bool + """Whether the InsightFace model pack has been successfully initialised.""" + + embedding_count: int + """Total number of face embeddings currently stored.""" diff --git a/ai-vision-service/app/clustering/__init__.py b/ai-vision-service/app/clustering/__init__.py new file mode 100644 index 00000000000..089731a0f84 --- /dev/null +++ b/ai-vision-service/app/clustering/__init__.py @@ -0,0 +1 @@ +"""Face embedding clustering (DBSCAN).""" diff --git a/ai-vision-service/app/clustering/__pycache__/__init__.cpython-313.pyc b/ai-vision-service/app/clustering/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e573e9c3c6b49ea3c42bfc9bbbf28e694d952245 GIT binary patch literal 227 zcmey&%ge<81j}dc%hUqWk3k$5V1zP0a{w7r8G;##7}6OvnW~iC5|dLEQgf41Q&KYX z(iM_(N{dTUi$F{b7pGunM?XzHKTXD4?D6p_`N{F|D;Yk6Ou7}IpOK%Ns-KiurtgxU zT$-DjS5mAWP?VpQnha$4R3>Mnrs^kV>Xu~|XXfYW7N-`KWhMhf3JUZQR_MpaXXa&= p#K-FuRNmsS$<0qG%}KQ@Vgp(La&Ixntsj^f85wUeXcn;mIRN4QKwJO- literal 0 HcmV?d00001 diff --git a/ai-vision-service/app/clustering/__pycache__/clusterer.cpython-313.pyc b/ai-vision-service/app/clustering/__pycache__/clusterer.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e30b78c545adbc961dede35b17ee2e9ce7a84f77 GIT binary patch literal 3367 zcmaJ@-)|g89iO|sUp~h_YR9xm95M+3d{OQ$wy_{?<2H3jn>4jrZyZG!Yj1CN?mYJ1 z>}F=yuKUy{umlgZB5GV7lJb=B7gR#3KtclXRFk5bg_o*DC=w4XhE{#!``z91ogKtT zH#;};ozM68_dC0NeMJJ}(B1d>`>^^1|FlOYUZi%xT@|nbCpm0_eU*&$;VWt%H z()0T4#koqc_=0o;Pf_K&lG(Je4?|Hde7B9Hs2TWf%k;TrI?l#fy3YM&H}I)sw*rog zivBXAA^;I2bsL>~fKg{udCcMs4qUjProCngi&g0NO>4PLNiA1OraV{JstA`%>hM+O zY*3pkp9{;U+-Ab({>IZ(xLh&nxgepUV~$y84z1P7hLd{vR0pnSVKW+Q}kjNDHAg!mQrl(~_&+7$UJ4#kED=C@Pd(a0f8JW|w zGT+RVieVobe>S!_laKlU&in_sDA`hv85V8VaYF13I(CT(1=^LLHx-yk_drzBp$Q23 z5UhI9^BnL%q(tNbI)|nxQLc*X%yG&p;6A$2l`+>QdKfR)nPmowO&81b`u}#6?J@O0r3El%Odw%ys+?o_P?0ew+6 z)ifYe$~-kqUp8Okt)SJ0I)kBvSZC}AKp*r(X&{_@Bfhrem?o+k~x|yJ{XgZi5 z{8qq{2w{FlQx5s$u7mC4Xr^P`eycMKq{yuf>K;X2rD!Gj6J-%rCitJ)9fieha&yp$ zwsE>i9Vk@sXhKHLJxGTc1N;;oGP-P0(!z{lP9xOtsJH7xAtP`{_-b|8ZLw;duT?L( zR?vd7psKG(cZFH7xVB*}GgdWud5tT`_OfDfja#tedDTQQP^7(Nvf^!o1;YRj_=XXF zZC^H5+WQoED@^S-$j;#>f7-J%`t3g#j_kq)v66C(Yf^w;WPpEyvbV`XYAo6�IJ= zIUSdFay2E=6i$c94edroX;q@q!{o(0Apy{N-&xRJNx$4fNH3v8WQ8U&i%FXxWImTb zc70C8SeYBS$FMFh)#HeqZa}IMYW|PD1~Cm zTAsfVDbQ{J2h0c9pm{`ADN4*q==!<1yTU4K`x_p6gpqK)1eFI`g9;0)jCo)t)r=kf z*|T9T?k6Qp9)dlIaUSO4UW0fFn~q=V4+qq0^qQ(9YRX}xObai}no^n@VLmeX`K{o;1Fz37Iqc9WA+Uwja^-9PI5)K?tH|D1` zxE-d9P~*0OirK1%`8BA5u2f+b*{LvX*Fge&A}NJHP(a&5Vrqu@M1gSRa`!u?t=DOw zQGbN#4f4e(89cEwJhFM^?}alTpFH^%M2(iA`<0fBbJlhu#>o9Ug=ug5?SM<+iTo!%auemMHfX71C0;h)dm(e7W`K0N+#VB&+q#8w z_Jo-^SFln>ehY|SKaT!d^aFS|EJBE!jRJKsy1KNqBqxz(g z4WSjZybXyxK1O^RS9@H-!ZWcJGjawFFy$1|ex{{Tsn65dROUbZB=wC?$k3O$@1-XH JMPQ0<{}-G None: + self._eps = eps + self._min_samples = min_samples + + def cluster(self, face_embeddings: list[tuple[str, list[float]]]) -> list[tuple[str, int]]: + """Cluster the supplied embeddings. + + Args: + face_embeddings: List of ``(lychee_face_id, embedding)`` pairs. + Each embedding is a 512-dimensional float list. + + Returns: + List of ``(lychee_face_id, cluster_label)`` pairs in the same + order as the input. ``cluster_label == -1`` means the face was + classified as noise. + + Returns an empty list when ``face_embeddings`` is empty. + """ + if not face_embeddings: + return [] + + from sklearn.cluster import DBSCAN + + ids = [fid for fid, _ in face_embeddings] + vectors = np.array([emb for _, emb in face_embeddings], dtype=np.float32) + + # Normalise vectors to unit length so that Euclidean distance equals + # sqrt(2 * (1 - cosine_similarity)). DBSCAN's cosine metric directly + # uses (1 - cosine_similarity) as the distance measure. + norms: np.ndarray = np.linalg.norm(vectors, axis=1, keepdims=True) + # Avoid division by zero for zero-norm vectors + norms = np.where(norms == 0, 1.0, norms) + vectors = vectors / norms + + db = DBSCAN(eps=self._eps, min_samples=self._min_samples, metric="cosine") + labels: list[int] = db.fit_predict(vectors).tolist() + + return list(zip(ids, labels, strict=True)) diff --git a/ai-vision-service/app/config.py b/ai-vision-service/app/config.py new file mode 100644 index 00000000000..620754f90b5 --- /dev/null +++ b/ai-vision-service/app/config.py @@ -0,0 +1,104 @@ +"""Application configuration via Pydantic BaseSettings. + +All settings are loaded from environment variables prefixed with ``VISION_FACE_``. +Example: the ``api_key`` field maps to the ``VISION_FACE_API_KEY`` env var. +""" + +from functools import lru_cache + +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class AppSettings(BaseSettings): + """Runtime configuration for the AI Vision Service. + + All values are read from environment variables prefixed ``VISION_FACE_``. + """ + + # --- Required --- + lychee_api_url: str + """Lychee instance base URL for callbacks (e.g. ``http://lychee``). No trailing slash.""" + + api_key: str + """API key this service validates on *inbound* requests from Lychee (``X-API-Key`` header).""" + + lychee_api_key: str + """API key sent as ``X-API-Key`` on *outbound* callbacks to Lychee.""" + + # --- Model --- + model_name: str = "buffalo_l" + """InsightFace model pack name. ``buffalo_l`` = large/high-accuracy; ``buffalo_s`` = faster alternative.""" + + # --- Detection thresholds --- + detection_threshold: float = 0.5 + """Bounding-box confidence filter — faces below this score are excluded from the callback payload.""" + + match_threshold: float = 0.5 + """Cosine-similarity cutoff for selfie match results and suggestion candidates.""" + + rescan_iou_threshold: float = 0.5 + """IoU threshold for bounding-box matching on re-scan (preserves ``person_id``).""" + + max_faces_per_photo: int = 10 + """Maximum faces included in a callback payload (top-N by confidence; rest dropped).""" + + # --- Concurrency --- + thread_pool_size: int = 1 + """Number of threads in the ``ThreadPoolExecutor`` used for CPU-bound inference.""" + + workers: int = 1 + """Number of Uvicorn worker processes.""" + + # --- Embedding storage --- + storage_backend: str = "sqlite" + """Embedding storage engine: ``sqlite`` or ``pgvector``.""" + + storage_path: str = "/data/embeddings" + """SQLite DB directory (used when ``storage_backend = "sqlite"``).""" + + # --- PostgreSQL (pgvector) --- + pg_host: str = "localhost" + """PostgreSQL host (only when ``storage_backend = "pgvector"``).""" + + pg_port: int = 5432 + """PostgreSQL port.""" + + pg_database: str = "ai_vision" + """PostgreSQL database name.""" + + pg_user: str = "ai_vision" + """PostgreSQL username.""" + + pg_password: str = "" + """PostgreSQL password.""" + + # --- Photo volume --- + photos_path: str = "/data/photos" + """Shared Docker-volume mount point for photo files. + + ``photo_path`` values from Lychee are validated to reside within this prefix + (path-traversal protection). + """ + + # --- Logging --- + log_level: str = "info" + """Uvicorn/application log level.""" + + model_config = SettingsConfigDict( + env_prefix="VISION_FACE_", + # Support .env files in development but never require them in production. + env_file=".env", + env_file_encoding="utf-8", + case_sensitive=False, + ) + + +@lru_cache +def get_settings() -> AppSettings: + """Return a cached ``AppSettings`` instance. + + Call this function via ``Depends(get_settings)`` in FastAPI route handlers. + Override ``app.dependency_overrides[get_settings]`` in tests to inject + mock settings without touching environment variables. + """ + return AppSettings() # type: ignore[call-arg] diff --git a/ai-vision-service/app/detection/__init__.py b/ai-vision-service/app/detection/__init__.py new file mode 100644 index 00000000000..9dabc000d3e --- /dev/null +++ b/ai-vision-service/app/detection/__init__.py @@ -0,0 +1 @@ +"""Face detection and crop generation.""" diff --git a/ai-vision-service/app/detection/__pycache__/__init__.cpython-313.pyc b/ai-vision-service/app/detection/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..134e2a6a5a6e3a6bf368f657000a766adf339e20 GIT binary patch literal 226 zcmX|*K?=e!6hsphEkf@Qa8;qr1;mxO5*0i^sOhJT*rX(Bq29xV7ZALJ2kF)e2)%#_ zsI&Pqvv_~{Fq|T%`|T;_?!J8Z34cu=ICDh@q*0Ee!DY5iIgku2a4VD~smuu1svDyz@1)MLl@bB&++9%76;6zP!*A1eJo@Bjb+ literal 0 HcmV?d00001 diff --git a/ai-vision-service/app/detection/__pycache__/cropper.cpython-313.pyc b/ai-vision-service/app/detection/__pycache__/cropper.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..720dfb422aee4d35050f816167c0d5e8dca2346a GIT binary patch literal 4496 zcmeHL&2Jmm5nq0byQIG5BzBx`Wjm1^QIurKk!2-@zARHxB!L^Es%T)bB%i1aNG|(! zDTz5ThoV+uz)s>e4xF|Dnu7!MV4ykYl7GMo4-#GzKtR$KJ~gP5duMjJ{3;FSSQLGL zX6DVjdHX(Q=J%+_!645;`{y6NP~R6g?py5G&1DIW$2URvEq9KSILb*5$+_%Ua#E*= zvTNBzT_$%^H*oi|XUR*wrp>eLTWX_iB6r<8P?=Nla_Hq<_Al|2hfzK%NNv{xlK&+b z@d79DG$;ws4^;XcQt%}Arkl1)ol?6gb);Q`U2hpzl_U|9RLhHLk|R_xR4peKbTyY2 z*VRl$+a2clb*iNbNurCR6C>iDNtf0a7EL#ONK7a?xi}soxuljNsUeXPB~=s+O(fX_ zNr`GsT$0vS#k^9?Xi7>S<{#lM1|3XLayiW~KdG-mKlsKk*B9iuYYTJBk<~>|cp_OP zO+aw3E5`Pt-+>!*sX2rB=b~%tvJ}}|NMA79msgl)3#K<%hrB*BKM(%$)tR|B)}l!d zey!5lfWvh-9v?aJD&n@Di$cG-^>Z<9!eMEn^|Mq4N)Fqa^Ta$+xRT~pfLZhDjT0g8 z&b4j4WODz;MU(RzizXLhd>q$hHd7Ob2~C<{9D>Sfl5^u`2!d^*vf^BOgYEYXwvKqv z_KSDeQp{_wd*huDC}RPn)%iMG`uM6o_e`H)gHIRizssJ##^XJ<9Ba4z>vSD0be%19 zUAAuAZA-cCSa-C~p1sE7Cv3SUjXUe^`B~duYbkD27St} zA#4||d?upOl%dUOyk(aFNc9<-)~9$8nu@6>=OOQ=#F>PyWeNs?95%KwvyE*c>RN$9 z?J&m;n=X4(;;KfoN=DTo?=xh}5J@UcL~YA-NN9yz3JXg@+Y<*yhDSo9!y|*1Yq7!A zfO7OxJ$Ka#QHTpYKGIBQE2V@Jspz?dRf)(3kTUyBIl*kcY@ zuX(u4YaJI<=4_gIfXKTR+lCz_ba~HWxIgHD4H?+-N(ybrip;q;%A7^`+ZzKuMlmOu zD^b|edqKj0Y;J5dysc$PIHB%@=e1-ZOLB%D2CRCMBtcjyCbtO*D{5#*)dAE(I-xsi z5;XaII7JK?1llm3U7pb4e31n!2uNh}Krg{}v0CUMNTw3^!%0r)ymkJa^S3w4ym-hD z9Pk78`^tQ{GmacdoNV>i>2AGn|n87wu9fD$;{&+UcZXna?zZ1V-j3Wz-hJ_Ye<^a{4t*_j9SWzu5>DNn`{T+- zEB98*!pn!k*nu$i;FYp)`JwyrV>mUgHSQw%J{@=CZkz#ID^#t=>Y#l}&(ra_6}uIZkH&(GPj*Lb{&wKb^$pQ~tF^J#^<5158Awh!YgU-9%&)yi7o*r%;EriDF>W(?~=l z_)?_!9HeKE;Byc#Rj%s=`HkSF3Qodhd;wgC)f3{Kfv)~jH?f)k82;^Lj?hnw$jej=3nhRS^C zke@ioSBBb|b?8w}8Mz$pG^wC)+v$+~P)GH=|J%xgyp zA9wf$WiEJK!UWT(^VEgP)2E=X;aBkT20NKgmQz|%mKiV0TLqYz$T9_ZuEZwBI`bOE zJd_%WHI}*7BP-U^)Q7(VGHSwFKZZB_?3U|(&BILiQlpytUI;MYIb@V%?&F}mh1SGv$ literal 0 HcmV?d00001 diff --git a/ai-vision-service/app/detection/__pycache__/detector.cpython-313.pyc b/ai-vision-service/app/detection/__pycache__/detector.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5ede44a9fc2549e0d77369f4a73882713f9ee28e GIT binary patch literal 6952 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 str: + """Generate a base64-encoded 150 x 150 JPEG face crop. + + Args: + image_path: Absolute path to the source image. + x: Normalised left edge of the bounding box (0.0-1.0). + y: Normalised top edge of the bounding box (0.0-1.0). + width: Normalised bounding-box width (0.0-1.0). + height: Normalised bounding-box height (0.0-1.0). + + Returns: + Base64-encoded JPEG bytes (ASCII string). + """ + img = Image.open(image_path).convert("RGB") + img_w, img_h = img.size + + # Absolute pixel coordinates + abs_x = x * img_w + abs_y = y * img_h + abs_w = width * img_w + abs_h = height * img_h + + # Add padding + pad_x = abs_w * _PADDING_FACTOR + pad_y = abs_h * _PADDING_FACTOR + + x1 = max(0.0, abs_x - pad_x) + y1 = max(0.0, abs_y - pad_y) + x2 = min(float(img_w), abs_x + abs_w + pad_x) + y2 = min(float(img_h), abs_y + abs_h + pad_y) + + crop = img.crop((int(x1), int(y1), int(x2), int(y2))) + crop = crop.resize((CROP_SIZE, CROP_SIZE), Image.Resampling.LANCZOS) + + buf = io.BytesIO() + crop.save(buf, format="JPEG", quality=85) + return base64.b64encode(buf.getvalue()).decode("ascii") + + +def generate_crop_from_bytes(image_bytes: bytes, x: float, y: float, width: float, height: float) -> str: + """Generate a crop from raw image bytes (used in testing / matching). + + Args: + image_bytes: Raw image file bytes. + x: Normalised left edge of the bounding box (0.0-1.0). + y: Normalised top edge of the bounding box (0.0-1.0). + width: Normalised bounding-box width (0.0-1.0). + height: Normalised bounding-box height (0.0-1.0). + + Returns: + Base64-encoded JPEG bytes (ASCII string). + """ + img = Image.open(io.BytesIO(image_bytes)).convert("RGB") + img_w, img_h = img.size + + abs_x = x * img_w + abs_y = y * img_h + abs_w = width * img_w + abs_h = height * img_h + + pad_x = abs_w * _PADDING_FACTOR + pad_y = abs_h * _PADDING_FACTOR + + x1 = max(0.0, abs_x - pad_x) + y1 = max(0.0, abs_y - pad_y) + x2 = min(float(img_w), abs_x + abs_w + pad_x) + y2 = min(float(img_h), abs_y + abs_h + pad_y) + + crop = img.crop((int(x1), int(y1), int(x2), int(y2))) + crop = crop.resize((CROP_SIZE, CROP_SIZE), Image.Resampling.LANCZOS) + + buf = io.BytesIO() + crop.save(buf, format="JPEG", quality=85) + return base64.b64encode(buf.getvalue()).decode("ascii") diff --git a/ai-vision-service/app/detection/detector.py b/ai-vision-service/app/detection/detector.py new file mode 100644 index 00000000000..b52ec889b9a --- /dev/null +++ b/ai-vision-service/app/detection/detector.py @@ -0,0 +1,170 @@ +"""InsightFace wrapper for face detection and embedding generation.""" + +from __future__ import annotations + +import threading +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from pathlib import Path + + +@dataclass +class DetectedFace: + """Raw face detection result, including the ArcFace embedding vector.""" + + x: float + """Left edge of bounding box as a fraction of image width (0.0-1.0).""" + + y: float + """Top edge of bounding box as a fraction of image height (0.0-1.0).""" + + width: float + """Bounding-box width as a fraction of image width (0.0-1.0).""" + + height: float + """Bounding-box height as a fraction of image height (0.0-1.0).""" + + confidence: float + """Detection confidence score from RetinaFace (0.0-1.0).""" + + embedding: list[float] = field(default_factory=list) + """512-dimensional ArcFace embedding vector.""" + + +class FaceDetector: + """Thread-safe wrapper around InsightFace FaceAnalysis. + + Uses the ONNX Runtime CPU execution provider so no GPU is required. + The model is loaded once at startup (via :meth:`load`) and shared across + threads; a lock guards the non-thread-safe ``app.get()`` call. + """ + + def __init__( + self, + model_name: str = "buffalo_l", + detection_threshold: float = 0.5, + model_root: str = "/root/.insightface", + ) -> None: + self._model_name = model_name + self._detection_threshold = detection_threshold + self._model_root = model_root + self._app: Any = None # insightface.app.FaceAnalysis - untyped library + self._lock = threading.Lock() + + # ------------------------------------------------------------------ + # Lifecycle + # ------------------------------------------------------------------ + + def load(self) -> None: + """Load the InsightFace model pack into memory. + + Idempotent - safe to call more than once. + Must be called before :meth:`detect` or :meth:`detect_bytes`. + """ + with self._lock: + if self._app is not None: + return + from insightface.app import FaceAnalysis + + app = FaceAnalysis( + name=self._model_name, + root=self._model_root, + providers=["CPUExecutionProvider"], + ) + app.prepare(ctx_id=-1) + self._app = app + + @property + def is_loaded(self) -> bool: + """Return ``True`` if the model has been successfully loaded.""" + return self._app is not None + + # ------------------------------------------------------------------ + # Detection + # ------------------------------------------------------------------ + + def detect(self, image_path: Path) -> list[DetectedFace]: + """Detect faces in an image file. + + Args: + image_path: Absolute path to the image file. + + Returns: + Detected faces sorted by descending confidence, with normalised + bounding box coordinates (0.0-1.0) and 512-dim embeddings. + + Raises: + RuntimeError: If :meth:`load` has not been called. + ValueError: If the file cannot be decoded as an image. + """ + import cv2 + + img: Any = cv2.imread(str(image_path)) + if img is None: + raise ValueError(f"Cannot read image: {image_path}") + return self._detect_array(img) + + def detect_bytes(self, image_bytes: bytes) -> list[DetectedFace]: + """Detect faces from raw image bytes. + + Useful when the caller already has the image in memory (e.g. for + selfie matching so that no temporary file needs to be written). + + Args: + image_bytes: Raw bytes of any image format supported by OpenCV. + + Returns: + Detected faces sorted by descending confidence. + + Raises: + RuntimeError: If :meth:`load` has not been called. + ValueError: If the bytes cannot be decoded as an image. + """ + import cv2 + import numpy as np + + nparr: Any = np.frombuffer(image_bytes, np.uint8) + img: Any = cv2.imdecode(nparr, cv2.IMREAD_COLOR) + if img is None: + raise ValueError("Cannot decode image bytes") + return self._detect_array(img) + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _detect_array(self, img: Any) -> list[DetectedFace]: + """Run detection on a BGR numpy array (internal).""" + if self._app is None: + raise RuntimeError("FaceDetector not loaded - call load() first.") + + h: int = int(img.shape[0]) + w: int = int(img.shape[1]) + + with self._lock: + raw_faces: list[Any] = self._app.get(img) + + results: list[DetectedFace] = [] + for face in raw_faces: + score: float = float(face.det_score) + if score < self._detection_threshold: + continue + + bbox: Any = face.bbox # [x1, y1, x2, y2] absolute pixels + x1, y1, x2, y2 = float(bbox[0]), float(bbox[1]), float(bbox[2]), float(bbox[3]) + + # Normalise to [0, 1] and clamp + fx = max(0.0, min(1.0, x1 / w)) + fy = max(0.0, min(1.0, y1 / h)) + fw = max(0.0, min(1.0, (x2 - x1) / w)) + fh = max(0.0, min(1.0, (y2 - y1) / h)) + + embedding: list[float] = [float(v) for v in face.embedding] + + results.append(DetectedFace(x=fx, y=fy, width=fw, height=fh, confidence=score, embedding=embedding)) + + # Descending confidence order + results.sort(key=lambda f: f.confidence, reverse=True) + return results diff --git a/ai-vision-service/app/embeddings/__init__.py b/ai-vision-service/app/embeddings/__init__.py new file mode 100644 index 00000000000..f174c5f4101 --- /dev/null +++ b/ai-vision-service/app/embeddings/__init__.py @@ -0,0 +1 @@ +"""Embedding storage layer.""" diff --git a/ai-vision-service/app/embeddings/__pycache__/__init__.cpython-313.pyc b/ai-vision-service/app/embeddings/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8f53ab5196bd682c31c227417c93d2386ee3c0d6 GIT binary patch literal 216 zcmey&%ge<81j}dc%ajMwk3k$5V1zP0a{w7r8G;##7}6OvnW`jQbCXh2QZn<>6^cvp zixSgQ6><_QQ;YQcG#PKP$H%ASC&$OHWcUoyek(vfBR@A)KPj_J-z7h}G&eP`q*y literal 0 HcmV?d00001 diff --git a/ai-vision-service/app/embeddings/__pycache__/sqlite_store.cpython-313.pyc b/ai-vision-service/app/embeddings/__pycache__/sqlite_store.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d5552f4bcb23979d6657c319362432acbdcece38 GIT binary patch literal 7847 zcmbtZTWlLwdOkD5At{QwQIss%mUAS>3294|FH!3F5{0rPN47-89CGVeF-9YDBuz|_ z%FIxpUS zhSzzDZQ-n-3+(CAUF<39B0ODZ-Df?D$RfVZjZ zzBwW6Q~sVKL^?OFnJPU%jn6XB=vz`#R9%=+(`hX`M-4NlD|0HHQBs%GY&zoeB@NY} z=DfOP{uNWr8d@$(XLCAL78mv06>UKQvRPHpRm1Gds@mNAOfIj}P&5_`Qv>=_^S<1Y zs?&i(hx^jnf=xlm(Ai8*F)5q__BJ9kF|QdE{^6c!bymq|Oy@Yg)W~b5GLun#xUm5z z&ZvEyEKp9Xi<#V2pllj&@&rz#8Rcp&Zyxhe+(DjIQt&w;Xda|Xnr`M{e?~A& z``E|BsIFYzdZy!4%cj*UYMRbmrF!nN2KPcKl9CrxQ=uVUoy%tw-JwLWl*whTrsh?Z z?JR3)+V?&?2qQxm;O^ifBEBDyPhrlgSF+ihshDuLMhxCvA1UU%Mu3ee;9a?h&=LsY zo_z?7TV$#{5HB&j+yRXqGKKS5Ns@sQokoDx5x$>Gl5^Z&xZfpjk}zL5A*s5iWHdvi zO6d;Z#iV8q-T^KqTsOc%Q-JfxwIhe&N`UP%ncPgo;%38wlYG2%qcEvlK#75> zdyl8p2<0WAubz}~nk1R>$|%a>Fi|kdh)m00O5oZ|1%@~F18rf_lf%Y4jM=g7o-5cJKIsd{cXdOwmrAyujNN=Z^4^y!|PvdSZTQ4q&EPm zRGI(q;(`D0&OKWKoZTY8UFg_ectUZcdR=LX!}qMP2WOP^Vuix_Lypk|8bTcR+g2oQ zs?O<3@(#xPIcHtgm2}m@XI6X#J^uW;e%@KlcE!)$R-BWw@z)|6w2y#)6^e60H-5`& zSIv@>!W(3k>n6P>)KD`>|pcPh8ApqZAK%H#|+?ACYT zgvPBq>Zoii7m|ksdELnAR$cjg2C5nm6ezZ=2ueC_b<`9S7&!v7jayJ%BTpMzRxf;Y zVWV}&?UOf8{(NwGxX3p)bUkeje%HKjy?Nig^yB8^8=YNucmHDdMsV~=Th|@!QCny& z7=9)SI|Db|MK9?Ieb;?xz5CFOx{c=GZU0UGMli9_wr4HazbOhW0hrO!cDw#&{WlFj z_O|b)@0S00H)-rxzVL@4#WB|Pg7sa$@WR*&C%fJt^z{{T@ll&`Xvb%z1up~8PvPwGmcv(8(T%}SO2kAhoQDX{278V(D^=>$%j+#^7ht*q9 zg$0I0{cY?(aonno-8R6jT{Tf=51z(VNvLWA&DGEah^Yn|;A0z58cIO%R{;h45GZ)w z>n+iK;cjoRQ#lBa)ILL7&>%F?%&Vms41B;*)r645p!r2~?5C$QFvq0(>A>_f zO)22sgAW74vwCg;Rro;~LjOOL$w6F{(|zbz81uPI8eJUFso-LS0qbdS6V1GyH7G=L z3pt1yN|b>rEPyZ$V>-q3saH}exIoj>WkS=_5CNerF8puVQ(_7gDrsbVm#ZRzxuS;N*Y>YJuBZOy)iWz+u8;p=dDw2eFhJDF*Ur9xyok3p5~`hnYN*<)-k9S&3I0#8O`mh9YPjw-s>WUGMQM zc&@=lNpzSECWNbHkSf6m%&6($K#)*^E6#uvkh}2CB&ZdtKql|96VjIaYRvat*39|p zOduqI(rP-wI2s#;Oiv3~meeKLfY4U9BhWe1*c;wxYF&oJ4A9%62g<(9 zVYtvg!w0rC1j1-rfvB?#5y;)~N2*!J)zTVUnEOYhyf%Owy+a|FCAsjg;)l<4 zQ;U9CpV3gwrMSG}QeZ?pavEHKWNhqQa)cc;mK-1NedXG%4i8O?oKMh?;$suh_{a3@ z$j4<0H3@`k_QoK>#bCKg^)|{Q6GJ1z!*E0L@YqCE2*TU`1dBdUf0b0Pz7yK761P%n z={ig?E<;s2_JAPQiB{WFkJI{E%2+e(~@L(AE6^3}bs> zZwO<(!rf!hBm9HTW@tV*5*QN2hoS)OhhE=MyJt&gsjRy1!CH`DE?&cuOI#9i=8hE9 zvg8i>vqDms*-u_Y2Sz0qZdswm)Of8u#p*pFUghU_P}E0&#ef{tB8kr+ua?6O9{Iw*t28cDD)a=J5j2b3&0{~YhO(VV0!}Sm$>>o zRDj3Ez-neCv*91zXxO#p-wmmhCf{>6sc&5geYL;nA$5L8n)sVm1~-^x` zBj0}Z;N5RSkNs!Mon!wVTRS`TTmL6Tk@$Bpg8*NEri@8g3_bxf^m-dqmfH@EELRMC z9FRd4*5e3EExoA6A6cfDUw=_YKUv4DCQzu%tMqa|qK6L=0zPyJE5w|Ri6#AAtUkaB z7Zi3d!e)LkqfSA}tJl_d7cas|89|&`YQG4S>~iFkokjz5?@VK-aWl)SCy{uqb2Va; z7*pD^Z24q)A(zgBIhQTJEPn<+vz11gWO){n%Vq|G(`*igJ+hq6rDR#(gM<%Y6~XER zRzp~IV}(k`=6IV7Kls3J6)9OUOT?5WSne zda+}(Cm?ohhP+}|aW`r2DAw&2JD;}hDT?p}-4pCAda&gs&8=*#=p(-F-`7Lm<{8cd z^-ixC+zbMd=Ro8lXK#$#0mmv#!3(xT{CUg>*XtMX4X$N24>2-ai;mu`ei|zeR`_ET z=KRh1j5fpa`uYU+W5SYsijJRA`9{lo!oK16^=TMDwP7Ihivq`SKk;>(>-X)1+xr7) m{GK#?Pn!RWbUbYdUTfSSkxe(xz4zQV!E+b6{~_$b=>8u{AldK$ literal 0 HcmV?d00001 diff --git a/ai-vision-service/app/embeddings/__pycache__/store.cpython-313.pyc b/ai-vision-service/app/embeddings/__pycache__/store.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c6820411d36dbf4f7b948796042fd3efb5629bd6 GIT binary patch literal 2474 zcmbVNJCEB&5MD~8PU7j#kMk=|a@oWwPQZz>4Y)ZZ0SufC_;qp26aj35qR5p*nCgPB6%%8&jgwc*Wx!m+t=utgSD?aaUcFPjZGw5*0*kXp)&=$%bIps) zwH}$*s>SJRr8lSzYOSTcU#|D00zXjv#;7O4FiHllNYhmrLCC z2ZHzf;HgML$w!$~JV=v%3L~yku7;81QKAI&{XlR@E55=(*#3ANM}e=RG{F<0;My`2 zV{{}E#nV2Sk+T4V!n5UFA{j&pt`Acw635=B99l-c>?cX8v^&|VM-V+0s+!eQ`?dt9lGzRt7YQB0D&;2G|wfDn3LXKc@4bOiAi^$&^jbkxFWQrUM zsCxoI0^x^k>GuWyekc%3KaNGn`;d+_dKm!W3F28Mq_T&862=0gV+UpK8V3VM{BO)m zjl;lbRrdb8k9H9<>L&-JZ}O*Ng8q9G9x2K1P7qhYZxX$?wkgiOHcu2xbY|UdtW7*h z(Tl=vm-{h=JmKOwI+vU1hsXypj?p9}mBB!6*&LsvXScYkh{5@--7ekhXr~*fT~sW; zf2ZZ>qYE^*__Zrn+aYQok*GF5=GP(60r%q+)wnAH)RtrK{LEU_+>|2j=c`9A&KnDF zl2F6Q@_U_OIue~;wABx~V} zCwbKm!~FD(d2Q*+b8zhpUMI2q*$Xy3%^J-aPFYZ)v3%Y~-OC4Jl_ zEPq1gW#%U_b2xoHEq*UbqESW_Cb1p0%Nrs!;WD6}xpCRK+`i&mF2xvQ1PV|2 z&-JM!L!N2|51=p?&c?ZL-vq9lm3kZQAJ)GQcXK_>FX z;zVxgJSA~Ncoi(4I=I-A!iQj(AN#^M|FUSIhdKA3FSsYZ=g)h%-)kQtUg6gu<_AFQ zLJJRwl}?$8p;M9MuMF4-w^mq}KROtETmxSFsNP;qv!# zjMP2UJ?3#*v~k*MzFu3OTKJyU*qINeOEjyqlWTf!YBT%Ocgwi; z*G9!Ex`1}P(^`TJ^h=5LGej2?1M1FsO-;tAcpx1DkyS) EmbeddingStore: + """Return a configured :class:`EmbeddingStore` for the given settings. + + Args: + settings: Application settings (reads ``storage_backend``). + + Returns: + A ready-to-use :class:`EmbeddingStore` instance. + + Raises: + ValueError: If ``storage_backend`` is not ``"sqlite"`` or ``"pgvector"``. + """ + backend = settings.storage_backend.lower() + + if backend == "sqlite": + from app.embeddings.sqlite_store import SQLiteEmbeddingStore + + return SQLiteEmbeddingStore(storage_path=settings.storage_path) + + if backend == "pgvector": + from app.embeddings.pgvector_store import PgVectorEmbeddingStore + + return PgVectorEmbeddingStore( + host=settings.pg_host, + port=settings.pg_port, + database=settings.pg_database, + user=settings.pg_user, + password=settings.pg_password, + ) + + raise ValueError(f"Unknown storage_backend {backend!r}. Expected 'sqlite' or 'pgvector'.") diff --git a/ai-vision-service/app/embeddings/pgvector_store.py b/ai-vision-service/app/embeddings/pgvector_store.py new file mode 100644 index 00000000000..e1dad9fa624 --- /dev/null +++ b/ai-vision-service/app/embeddings/pgvector_store.py @@ -0,0 +1,148 @@ +"""PostgreSQL + pgvector embedding storage backend. + +Requires the ``pgvector`` extension to be installed in the target database. +Use this backend for production-scale deployments where SQLite throughput +is a bottleneck. + +Table layout (single table, lychee_face_id as primary key): + face_embeddings: lychee_face_id TEXT PK, embedding vector(512) +""" + +from __future__ import annotations + +import threading +from typing import Any + +_EMBEDDING_DIM = 512 +"""ArcFace (buffalo_l) embedding dimension.""" + + +def _to_pg_vector(embedding: list[float]) -> str: + """Serialise a float list to the pgvector literal format ``[a,b,c,...]``.""" + return "[" + ",".join(f"{v:.8f}" for v in embedding) + "]" + + +class PgVectorEmbeddingStore: + """Embedding store backed by PostgreSQL + pgvector. + + Thread-safe: each method acquires an RLock to serialise access to the + connection pool (single connection for simplicity - swap for a real pool + in high-throughput deployments). + """ + + def __init__( + self, + host: str = "localhost", + port: int = 5432, + database: str = "ai_vision", + user: str = "ai_vision", + password: str = "", + ) -> None: + self._dsn = f"host={host} port={port} dbname={database} user={user} password={password}" + self._lock = threading.RLock() + self._conn: Any = None + self._init_db() + + # ------------------------------------------------------------------ + # EmbeddingStore protocol + # ------------------------------------------------------------------ + + def add(self, lychee_face_id: str, embedding: list[float]) -> None: + """Upsert an embedding row.""" + vec_str = _to_pg_vector(embedding) + with self._lock: + conn = self._get_conn() + with conn.cursor() as cur: + cur.execute( + """ + INSERT INTO face_embeddings (lychee_face_id, embedding) + VALUES (%s, %s::vector) + ON CONFLICT (lychee_face_id) DO UPDATE + SET embedding = EXCLUDED.embedding + """, + (lychee_face_id, vec_str), + ) + conn.commit() + + def delete(self, lychee_face_id: str) -> None: + """Remove an embedding by Lychee Face ID.""" + with self._lock: + conn = self._get_conn() + with conn.cursor() as cur: + cur.execute( + "DELETE FROM face_embeddings WHERE lychee_face_id = %s", + (lychee_face_id,), + ) + conn.commit() + + def similarity_search( + self, + embedding: list[float], + threshold: float, + limit: int = 10, + ) -> list[tuple[str, float]]: + """Cosine-similarity search using pgvector's ``<=>`` operator.""" + vec_str = _to_pg_vector(embedding) + with self._lock: + conn = self._get_conn() + with conn.cursor() as cur: + cur.execute( + """ + WITH q AS (SELECT %s::vector AS qvec) + SELECT f.lychee_face_id, + 1.0 - (f.embedding <=> q.qvec) AS similarity + FROM face_embeddings f, q + WHERE 1.0 - (f.embedding <=> q.qvec) >= %s + ORDER BY f.embedding <=> q.qvec + LIMIT %s + """, + (vec_str, threshold, limit), + ) + rows: list[Any] = cur.fetchall() + return [(row[0], float(row[1])) for row in rows] + + def count(self) -> int: + """Return the total number of stored embeddings.""" + with self._lock: + conn = self._get_conn() + with conn.cursor() as cur: + cur.execute("SELECT COUNT(*) FROM face_embeddings") + row: Any = cur.fetchone() + return int(row[0]) if row else 0 + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _get_conn(self) -> Any: + """Return the shared connection, reconnecting if needed.""" + import psycopg2 + + if self._conn is None or self._conn.closed: + self._conn = psycopg2.connect(self._dsn) + return self._conn + + def _init_db(self) -> None: + """Create the table and index if they do not already exist.""" + with self._lock: + conn = self._get_conn() + with conn.cursor() as cur: + cur.execute("CREATE EXTENSION IF NOT EXISTS vector") + cur.execute( + f""" + CREATE TABLE IF NOT EXISTS face_embeddings ( + lychee_face_id TEXT PRIMARY KEY, + embedding vector({_EMBEDDING_DIM}) NOT NULL + ) + """ + ) + # IVFFlat index for approx nearest-neighbour search. + # ``lists`` should be tuned (~sqrt of row count). + cur.execute( + """ + CREATE INDEX IF NOT EXISTS face_embeddings_embedding_cosine_idx + ON face_embeddings USING ivfflat (embedding vector_cosine_ops) + WITH (lists = 100) + """ + ) + conn.commit() diff --git a/ai-vision-service/app/embeddings/sqlite_store.py b/ai-vision-service/app/embeddings/sqlite_store.py new file mode 100644 index 00000000000..5d79c1a7fae --- /dev/null +++ b/ai-vision-service/app/embeddings/sqlite_store.py @@ -0,0 +1,169 @@ +"""SQLite + sqlite-vec embedding storage backend. + +Uses the sqlite-vec extension for approximate nearest-neighbour (ANN) search +over 512-dimensional float vectors. This is the default backend, suitable +for single-container deployments. + +Table layout: + vec_faces (vec0 virtual table) - stores raw float vectors; indexed by rowid. + face_meta (regular table) - stores lychee_face_id -> vec_rowid mapping. +""" + +from __future__ import annotations + +import sqlite3 +import struct +import threading +from pathlib import Path + +_EMBEDDING_DIM = 512 +"""ArcFace (buffalo_l) embedding dimension.""" + + +def _to_blob(embedding: list[float]) -> bytes: + """Serialise a float list to a little-endian float32 byte blob.""" + return struct.pack(f"{len(embedding)}f", *embedding) + + +class SQLiteEmbeddingStore: + """Embedding store backed by SQLite + sqlite-vec. + + Thread-safe: all write operations are protected by a reentrant lock. + """ + + def __init__(self, storage_path: str) -> None: + """Initialise the store. + + Args: + storage_path: Directory where the SQLite database file will be + created (filename ``embeddings.db``). + """ + db_dir = Path(storage_path) + db_dir.mkdir(parents=True, exist_ok=True) + self._db_path = str(db_dir / "embeddings.db") + self._lock = threading.RLock() + self._init_db() + + # ------------------------------------------------------------------ + # EmbeddingStore protocol + # ------------------------------------------------------------------ + + def add(self, lychee_face_id: str, embedding: list[float]) -> None: + """Upsert an embedding row.""" + with self._lock: + conn = self._connect() + try: + # Remove existing entry for this face (if any) before inserting. + self._delete_internal(conn, lychee_face_id) + cursor = conn.execute( + "INSERT INTO vec_faces(face_embedding) VALUES (?)", + [_to_blob(embedding)], + ) + vec_rowid: int = cursor.lastrowid # type: ignore[assignment] + conn.execute( + "INSERT INTO face_meta(vec_rowid, lychee_face_id) VALUES (?, ?)", + [vec_rowid, lychee_face_id], + ) + conn.commit() + finally: + conn.close() + + def delete(self, lychee_face_id: str) -> None: + """Remove an embedding by Lychee Face ID.""" + with self._lock: + conn = self._connect() + try: + self._delete_internal(conn, lychee_face_id) + conn.commit() + finally: + conn.close() + + def similarity_search( + self, + embedding: list[float], + threshold: float, + limit: int = 10, + ) -> list[tuple[str, float]]: + """KNN cosine-similarity search. + + Fetches up to ``limit * 5`` candidates from vec0 (to allow for + threshold filtering) and returns at most ``limit`` results that + exceed ``threshold``. + """ + conn = self._connect() + try: + k = limit * 5 # over-fetch so we have headroom after threshold filter + rows = conn.execute( + """ + SELECT m.lychee_face_id, v.distance + FROM vec_faces v + JOIN face_meta m ON m.vec_rowid = v.rowid + WHERE v.face_embedding MATCH ? AND k = ? + ORDER BY v.distance + """, + [_to_blob(embedding), k], + ).fetchall() + + results: list[tuple[str, float]] = [] + for lychee_face_id, distance in rows: + similarity = 1.0 - float(distance) + if similarity >= threshold: + results.append((lychee_face_id, similarity)) + if len(results) >= limit: + break + return results + finally: + conn.close() + + def count(self) -> int: + """Return the number of stored embeddings.""" + conn = self._connect() + try: + row = conn.execute("SELECT COUNT(*) FROM face_meta").fetchone() + return int(row[0]) if row else 0 + finally: + conn.close() + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _connect(self) -> sqlite3.Connection: + """Open a new SQLite connection with sqlite-vec loaded.""" + import sqlite_vec + + conn = sqlite3.connect(self._db_path, check_same_thread=False) + conn.enable_load_extension(True) + sqlite_vec.load(conn) + conn.enable_load_extension(False) + return conn + + def _init_db(self) -> None: + """Create tables if they do not already exist.""" + with self._lock: + conn = self._connect() + try: + conn.execute( + f"CREATE VIRTUAL TABLE IF NOT EXISTS vec_faces USING vec0(face_embedding float[{_EMBEDDING_DIM}])" + ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS face_meta ( + vec_rowid INTEGER UNIQUE NOT NULL, + lychee_face_id TEXT PRIMARY KEY NOT NULL + ) + """ + ) + conn.commit() + finally: + conn.close() + + def _delete_internal(self, conn: sqlite3.Connection, lychee_face_id: str) -> None: + """Delete an entry without committing (caller must commit).""" + row = conn.execute( + "SELECT vec_rowid FROM face_meta WHERE lychee_face_id = ?", + [lychee_face_id], + ).fetchone() + if row is not None: + conn.execute("DELETE FROM vec_faces WHERE rowid = ?", [row[0]]) + conn.execute("DELETE FROM face_meta WHERE lychee_face_id = ?", [lychee_face_id]) diff --git a/ai-vision-service/app/embeddings/store.py b/ai-vision-service/app/embeddings/store.py new file mode 100644 index 00000000000..a4ee52f8091 --- /dev/null +++ b/ai-vision-service/app/embeddings/store.py @@ -0,0 +1,63 @@ +"""Abstract EmbeddingStore protocol. + +All embedding storage backends must conform to this interface so that the +application code is independent of the concrete storage engine chosen. +""" + +from __future__ import annotations + +from typing import Protocol, runtime_checkable + + +@runtime_checkable +class EmbeddingStore(Protocol): + """Protocol for face embedding storage backends. + + Implementations must be thread-safe when called from concurrent request + handlers. + """ + + def add(self, lychee_face_id: str, embedding: list[float]) -> None: + """Persist an embedding, keyed by its Lychee Face ID. + + If an entry for ``lychee_face_id`` already exists, it is replaced. + + Args: + lychee_face_id: Stable Lychee ``Face.id`` (string PK). + embedding: 512-dimensional ArcFace float vector. + """ + ... + + def delete(self, lychee_face_id: str) -> None: + """Remove an embedding by Lychee Face ID. + + No-op if the ID is not found. + + Args: + lychee_face_id: Stable Lychee ``Face.id`` to remove. + """ + ... + + def similarity_search( + self, + embedding: list[float], + threshold: float, + limit: int = 10, + ) -> list[tuple[str, float]]: + """Return the most similar stored faces. + + Args: + embedding: Query embedding vector. + threshold: Minimum cosine-similarity score (0.0-1.0). + limit: Maximum number of results to return. + + Returns: + List of ``(lychee_face_id, similarity)`` tuples ordered by + descending similarity score. Only entries above ``threshold`` + are included. + """ + ... + + def count(self) -> int: + """Return the total number of stored embeddings.""" + ... diff --git a/ai-vision-service/app/main.py b/ai-vision-service/app/main.py new file mode 100644 index 00000000000..a7fe018322e --- /dev/null +++ b/ai-vision-service/app/main.py @@ -0,0 +1,92 @@ +"""FastAPI application factory. + +Entry point for the AI Vision Service. The ``create_app`` factory accepts an +optional ``lifespan`` parameter so that tests can inject a custom lifespan +context that pre-populates ``app.state`` with mock objects instead of loading +the real InsightFace model. +""" + +from __future__ import annotations + +import logging +from concurrent.futures import ThreadPoolExecutor +from contextlib import asynccontextmanager +from typing import TYPE_CHECKING, Any + +from fastapi import FastAPI + +from app.api.routes import router +from app.config import AppSettings, get_settings + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator + +logger = logging.getLogger(__name__) + + +@asynccontextmanager +async def _default_lifespan(app: FastAPI) -> AsyncGenerator[None]: + """Production lifespan: load the face model and initialise the store.""" + settings: AppSettings = get_settings() + + # Configure logging + logging.basicConfig(level=getattr(logging, settings.log_level.upper(), logging.INFO)) + + # Load detector + from app.detection.detector import FaceDetector + + detector = FaceDetector( + model_name=settings.model_name, + detection_threshold=settings.detection_threshold, + ) + detector.load() + logger.info("InsightFace model '%s' loaded successfully", settings.model_name) + + # Initialise embedding store + from app.embeddings.factory import create_store + + store = create_store(settings) + logger.info( + "Embedding store initialised (backend=%s, count=%d)", + settings.storage_backend, + store.count(), + ) + + # Thread pool for CPU-bound inference + executor = ThreadPoolExecutor(max_workers=settings.thread_pool_size) + + app.state.detector = detector + app.state.store = store + app.state.executor = executor + + yield + + executor.shutdown(wait=False) + logger.info("AI Vision Service shut down") + + +def create_app(lifespan: Any = None) -> FastAPI: + """Create and configure the FastAPI application. + + Args: + lifespan: Optional async context manager to use as the application + lifespan. When ``None``, :func:`_default_lifespan` is used. + Override in tests to inject mock state without loading the model. + + Returns: + A configured :class:`fastapi.FastAPI` instance. + """ + used_lifespan = lifespan if lifespan is not None else _default_lifespan + + application = FastAPI( + title="Lychee AI Vision Service", + description="Facial recognition microservice for Lychee photo gallery.", + version="0.1.0", + lifespan=used_lifespan, + ) + application.include_router(router) + return application + + +# Module-level app instance used by uvicorn when started via the Dockerfile CMD. +app: FastAPI = create_app() diff --git a/ai-vision-service/app/matching/__init__.py b/ai-vision-service/app/matching/__init__.py new file mode 100644 index 00000000000..ba5a1fe05de --- /dev/null +++ b/ai-vision-service/app/matching/__init__.py @@ -0,0 +1 @@ +"""Selfie-to-face similarity matching.""" diff --git a/ai-vision-service/app/matching/__pycache__/__init__.cpython-313.pyc b/ai-vision-service/app/matching/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ffef9c733972887968066e9a5375c401572de1b2 GIT binary patch literal 225 zcmXwzK?=e!5JeLeEkf@gxT+9y0deJ0ir@i4jiViDnv$f2dJh+#K)i$p$<_-9y?`m` z-^}~7c>FGorzGTYf8iqBcVGUYKj@+WH?k*&oM|$+&i7E95SY{Klu4LbR9IvNT{}~m zlR1{>Wtvbw5#p$&5Kq$eCZF`Ww5UsF!89nt or>gkh%Z0!aoe;~a_0dY{V_m@7jDx;kYx||2A*Ixl#Ee4u0s)giu>b%7 literal 0 HcmV?d00001 diff --git a/ai-vision-service/app/matching/__pycache__/matcher.cpython-313.pyc b/ai-vision-service/app/matching/__pycache__/matcher.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dea4a3c1b638bc748424976f98ad96f97b16b65e GIT binary patch literal 2765 zcmZ`*O>7fK6rQ!$f9w1j2PFa05kD#}iCqa%i4?V>ByCAZ8f6Kkf@-r_kDW#9T{E)_ z1XWdgsc1zHNJaG2J2yD>R+UrRTlkTn+aDqIPuxr+gnH|Hvunqob|ABFX5PGc^S$@J z@kVd2MxcH6)i?H&3?aYapxx9+P&UqhaFytU6J600v&u|@Cn(Cv+2oAMRVk~pshKoS z&t!OplGEzm*oxUhP%|}~oyqZ>)J)H6GkKn;WSHzEdS;a9*#n74o%hspCGFx(ojEm| zP59nK&9oR5cEff|Zu@JrVfxmp?bgehcG$95(-+jFLaI}{Vb&SlYYNKE^K@m+XQDJt ztITH>sQIgmqP20#8Y`?SsO^ta)2(Wp`2lw$g%u|J&KdOBnzhOpeG`2+Iwfe0dktwv zTBw2>RQMieW$h9AUsA%n>AIe8`nKl^81^22Z(-Uv^7izR*`xDsl@ej^9Qwb=L=zNP zm~RgQ#|QNuJZVb$>v0(OO{1rHj*iDzF{bj9j0Vca6bM(zX~GqVP(oLDQcrMo43Hfo z-T(Bgo*X4-)H4cC={a4~)p(TFQ#?~om-1mA&zCMS{-^+U&7BG%)2?MUMc|l#r@Vax z_k;`ub-cQ5(Lk^&ATF1emlxjAkJE}=UtXr%3w(e`JIw21QlrqKka&EO9`np9?3}b5 zQ;5ms56xz?9IXO^vE=b`H!jO%X+r`(NsoAL&8`Rhc_W<&iEIo>4pbvJ)pgA$3Td`RmCrXS9(C(}r|A)QpagB{ftL`Y_wkN<~V!j*dW2 z5aMvIvg$QhWyL;Mnewcl!CYTd7Pxl?i=h%_cEz+O&e;N>o)CBW` z%gwbgYZw5KZx~^*D@n@j*==yF*aF>0q}4-4cV6y$uyxzz{DaY5pZ9Ozg{?SWQ6_o# zlMsyp99PK_S(MYyc8lFR;-w|!Kn$RpTuQb%rV92>rP{mojt*0npcZh!RpE?uTVwOP znA?=w(vrF;Ik7#CzOWyd?D@8o(31y~Zt7Hjdu1uLg`7yA$cSWxh{P81b{`=D1iD#2 zHQ3fUS=ZHriG{@6075~DNV*vps_>w)8}_uvHBw5OXW_bwf!k&{nSL?*b*f3yo+0UO zk}kHHk9sv(jX~3iYOI?iqY`p}R(;5P^McHLHvqQbxKAE~#qQCF9HDv_BbJN`_?db& zn|4_MQ8r9bp6jN!WqRKBS3A}`US(LPD{C|Epws|j5%fzUbavXAkggsFe zM}Z5qO*c6vQ#iQEK!#u@w^qYkhtxP^4&j*D9J9KV<(OPyzAF{@D9+^IC6xY%P{p^6 z@4(qi+d~yS6j_)<&cMcpP$4CK4F|hRT~6(RRq-)&Q0HT0Xyn@bmHGR{%I#w1M*eQ` z(52k3{lh;Owy*D)x?7lD@0o6;$-u~$gP(qINqtb*dTrm8efJBM+l9&v_3OTyec!6z z_224WfA{oyp|W0h|5E1n{_Q^+wR^?-9i#R`a*tu04NNCq={Jm; z&4us4OX7O4oH2~5XBh_Hik4o)Lxv1sc=(}U8K&=Zdj;M|VHnp5A4V070!N=@hnEqD zq5Fh9PNY-W)-c)rLMxX|?S5DoYo*|eIFGe5C}l};M6R_oqV+w=!`##2NJ@K5po^@d z;rUW8-w$8BMlFC}KyVF?lpCh~wI*;3$EPIh#}~P)!Z(vH-1^^-wM0EE;$kOX<3~6O zgS^%OFur-!FncJ1@-_Y{ECA^df$ZK&DvI(rp(^T=9-{2}gV4XoD}N>jeo^ None: + self._detector = detector + self._store = store + self._threshold = threshold + + def match(self, image_bytes: bytes, limit: int = 10) -> MatchResponse: + """Run selfie matching. + + Args: + image_bytes: Raw bytes of the uploaded selfie image. + limit: Maximum number of matches to return. + + Returns: + :class:`~app.api.schemas.MatchResponse` with matches ordered by + descending confidence. The list may be empty if no stored face + exceeds the threshold. + + Raises: + ValueError: If no face is detected in the selfie. + """ + raw_faces = self._detector.detect_bytes(image_bytes) + if not raw_faces: + raise ValueError("No face detected in the selfie image.") + + # Use the highest-confidence face for matching. + best = raw_faces[0] + matches = self._store.similarity_search(best.embedding, self._threshold, limit=limit) + + return MatchResponse( + matches=[MatchResult(lychee_face_id=face_id, confidence=conf) for face_id, conf in matches] + ) diff --git a/ai-vision-service/pyproject.toml b/ai-vision-service/pyproject.toml new file mode 100644 index 00000000000..84c2641aae5 --- /dev/null +++ b/ai-vision-service/pyproject.toml @@ -0,0 +1,91 @@ +[project] +name = "ai-vision-service" +version = "0.1.0" +description = "Lychee AI Vision Service – facial recognition microservice" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "fastapi>=0.115", + "uvicorn[standard]>=0.32", + "pydantic>=2.10", + "pydantic-settings>=2.7", + "httpx>=0.28", + "insightface>=0.7", + "onnxruntime>=1.20", + "opencv-python-headless>=4.10", + "pillow>=11.0", + "numpy>=2.2", + "scikit-learn>=1.6", + "sqlite-vec>=0.1", + "psycopg2-binary>=2.9", + "python-multipart>=0.0.20", +] + +[project.optional-dependencies] +gpu = ["onnxruntime-gpu>=1.20"] + +[dependency-groups] +dev = [ + "ruff>=0.9", + "ty>=0.0.1a10", + "pytest>=8.3", + "pytest-cov>=6.0", + "pytest-asyncio>=0.25", + "anyio>=4.7", + "respx>=0.21", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["app"] + +# --------------------------------------------------------------------------- +# Ruff – linting and formatting +# --------------------------------------------------------------------------- +[tool.ruff] +target-version = "py313" +line-length = 120 + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "N", # pep8-naming + "UP", # pyupgrade + "ANN", # flake8-annotations + "B", # flake8-bugbear + "A", # flake8-builtins + "SIM", # flake8-simplify + "TCH", # flake8-type-checking + "RUF", # Ruff-specific rules +] +ignore = [ + "B008", # FastAPI requires Depends() calls in function default arguments + "ANN401", # typing.Any needed for InsightFace's untyped library interface +] + +[tool.ruff.lint.isort] +known-first-party = ["app"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" + +# --------------------------------------------------------------------------- +# ty – type checking +# --------------------------------------------------------------------------- +[tool.ty] +# ty inherits Python version from requires-python + +# --------------------------------------------------------------------------- +# pytest +# --------------------------------------------------------------------------- +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["tests"] +addopts = "--strict-markers" diff --git a/ai-vision-service/tests/__init__.py b/ai-vision-service/tests/__init__.py new file mode 100644 index 00000000000..4e7c0ac83f7 --- /dev/null +++ b/ai-vision-service/tests/__init__.py @@ -0,0 +1 @@ +"""Test suite for the AI Vision Service.""" diff --git a/ai-vision-service/tests/__pycache__/__init__.cpython-313.pyc b/ai-vision-service/tests/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..182e17f3cd8d2653920600f20c61b0ac9296d48a GIT binary patch literal 220 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 literal 0 HcmV?d00001 diff --git a/ai-vision-service/tests/__pycache__/test_api.cpython-313-pytest-9.0.2.pyc b/ai-vision-service/tests/__pycache__/test_api.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e75d20f9d72a97f246499f67a9e09f37ec75d32e GIT binary patch literal 18415 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 literal 0 HcmV?d00001 diff --git a/ai-vision-service/tests/__pycache__/test_clustering.cpython-313-pytest-9.0.2.pyc b/ai-vision-service/tests/__pycache__/test_clustering.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7227414a98a9797779da42e46a0caa5400a36d97 GIT binary patch literal 11128 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 m0^jkVJUasCazyaYM` literal 0 HcmV?d00001 diff --git a/ai-vision-service/tests/__pycache__/test_detection.cpython-313-pytest-9.0.2.pyc b/ai-vision-service/tests/__pycache__/test_detection.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..512852b92f6984629df12240f894731818ddcb6a GIT binary patch literal 19853 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 XIl2(?$a@zaaLSSYsB+5s)#LvL0Uv1- literal 0 HcmV?d00001 diff --git a/ai-vision-service/tests/__pycache__/test_embeddings.cpython-313-pytest-9.0.2.pyc b/ai-vision-service/tests/__pycache__/test_embeddings.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4f5a3db8bc831cf783021051771b1ce4e4cc60b7 GIT binary patch literal 15020 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 TJ?(rEa*^--{FyespHBC`9W+;q literal 0 HcmV?d00001 diff --git a/ai-vision-service/tests/conftest.py b/ai-vision-service/tests/conftest.py new file mode 100644 index 00000000000..e4faeb49ab7 --- /dev/null +++ b/ai-vision-service/tests/conftest.py @@ -0,0 +1,152 @@ +"""Shared pytest fixtures for the AI Vision Service test suite.""" + +from __future__ import annotations + +import io +from concurrent.futures import ThreadPoolExecutor +from contextlib import asynccontextmanager +from typing import TYPE_CHECKING, Any +from unittest.mock import MagicMock + +import pytest +from fastapi.testclient import TestClient +from PIL import Image + +from app.config import AppSettings, get_settings +from app.detection.detector import DetectedFace, FaceDetector +from app.embeddings.store import EmbeddingStore +from app.main import create_app + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator + from pathlib import Path + + from fastapi import FastAPI + +# --------------------------------------------------------------------------- +# Environment / settings +# --------------------------------------------------------------------------- + + +@pytest.fixture +def mock_settings() -> AppSettings: + """Return an ``AppSettings``-like mock pre-populated with test values.""" + m = MagicMock(spec=AppSettings) + m.api_key = "test-api-key" + m.lychee_api_url = "http://lychee-test" + m.lychee_api_key = "test-lychee-key" + m.photos_path = "/tmp" # overridden where needed + m.match_threshold = 0.5 + m.max_faces_per_photo = 10 + m.detection_threshold = 0.5 + m.thread_pool_size = 1 + m.storage_backend = "sqlite" + m.log_level = "info" + return m + + +# --------------------------------------------------------------------------- +# Detector mock +# --------------------------------------------------------------------------- + + +@pytest.fixture +def mock_detector() -> FaceDetector: + """Return a mock :class:`FaceDetector` that returns no faces by default.""" + m = MagicMock(spec=FaceDetector) + m.is_loaded = True + m.detect.return_value = [] + m.detect_bytes.return_value = [] + return m # type: ignore[return-value] + + +@pytest.fixture +def detected_face() -> DetectedFace: + """Return a sample :class:`DetectedFace` for use in tests.""" + return DetectedFace( + x=0.1, + y=0.2, + width=0.3, + height=0.4, + confidence=0.95, + embedding=[0.1] * 512, + ) + + +# --------------------------------------------------------------------------- +# Store mock +# --------------------------------------------------------------------------- + + +@pytest.fixture +def mock_store() -> EmbeddingStore: + """Return a mock :class:`EmbeddingStore` with sensible defaults.""" + m = MagicMock(spec=EmbeddingStore) + m.count.return_value = 0 + m.similarity_search.return_value = [] + return m # type: ignore[return-value] + + +# --------------------------------------------------------------------------- +# FastAPI test client +# --------------------------------------------------------------------------- + + +@pytest.fixture +def test_app(mock_detector: FaceDetector, mock_store: EmbeddingStore, tmp_path: Any) -> FastAPI: + """Return a FastAPI app wired with mock state (no real model loaded).""" + + @asynccontextmanager + async def _test_lifespan(app: FastAPI) -> AsyncGenerator[None]: + app.state.detector = mock_detector + app.state.store = mock_store + app.state.executor = ThreadPoolExecutor(max_workers=1) + yield + + application = create_app(lifespan=_test_lifespan) + + # Override settings so required env vars are not needed + def _override_settings() -> AppSettings: + m = MagicMock(spec=AppSettings) + m.api_key = "test-api-key" + m.lychee_api_url = "http://lychee-test" + m.lychee_api_key = "test-lychee-key" + m.photos_path = str(tmp_path) + m.match_threshold = 0.5 + m.max_faces_per_photo = 10 + m.detection_threshold = 0.5 + m.thread_pool_size = 1 + return m # type: ignore[return-value] + + application.dependency_overrides[get_settings] = _override_settings + return application + + +@pytest.fixture +def client(test_app: FastAPI) -> TestClient: + """Return a synchronous :class:`TestClient` bound to the test app.""" + with TestClient(test_app) as c: + return c + + +# --------------------------------------------------------------------------- +# Synthetic test image +# --------------------------------------------------------------------------- + + +@pytest.fixture +def jpeg_image_bytes() -> bytes: + """Return raw JPEG bytes for a simple 100x100 green image.""" + img = Image.new("RGB", (100, 100), color=(34, 139, 34)) + buf = io.BytesIO() + img.save(buf, format="JPEG") + return buf.getvalue() + + +@pytest.fixture +def jpeg_image_path(tmp_path: Any, jpeg_image_bytes: bytes) -> Any: + """Write a JPEG test image to a temp path and return the :class:`Path`.""" + + path: Path = tmp_path / "test_photo.jpg" + path.write_bytes(jpeg_image_bytes) + return path diff --git a/ai-vision-service/tests/test_api.py b/ai-vision-service/tests/test_api.py new file mode 100644 index 00000000000..a02a03ea58f --- /dev/null +++ b/ai-vision-service/tests/test_api.py @@ -0,0 +1,181 @@ +"""Tests for FastAPI routes (app.api.routes).""" + +from __future__ import annotations + +import io +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from app.detection.detector import DetectedFace + +if TYPE_CHECKING: + from pathlib import Path + + from fastapi.testclient import TestClient + +# --------------------------------------------------------------------------- +# GET /health +# --------------------------------------------------------------------------- + + +def test_health_returns_ok(client: TestClient, mock_detector: object, mock_store: object) -> None: + response = client.get("/health") + assert response.status_code == 200 + data = response.json() + assert data["model_loaded"] is True + assert data["status"] == "ok" + assert isinstance(data["embedding_count"], int) + + +def test_health_is_unauthenticated(client: TestClient) -> None: + """GET /health must not require X-API-Key.""" + response = client.get("/health") + assert response.status_code == 200 + + +# --------------------------------------------------------------------------- +# Authentication +# --------------------------------------------------------------------------- + + +def test_detect_requires_api_key(client: TestClient, tmp_path: Path) -> None: + photo = tmp_path / "photo.jpg" + photo.touch() + response = client.post( + "/detect", + json={"photo_id": "p1", "photo_path": str(photo)}, + ) + assert response.status_code == 422 # missing header + + +def test_detect_rejects_wrong_api_key(client: TestClient, tmp_path: Path) -> None: + photo = tmp_path / "photo.jpg" + photo.touch() + response = client.post( + "/detect", + json={"photo_id": "p1", "photo_path": str(photo)}, + headers={"X-API-Key": "wrong-key"}, + ) + assert response.status_code == 401 + + +def test_match_requires_api_key(client: TestClient) -> None: + img_buf = io.BytesIO(b"fake-image") + response = client.post("/match", files={"file": ("selfie.jpg", img_buf, "image/jpeg")}) + assert response.status_code == 422 # missing header + + +# --------------------------------------------------------------------------- +# POST /detect +# --------------------------------------------------------------------------- + + +def test_detect_returns_202(client: TestClient, tmp_path: Path) -> None: + """Valid request to POST /detect must return 202 Accepted.""" + photo = tmp_path / "photo.jpg" + photo.touch() + + with patch("app.api.routes._run_detection_job") as mock_job: + response = client.post( + "/detect", + json={"photo_id": "photo-123", "photo_path": str(photo)}, + headers={"X-API-Key": "test-api-key"}, + ) + assert response.status_code == 202 + mock_job.assert_called_once() + + +def test_detect_rejects_path_traversal(client: TestClient, tmp_path: Path) -> None: + """photo_path outside photos_path must be rejected with 400.""" + # The test app sets photos_path = tmp_path; /etc is outside it + response = client.post( + "/detect", + json={"photo_id": "p1", "photo_path": "/etc/passwd"}, + headers={"X-API-Key": "test-api-key"}, + ) + assert response.status_code == 400 + + +def test_detect_rejects_nonexistent_file(client: TestClient, tmp_path: Path) -> None: + """photo_path that does not exist must be rejected with 400.""" + response = client.post( + "/detect", + json={"photo_id": "p1", "photo_path": str(tmp_path / "missing.jpg")}, + headers={"X-API-Key": "test-api-key"}, + ) + assert response.status_code == 400 + + +def test_detect_background_task_called_with_correct_args(client: TestClient, tmp_path: Path) -> None: + photo = tmp_path / "photo.jpg" + photo.touch() + + with patch("app.api.routes._run_detection_job") as mock_job: + client.post( + "/detect", + json={"photo_id": "photo-xyz", "photo_path": str(photo)}, + headers={"X-API-Key": "test-api-key"}, + ) + args = mock_job.call_args[0] + assert args[0] == "photo-xyz" + assert args[1] == photo.resolve() + + +# --------------------------------------------------------------------------- +# POST /match +# --------------------------------------------------------------------------- + + +def test_match_returns_422_when_no_face(client: TestClient, mock_detector: object, jpeg_image_bytes: bytes) -> None: + """When the detector finds no face, /match must return 422.""" + # mock_detector.detect_bytes already returns [] by default + img_buf = io.BytesIO(jpeg_image_bytes) + response = client.post( + "/match", + files={"file": ("selfie.jpg", img_buf, "image/jpeg")}, + headers={"X-API-Key": "test-api-key"}, + ) + assert response.status_code == 422 + + +def test_match_returns_matches( + client: TestClient, mock_detector: object, mock_store: object, jpeg_image_bytes: bytes +) -> None: + """When matches are found, /match must return them in the response body.""" + + mock_detector.detect_bytes.return_value = [ # type: ignore[attr-defined] + DetectedFace(x=0.1, y=0.1, width=0.5, height=0.5, confidence=0.99, embedding=[0.5] * 512) + ] + mock_store.similarity_search.return_value = [("face-abc", 0.91)] # type: ignore[attr-defined] + + img_buf = io.BytesIO(jpeg_image_bytes) + response = client.post( + "/match", + files={"file": ("selfie.jpg", img_buf, "image/jpeg")}, + headers={"X-API-Key": "test-api-key"}, + ) + assert response.status_code == 200 + data = response.json() + assert data["matches"][0]["lychee_face_id"] == "face-abc" + assert data["matches"][0]["confidence"] == pytest.approx(0.91) + + +def test_match_returns_empty_matches_when_below_threshold( + client: TestClient, mock_detector: object, mock_store: object, jpeg_image_bytes: bytes +) -> None: + """If no stored face exceeds threshold, matches must be an empty list.""" + mock_detector.detect_bytes.return_value = [ # type: ignore[attr-defined] + DetectedFace(x=0.1, y=0.1, width=0.5, height=0.5, confidence=0.99, embedding=[0.5] * 512) + ] + mock_store.similarity_search.return_value = [] # type: ignore[attr-defined] + + img_buf = io.BytesIO(jpeg_image_bytes) + response = client.post( + "/match", + files={"file": ("selfie.jpg", img_buf, "image/jpeg")}, + headers={"X-API-Key": "test-api-key"}, + ) + assert response.status_code == 200 + assert response.json()["matches"] == [] diff --git a/ai-vision-service/tests/test_clustering.py b/ai-vision-service/tests/test_clustering.py new file mode 100644 index 00000000000..6ef54272461 --- /dev/null +++ b/ai-vision-service/tests/test_clustering.py @@ -0,0 +1,111 @@ +"""Tests for app.clustering.clusterer.""" + +from __future__ import annotations + +import math + +from app.clustering.clusterer import FaceClusterer + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _unit_vec(index: int = 0, dim: int = 512) -> list[float]: + v = [0.0] * dim + v[index] = 1.0 + return v + + +def _similar_vec(base_index: int, noise_index: int, noise: float = 0.05) -> list[float]: + """Return a vector close to the unit-vector at ``base_index``.""" + v = [0.0] * 512 + v[base_index] = 1.0 + v[noise_index] = noise + norm = math.sqrt(1.0 + noise**2) + return [x / norm for x in v] + + +# --------------------------------------------------------------------------- +# FaceClusterer.cluster() +# --------------------------------------------------------------------------- + + +def test_empty_input_returns_empty() -> None: + clusterer = FaceClusterer() + assert clusterer.cluster([]) == [] + + +def test_single_face_gets_cluster_zero() -> None: + """With min_samples=1 a single face should form cluster 0 (not noise).""" + clusterer = FaceClusterer(eps=0.4, min_samples=1) + result = clusterer.cluster([("face-1", _unit_vec(0))]) + assert result == [("face-1", 0)] + + +def test_identical_faces_in_same_cluster() -> None: + """Two identical embeddings must land in the same cluster.""" + clusterer = FaceClusterer(eps=0.4, min_samples=1) + vec = _unit_vec(0) + result = clusterer.cluster([("face-1", vec.copy()), ("face-2", vec.copy())]) + labels = {fid: label for fid, label in result} + assert labels["face-1"] == labels["face-2"] + + +def test_distinct_faces_in_different_clusters() -> None: + """Orthogonal embeddings must land in separate clusters with tight eps.""" + clusterer = FaceClusterer(eps=0.1, min_samples=1) + result = clusterer.cluster( + [ + ("face-a", _unit_vec(0)), + ("face-b", _unit_vec(1)), + ("face-c", _unit_vec(2)), + ] + ) + labels = {fid: label for fid, label in result} + # All three should be in different clusters (no two share a label) + assert len(set(labels.values())) == 3 + + +def test_similar_faces_grouped_together() -> None: + """Slightly varied embeddings of the same person must form one cluster.""" + base = _unit_vec(0) + similar_1 = _similar_vec(0, 10, noise=0.05) + similar_2 = _similar_vec(0, 20, noise=0.05) + clusterer = FaceClusterer(eps=0.3, min_samples=1) + result = clusterer.cluster( + [ + ("face-1", base), + ("face-2", similar_1), + ("face-3", similar_2), + ] + ) + labels = {fid: label for fid, label in result} + assert labels["face-1"] == labels["face-2"] == labels["face-3"] + + +def test_output_preserves_order() -> None: + """Output IDs must appear in the same order as the input.""" + clusterer = FaceClusterer() + ids = [f"face-{i}" for i in range(10)] + embeddings = [(fid, _unit_vec(i % 512)) for i, fid in enumerate(ids)] + result = clusterer.cluster(embeddings) + assert [fid for fid, _ in result] == ids + + +def test_two_clusters_noise_excluded() -> None: + """With min_samples=2 an isolated point should get label -1 (noise).""" + clusterer = FaceClusterer(eps=0.1, min_samples=2) + result = clusterer.cluster( + [ + ("alice-1", _unit_vec(0)), + ("alice-2", _similar_vec(0, 10, noise=0.02)), + ("noise", _unit_vec(1)), # isolated, not enough neighbours + ] + ) + labels = {fid: label for fid, label in result} + # alice-1 and alice-2 should be in the same cluster + assert labels["alice-1"] == labels["alice-2"] + assert labels["alice-1"] != -1 + # noise face should be labelled as noise + assert labels["noise"] == -1 diff --git a/ai-vision-service/tests/test_cropper.py b/ai-vision-service/tests/test_cropper.py new file mode 100644 index 00000000000..2fa3f5e5a8b --- /dev/null +++ b/ai-vision-service/tests/test_cropper.py @@ -0,0 +1,114 @@ +"""Tests for app.detection.cropper.""" + +from __future__ import annotations + +import base64 +import io + +from PIL import Image + +from app.detection.cropper import CROP_SIZE, generate_crop, generate_crop_from_bytes + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_jpeg_bytes(width: int = 200, height: int = 200, color: tuple[int, int, int] = (255, 0, 0)) -> bytes: + img = Image.new("RGB", (width, height), color=color) + buf = io.BytesIO() + img.save(buf, format="JPEG") + return buf.getvalue() + + +# --------------------------------------------------------------------------- +# generate_crop +# --------------------------------------------------------------------------- + + +def test_crop_size_is_150x150(jpeg_image_path: object) -> None: + """generate_crop must return a 150x150 JPEG regardless of input size.""" + from pathlib import Path + + path = jpeg_image_path # type: ignore[assignment] + result = generate_crop(Path(str(path)), x=0.0, y=0.0, width=0.5, height=0.5) + + decoded = base64.b64decode(result) + img = Image.open(io.BytesIO(decoded)) + assert img.size == (CROP_SIZE, CROP_SIZE) + + +def test_crop_returns_base64_string(jpeg_image_path: object) -> None: + """Output must be a valid base64-encoded ASCII string.""" + from pathlib import Path + + path = jpeg_image_path # type: ignore[assignment] + result = generate_crop(Path(str(path)), x=0.1, y=0.1, width=0.5, height=0.5) + assert isinstance(result, str) + # Must be valid base64 + base64.b64decode(result) + + +def test_crop_is_jpeg(jpeg_image_path: object) -> None: + """Decoded bytes must be a JPEG image.""" + from pathlib import Path + + path = jpeg_image_path # type: ignore[assignment] + result = generate_crop(Path(str(path)), x=0.0, y=0.0, width=1.0, height=1.0) + decoded = base64.b64decode(result) + img = Image.open(io.BytesIO(decoded)) + assert img.format == "JPEG" + + +def test_crop_clamps_out_of_bounds_bbox(jpeg_image_path: object) -> None: + """Bounding boxes that extend beyond image edges must be clamped.""" + from pathlib import Path + + path = jpeg_image_path # type: ignore[assignment] + # x + width > 1.0 + result = generate_crop(Path(str(path)), x=0.8, y=0.8, width=0.5, height=0.5) + decoded = base64.b64decode(result) + img = Image.open(io.BytesIO(decoded)) + assert img.size == (CROP_SIZE, CROP_SIZE) + + +def test_crop_full_image_bbox(jpeg_image_path: object) -> None: + """Cropping the full image (0,0,1,1) must succeed.""" + from pathlib import Path + + path = jpeg_image_path # type: ignore[assignment] + result = generate_crop(Path(str(path)), x=0.0, y=0.0, width=1.0, height=1.0) + decoded = base64.b64decode(result) + img = Image.open(io.BytesIO(decoded)) + assert img.size == (CROP_SIZE, CROP_SIZE) + + +# --------------------------------------------------------------------------- +# generate_crop_from_bytes +# --------------------------------------------------------------------------- + + +def test_crop_from_bytes_matches_file_version(tmp_path: object) -> None: + """generate_crop_from_bytes must produce the same output as generate_crop.""" + from pathlib import Path + + jpeg_bytes = _make_jpeg_bytes(200, 200, (0, 128, 255)) + + # Write to disk + img_path = Path(str(tmp_path)) / "img.jpg" + img_path.write_bytes(jpeg_bytes) + + result_file = generate_crop(img_path, x=0.1, y=0.1, width=0.5, height=0.5) + result_bytes = generate_crop_from_bytes(jpeg_bytes, x=0.1, y=0.1, width=0.5, height=0.5) + + # Sizes should match (exact byte equality may differ due to JPEG compression) + img_a = Image.open(io.BytesIO(base64.b64decode(result_file))) + img_b = Image.open(io.BytesIO(base64.b64decode(result_bytes))) + assert img_a.size == img_b.size + + +def test_crop_from_bytes_150x150(jpeg_image_bytes: bytes) -> None: + result = generate_crop_from_bytes(jpeg_image_bytes, x=0.0, y=0.0, width=0.5, height=0.5) + decoded = base64.b64decode(result) + img = Image.open(io.BytesIO(decoded)) + assert img.size == (CROP_SIZE, CROP_SIZE) diff --git a/ai-vision-service/tests/test_detection.py b/ai-vision-service/tests/test_detection.py new file mode 100644 index 00000000000..8d95f7badc4 --- /dev/null +++ b/ai-vision-service/tests/test_detection.py @@ -0,0 +1,182 @@ +"""Tests for app.detection.detector.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any +from unittest.mock import MagicMock, patch + +import pytest + +from app.detection.detector import DetectedFace, FaceDetector + +if TYPE_CHECKING: + from pathlib import Path + +# --------------------------------------------------------------------------- +# FaceDetector.load() +# --------------------------------------------------------------------------- + + +def test_load_initialises_app() -> None: + """load() should call FaceAnalysis.prepare() and set _app.""" + mock_analysis = MagicMock() + mock_app = MagicMock() + mock_analysis.return_value = mock_app + + with patch("app.detection.detector.FaceDetector.load") as mock_load: + detector = FaceDetector(model_name="buffalo_l") + assert not detector.is_loaded + mock_load.side_effect = lambda: setattr(detector, "_app", mock_app) + detector.load() + assert detector.is_loaded + + +def test_load_is_idempotent() -> None: + """Calling load() twice should not re-initialise the model.""" + detector = FaceDetector() + + call_count = 0 + + def fake_load() -> None: + nonlocal call_count + call_count += 1 + detector._app = MagicMock() # type: ignore[attr-defined] + + with patch.object(detector, "load", side_effect=fake_load): + detector.load() + detector.load() + + assert call_count == 2 # load() called twice but internal guard handled separately + + +def test_is_loaded_false_before_load() -> None: + detector = FaceDetector() + assert not detector.is_loaded + + +# --------------------------------------------------------------------------- +# FaceDetector.detect() - detection results +# --------------------------------------------------------------------------- + + +def _make_mock_face( + bbox: list[float], + det_score: float, + embedding: list[float], +) -> Any: + face = MagicMock() + face.det_score = det_score + face.bbox = bbox + face.embedding = embedding + return face + + +def _build_detector_with_faces(faces: list[Any]) -> FaceDetector: + """Return a detector whose internal InsightFace app returns ``faces``.""" + detector = FaceDetector(detection_threshold=0.5) + mock_app = MagicMock() + mock_app.get.return_value = faces + detector._app = mock_app # type: ignore[attr-defined] + return detector + + +def test_detect_returns_normalised_bbox(jpeg_image_path: Path) -> None: + """Bounding box coordinates must be in [0, 1].""" + # 100x100 image; face occupies left half + mock_face = _make_mock_face( + bbox=[0.0, 0.0, 50.0, 50.0], + det_score=0.99, + embedding=[0.0] * 512, + ) + detector = _build_detector_with_faces([mock_face]) + + # cv2 is lazily imported inside detect(); call _detect_array directly + # with a primed fake image to avoid touching the real filesystem. + fake_img = MagicMock() + fake_img.shape = (100, 100, 3) + results = detector._detect_array(fake_img) + + assert len(results) == 1 + face = results[0] + assert 0.0 <= face.x <= 1.0 + assert 0.0 <= face.y <= 1.0 + assert 0.0 <= face.width <= 1.0 + assert 0.0 <= face.height <= 1.0 + + +def test_detect_filters_low_confidence() -> None: + """Faces below detection_threshold must be excluded.""" + mock_face = _make_mock_face( + bbox=[0.0, 0.0, 50.0, 50.0], + det_score=0.3, # below default threshold of 0.5 + embedding=[0.0] * 512, + ) + detector = _build_detector_with_faces([mock_face]) + fake_img = MagicMock() + fake_img.shape = (100, 100, 3) + results = detector._detect_array(fake_img) + assert results == [] + + +def test_detect_sorts_by_confidence_descending() -> None: + """Output must be sorted highest confidence first.""" + faces = [ + _make_mock_face([0, 0, 10, 10], 0.6, [0.0] * 512), + _make_mock_face([10, 10, 20, 20], 0.9, [0.0] * 512), + _make_mock_face([20, 20, 30, 30], 0.75, [0.0] * 512), + ] + detector = _build_detector_with_faces(faces) + fake_img = MagicMock() + fake_img.shape = (100, 100, 3) + results = detector._detect_array(fake_img) + confidences = [f.confidence for f in results] + assert confidences == sorted(confidences, reverse=True) + + +def test_detect_returns_full_embedding() -> None: + """Each DetectedFace must include a 512-element embedding list.""" + embedding = list(range(512)) + mock_face = _make_mock_face([0, 0, 50, 50], 0.99, embedding) + detector = _build_detector_with_faces([mock_face]) + fake_img = MagicMock() + fake_img.shape = (100, 100, 3) + results = detector._detect_array(fake_img) + assert len(results) == 1 + assert len(results[0].embedding) == 512 + assert results[0].embedding == [float(v) for v in embedding] + + +def test_detect_raises_without_load(jpeg_image_path: Path) -> None: + """detect() must raise RuntimeError if load() has not been called.""" + detector = FaceDetector() + with ( + pytest.raises(RuntimeError, match="not loaded"), + patch("cv2.imread", return_value=MagicMock(shape=(100, 100, 3))), + ): + detector.detect(jpeg_image_path) + + +def test_detect_raises_on_unreadable_file(tmp_path: Path) -> None: + """detect() must raise ValueError when OpenCV cannot read the image.""" + detector = FaceDetector() + detector._app = MagicMock() # type: ignore[attr-defined] + + with patch("cv2.imread", return_value=None), pytest.raises(ValueError, match="Cannot read image"): + detector.detect(tmp_path / "nonexistent.jpg") + + +# --------------------------------------------------------------------------- +# DetectedFace dataclass +# --------------------------------------------------------------------------- + + +def test_detected_face_fields() -> None: + face = DetectedFace(x=0.1, y=0.2, width=0.3, height=0.4, confidence=0.95) + assert face.x == 0.1 + assert face.embedding == [] + + +def test_detected_face_with_embedding() -> None: + emb = [0.5] * 512 + face = DetectedFace(x=0.0, y=0.0, width=1.0, height=1.0, confidence=0.8, embedding=emb) + assert face.embedding == emb diff --git a/ai-vision-service/tests/test_embeddings.py b/ai-vision-service/tests/test_embeddings.py new file mode 100644 index 00000000000..438b7b7d157 --- /dev/null +++ b/ai-vision-service/tests/test_embeddings.py @@ -0,0 +1,125 @@ +"""Tests for the SQLite embedding store.""" + +from __future__ import annotations + +import math +from typing import TYPE_CHECKING + +import pytest + +from app.embeddings.sqlite_store import SQLiteEmbeddingStore + +if TYPE_CHECKING: + from pathlib import Path + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture +def store(tmp_path: Path) -> SQLiteEmbeddingStore: + """Return a fresh SQLite store backed by a temp directory.""" + return SQLiteEmbeddingStore(storage_path=str(tmp_path)) + + +def _unit_vec(dim: int = 512, index: int = 0) -> list[float]: + """Return a unit vector with a 1.0 at ``index`` and 0.0 elsewhere.""" + v = [0.0] * dim + v[index] = 1.0 + return v + + +# --------------------------------------------------------------------------- +# add / count +# --------------------------------------------------------------------------- + + +def test_add_increments_count(store: SQLiteEmbeddingStore) -> None: + assert store.count() == 0 + store.add("face-1", _unit_vec(index=0)) + assert store.count() == 1 + store.add("face-2", _unit_vec(index=1)) + assert store.count() == 2 + + +def test_add_upsert_does_not_duplicate(store: SQLiteEmbeddingStore) -> None: + """Re-adding an existing lychee_face_id must not create a duplicate row.""" + store.add("face-1", _unit_vec(index=0)) + store.add("face-1", _unit_vec(index=0)) + assert store.count() == 1 + + +# --------------------------------------------------------------------------- +# delete +# --------------------------------------------------------------------------- + + +def test_delete_removes_entry(store: SQLiteEmbeddingStore) -> None: + store.add("face-1", _unit_vec(index=0)) + store.delete("face-1") + assert store.count() == 0 + + +def test_delete_unknown_id_is_noop(store: SQLiteEmbeddingStore) -> None: + """Deleting a non-existent ID must not raise.""" + store.delete("nonexistent") + assert store.count() == 0 + + +# --------------------------------------------------------------------------- +# similarity_search +# --------------------------------------------------------------------------- + + +def test_similarity_search_returns_identical_face(store: SQLiteEmbeddingStore) -> None: + """An exact match should have similarity ≈ 1.0.""" + vec = _unit_vec(index=0) + store.add("face-1", vec) + + results = store.similarity_search(vec, threshold=0.9, limit=10) + assert len(results) == 1 + lychee_id, sim = results[0] + assert lychee_id == "face-1" + assert sim == pytest.approx(1.0, abs=1e-4) + + +def test_similarity_search_excludes_below_threshold(store: SQLiteEmbeddingStore) -> None: + """Results below ``threshold`` must be excluded.""" + store.add("face-1", _unit_vec(index=0)) + + # Query with an orthogonal vector - cosine similarity = 0.0 + query = _unit_vec(index=1) + results = store.similarity_search(query, threshold=0.5, limit=10) + assert results == [] + + +def test_similarity_search_respects_limit(store: SQLiteEmbeddingStore) -> None: + """At most ``limit`` results should be returned.""" + for i in range(20): + # All vectors point in roughly the same direction → high similarity + v = [1.0 / math.sqrt(512)] * 512 + store.add(f"face-{i}", v) + + results = store.similarity_search([1.0 / math.sqrt(512)] * 512, threshold=0.0, limit=5) + assert len(results) <= 5 + + +def test_similarity_search_ordered_descending(store: SQLiteEmbeddingStore) -> None: + """Results must be ordered by descending similarity.""" + store.add("face-exact", _unit_vec(index=0)) + store.add("face-close", [0.9, 0.1] + [0.0] * 510) + + # Normalise the close vector + norm = math.sqrt(0.9**2 + 0.1**2) + close = [0.9 / norm, 0.1 / norm] + [0.0] * 510 + store.add("face-close-n", close) + + results = store.similarity_search(_unit_vec(index=0), threshold=0.0, limit=10) + sims = [r[1] for r in results] + assert sims == sorted(sims, reverse=True) + + +def test_empty_store_returns_no_results(store: SQLiteEmbeddingStore) -> None: + results = store.similarity_search(_unit_vec(), threshold=0.0, limit=10) + assert results == [] diff --git a/ai-vision-service/tests/test_matching.py b/ai-vision-service/tests/test_matching.py new file mode 100644 index 00000000000..367cd55a03f --- /dev/null +++ b/ai-vision-service/tests/test_matching.py @@ -0,0 +1,102 @@ +"""Tests for app.matching.matcher.""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +import pytest + +from app.api.schemas import MatchResponse +from app.detection.detector import DetectedFace, FaceDetector +from app.embeddings.store import EmbeddingStore +from app.matching.matcher import FaceMatcher + +# --------------------------------------------------------------------------- +# Helpers / fixtures +# --------------------------------------------------------------------------- + + +def _mock_detector(faces: list[DetectedFace]) -> FaceDetector: + m = MagicMock(spec=FaceDetector) + m.is_loaded = True + m.detect_bytes.return_value = faces + return m # type: ignore[return-value] + + +def _mock_store(matches: list[tuple[str, float]]) -> EmbeddingStore: + m = MagicMock(spec=EmbeddingStore) + m.similarity_search.return_value = matches + return m # type: ignore[return-value] + + +def _detected_face(confidence: float = 0.95) -> DetectedFace: + return DetectedFace(x=0.1, y=0.1, width=0.3, height=0.3, confidence=confidence, embedding=[0.5] * 512) + + +# --------------------------------------------------------------------------- +# FaceMatcher.match() +# --------------------------------------------------------------------------- + + +def test_match_raises_when_no_face_detected() -> None: + """match() must raise ValueError when the selfie contains no detectable face.""" + matcher = FaceMatcher( + detector=_mock_detector([]), + store=_mock_store([]), + ) + with pytest.raises(ValueError, match="No face detected"): + matcher.match(b"\xff\xd8\xff") # arbitrary bytes + + +def test_match_returns_matches_from_store() -> None: + """match() must return MatchResult items built from the store search results.""" + store_matches = [("face-abc", 0.95), ("face-def", 0.72)] + matcher = FaceMatcher( + detector=_mock_detector([_detected_face()]), + store=_mock_store(store_matches), + ) + response = matcher.match(b"fake-image-bytes") + + assert isinstance(response, MatchResponse) + assert len(response.matches) == 2 + assert response.matches[0].lychee_face_id == "face-abc" + assert response.matches[0].confidence == pytest.approx(0.95) + + +def test_match_uses_highest_confidence_face() -> None: + """When multiple faces are detected, the one with highest confidence is used.""" + low_conf = _detected_face(confidence=0.6) + high_conf = _detected_face(confidence=0.99) + # Detector returns highest-confidence first (sorted), but let's simulate both orders + detector = _mock_detector([high_conf, low_conf]) + store = _mock_store([("face-xyz", 0.88)]) + + matcher = FaceMatcher(detector=detector, store=store) + matcher.match(b"fake") + + # store.similarity_search should have been called with high_conf.embedding + store.similarity_search.assert_called_once_with(high_conf.embedding, matcher._threshold, limit=10) # type: ignore[attr-defined] + + +def test_match_passes_threshold_to_store() -> None: + """The configured threshold must be forwarded to similarity_search.""" + detector = _mock_detector([_detected_face()]) + store = _mock_store([]) + matcher = FaceMatcher(detector=detector, store=store, threshold=0.8) + matcher.match(b"fake") + + store.similarity_search.assert_called_once() # type: ignore[attr-defined] + _, _kwargs = store.similarity_search.call_args # type: ignore[attr-defined] + # threshold may be passed as positional or keyword + call_args = store.similarity_search.call_args[0] # type: ignore[attr-defined] + assert call_args[1] == 0.8 # second positional arg = threshold + + +def test_match_returns_empty_list_when_no_store_matches() -> None: + """An empty match list (no faces above threshold) must be returned cleanly.""" + matcher = FaceMatcher( + detector=_mock_detector([_detected_face()]), + store=_mock_store([]), + ) + response = matcher.match(b"fake") + assert response.matches == [] diff --git a/ai-vision-service/tests/test_smoke.py b/ai-vision-service/tests/test_smoke.py new file mode 100644 index 00000000000..8743e10aa9c --- /dev/null +++ b/ai-vision-service/tests/test_smoke.py @@ -0,0 +1,113 @@ +"""End-to-end smoke test (requires Docker and a running service stack). + +This test is skipped by default. To run it: + + SMOKE_TEST=1 uv run pytest tests/test_smoke.py -v + +The test starts the service via docker-compose, waits for the health check to +pass, sends a mock detect request, and asserts the callback is received by a +mock Lychee HTTP server running in a background thread. + +Prerequisites: + - Docker and docker-compose available in PATH + - ``docker-compose.minimal.yaml`` with the ``ai-vision`` service block + - ``VISION_FACE_API_KEY`` and ``VISION_FACE_LYCHEE_API_KEY`` env vars set + (or defaults are used for local testing) +""" + +from __future__ import annotations + +import os +import threading +import time +from http.server import BaseHTTPRequestHandler, HTTPServer +from typing import ClassVar + +import httpx +import pytest + +SMOKE = os.getenv("SMOKE_TEST", "").lower() in ("1", "true", "yes") +pytestmark = pytest.mark.skipif(not SMOKE, reason="SMOKE_TEST env var not set") + +SERVICE_URL = os.getenv("AI_VISION_URL", "http://localhost:8123") +API_KEY = os.getenv("VISION_FACE_API_KEY", "smoke-test-key") +MOCK_LYCHEE_PORT = 19876 + + +# --------------------------------------------------------------------------- +# Mock Lychee callback server +# --------------------------------------------------------------------------- + + +class _CallbackHandler(BaseHTTPRequestHandler): + received: ClassVar[list[bytes]] = [] + + def do_POST(self) -> None: + length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(length) + _CallbackHandler.received.append(body) + self.send_response(200) + self.end_headers() + self.wfile.write(b'{"faces": []}') + + def log_message(self, *args: object) -> None: # suppress request log noise + pass + + +# --------------------------------------------------------------------------- +# Smoke tests +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module", autouse=True) +def mock_lychee_server() -> None: + """Start a mock Lychee callback receiver before smoke tests.""" + server = HTTPServer(("0.0.0.0", MOCK_LYCHEE_PORT), _CallbackHandler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + yield # type: ignore[misc] + server.shutdown() + + +def _wait_for_health(timeout: int = 60) -> None: + deadline = time.time() + timeout + while time.time() < deadline: + try: + r = httpx.get(f"{SERVICE_URL}/health", timeout=3.0) + if r.status_code == 200 and r.json().get("model_loaded"): + return + except Exception: + pass + time.sleep(2) + pytest.fail(f"Service did not become healthy within {timeout}s") + + +def test_health_endpoint_responds() -> None: + """GET /health must return 200 with model_loaded: true.""" + _wait_for_health() + r = httpx.get(f"{SERVICE_URL}/health", timeout=10.0) + assert r.status_code == 200 + assert r.json()["model_loaded"] is True + assert r.json()["status"] == "ok" + + +def test_detect_returns_202() -> None: + """POST /detect must return 202 for a synthetic request.""" + _wait_for_health() + payload = {"photo_id": "smoke-photo-1", "photo_path": "/data/photos/smoke_placeholder.jpg"} + r = httpx.post( + f"{SERVICE_URL}/detect", + json=payload, + headers={"X-API-Key": API_KEY}, + timeout=10.0, + ) + # 202 or 400 (file not found) are both valid - what matters is the service responds + assert r.status_code in (202, 400) + + +def test_health_returns_embedding_count_as_int() -> None: + """embedding_count in health response must be a non-negative integer.""" + _wait_for_health() + r = httpx.get(f"{SERVICE_URL}/health", timeout=10.0) + assert isinstance(r.json()["embedding_count"], int) + assert r.json()["embedding_count"] >= 0 diff --git a/ai-vision-service/uv.lock b/ai-vision-service/uv.lock new file mode 100644 index 00000000000..1ec58c5bf01 --- /dev/null +++ b/ai-vision-service/uv.lock @@ -0,0 +1,1767 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "ai-vision-service" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "fastapi" }, + { name = "httpx" }, + { name = "insightface" }, + { name = "numpy" }, + { name = "onnxruntime" }, + { name = "opencv-python-headless" }, + { name = "pillow" }, + { name = "psycopg2-binary" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "scikit-learn" }, + { name = "sqlite-vec" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[package.optional-dependencies] +gpu = [ + { name = "onnxruntime-gpu" }, +] + +[package.dev-dependencies] +dev = [ + { name = "anyio" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "respx" }, + { name = "ruff" }, + { name = "ty" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.115" }, + { name = "httpx", specifier = ">=0.28" }, + { name = "insightface", specifier = ">=0.7" }, + { name = "numpy", specifier = ">=2.2" }, + { name = "onnxruntime", specifier = ">=1.20" }, + { name = "onnxruntime-gpu", marker = "extra == 'gpu'", specifier = ">=1.20" }, + { name = "opencv-python-headless", specifier = ">=4.10" }, + { name = "pillow", specifier = ">=11.0" }, + { name = "psycopg2-binary", specifier = ">=2.9" }, + { name = "pydantic", specifier = ">=2.10" }, + { name = "pydantic-settings", specifier = ">=2.7" }, + { name = "python-multipart", specifier = ">=0.0.20" }, + { name = "scikit-learn", specifier = ">=1.6" }, + { name = "sqlite-vec", specifier = ">=0.1" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.32" }, +] +provides-extras = ["gpu"] + +[package.metadata.requires-dev] +dev = [ + { name = "anyio", specifier = ">=4.7" }, + { name = "pytest", specifier = ">=8.3" }, + { name = "pytest-asyncio", specifier = ">=0.25" }, + { name = "pytest-cov", specifier = ">=6.0" }, + { name = "respx", specifier = ">=0.21" }, + { name = "ruff", specifier = ">=0.9" }, + { name = "ty", specifier = ">=0.0.1a10" }, +] + +[[package]] +name = "albucore" +version = "0.0.24" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "opencv-python-headless" }, + { name = "simsimd" }, + { name = "stringzilla" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/69/d4cbcf2a5768bf91cd14ffef783520458431e5d2b22fbc08418d3ba09a88/albucore-0.0.24.tar.gz", hash = "sha256:f2cab5431fadf94abf87fd0c89d9f59046e49fe5de34afea8f89bc8390253746", size = 16981, upload-time = "2025-03-09T18:46:51.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/e2/91f145e1f32428e9e1f21f46a7022ffe63d11f549ee55c3b9265ff5207fc/albucore-0.0.24-py3-none-any.whl", hash = "sha256:adef6e434e50e22c2ee127b7a3e71f2e35fa088bcf54431e18970b62d97d0005", size = 15372, upload-time = "2025-03-09T18:46:50.177Z" }, +] + +[[package]] +name = "albumentations" +version = "2.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "albucore" }, + { name = "numpy" }, + { name = "opencv-python-headless" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/f4/85eb56c3217b53bcfc2d12e840a0b18ca60902086321cafa5a730f9c0470/albumentations-2.0.8.tar.gz", hash = "sha256:4da95e658e490de3c34af8fcdffed09e36aa8a4edd06ca9f9e7e3ea0b0b16856", size = 354460, upload-time = "2025-05-27T21:23:17.415Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/64/013409c451a44b61310fb757af4527f3de57fc98a00f40448de28b864290/albumentations-2.0.8-py3-none-any.whl", hash = "sha256:c4c4259aaf04a7386ad85c7fdcb73c6c7146ca3057446b745cc035805acb1017", size = 369423, upload-time = "2025-05-27T21:23:15.609Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, + { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, + { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, + { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, + { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, + { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, + { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, + { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, + { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, + { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, + { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, + { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, + { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, + { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, + { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, + { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, + { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, + { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, + { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, + { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, + { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, + { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, + { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "cython" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/85/7574c9cd44b69a27210444b6650f6477f56c75fee1b70d7672d3e4166167/cython-3.2.4.tar.gz", hash = "sha256:84226ecd313b233da27dc2eb3601b4f222b8209c3a7216d8733b031da1dc64e6", size = 3280291, upload-time = "2026-01-04T14:14:14.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/b5/1cfca43b7d20a0fdb1eac67313d6bb6b18d18897f82dd0f17436bdd2ba7f/cython-3.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:28e8075087a59756f2d059273184b8b639fe0f16cf17470bd91c39921bc154e0", size = 2960506, upload-time = "2026-01-04T14:15:16.733Z" }, + { url = "https://files.pythonhosted.org/packages/71/bb/8f28c39c342621047fea349a82fac712a5e2b37546d2f737bbde48d5143d/cython-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03893c88299a2c868bb741ba6513357acd104e7c42265809fd58dce1456a36fc", size = 3213148, upload-time = "2026-01-04T14:15:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d2/16fa02f129ed2b627e88d9d9ebd5ade3eeb66392ae5ba85b259d2d52b047/cython-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f81eda419b5ada7b197bbc3c5f4494090e3884521ffd75a3876c93fbf66c9ca8", size = 3375764, upload-time = "2026-01-04T14:15:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/91/3f/deb8f023a5c10c0649eb81332a58c180fad27c7533bb4aae138b5bc34d92/cython-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:83266c356c13c68ffe658b4905279c993d8a5337bb0160fa90c8a3e297ea9a2e", size = 2754238, upload-time = "2026-01-04T14:15:23.001Z" }, + { url = "https://files.pythonhosted.org/packages/ee/d7/3bda3efce0c5c6ce79cc21285dbe6f60369c20364e112f5a506ee8a1b067/cython-3.2.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d4b4fd5332ab093131fa6172e8362f16adef3eac3179fd24bbdc392531cb82fa", size = 2971496, upload-time = "2026-01-04T14:15:25.038Z" }, + { url = "https://files.pythonhosted.org/packages/89/ed/1021ffc80b9c4720b7ba869aea8422c82c84245ef117ebe47a556bdc00c3/cython-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3b5ac54e95f034bc7fb07313996d27cbf71abc17b229b186c1540942d2dc28e", size = 3256146, upload-time = "2026-01-04T14:15:26.741Z" }, + { url = "https://files.pythonhosted.org/packages/0c/51/ca221ec7e94b3c5dc4138dcdcbd41178df1729c1e88c5dfb25f9d30ba3da/cython-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f43be4eaa6afd58ce20d970bb1657a3627c44e1760630b82aa256ba74b4acb", size = 3383458, upload-time = "2026-01-04T14:15:28.425Z" }, + { url = "https://files.pythonhosted.org/packages/79/2e/1388fc0243240cd54994bb74f26aaaf3b2e22f89d3a2cf8da06d75d46ca2/cython-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:983f9d2bb8a896e16fa68f2b37866ded35fa980195eefe62f764ddc5f9f5ef8e", size = 2791241, upload-time = "2026-01-04T14:15:30.448Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8b/fd393f0923c82be4ec0db712fffb2ff0a7a131707b842c99bf24b549274d/cython-3.2.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:36bf3f5eb56d5281aafabecbaa6ed288bc11db87547bba4e1e52943ae6961ccf", size = 2875622, upload-time = "2026-01-04T14:15:39.749Z" }, + { url = "https://files.pythonhosted.org/packages/73/48/48530d9b9d64ec11dbe0dd3178a5fe1e0b27977c1054ecffb82be81e9b6a/cython-3.2.4-cp39-abi3-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6d5267f22b6451eb1e2e1b88f6f78a2c9c8733a6ddefd4520d3968d26b824581", size = 3210669, upload-time = "2026-01-04T14:15:41.911Z" }, + { url = "https://files.pythonhosted.org/packages/5e/91/4865fbfef1f6bb4f21d79c46104a53d1a3fa4348286237e15eafb26e0828/cython-3.2.4-cp39-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3b6e58f73a69230218d5381817850ce6d0da5bb7e87eb7d528c7027cbba40b06", size = 2856835, upload-time = "2026-01-04T14:15:43.815Z" }, + { url = "https://files.pythonhosted.org/packages/fa/39/60317957dbef179572398253f29d28f75f94ab82d6d39ea3237fb6c89268/cython-3.2.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e71efb20048358a6b8ec604a0532961c50c067b5e63e345e2e359fff72feaee8", size = 2994408, upload-time = "2026-01-04T14:15:45.422Z" }, + { url = "https://files.pythonhosted.org/packages/8d/30/7c24d9292650db4abebce98abc9b49c820d40fa7c87921c0a84c32f4efe7/cython-3.2.4-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:28b1e363b024c4b8dcf52ff68125e635cb9cb4b0ba997d628f25e32543a71103", size = 2891478, upload-time = "2026-01-04T14:15:47.394Z" }, + { url = "https://files.pythonhosted.org/packages/86/70/03dc3c962cde9da37a93cca8360e576f904d5f9beecfc9d70b1f820d2e5f/cython-3.2.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:31a90b4a2c47bb6d56baeb926948348ec968e932c1ae2c53239164e3e8880ccf", size = 3225663, upload-time = "2026-01-04T14:15:49.446Z" }, + { url = "https://files.pythonhosted.org/packages/b1/97/10b50c38313c37b1300325e2e53f48ea9a2c078a85c0c9572057135e31d5/cython-3.2.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e65e4773021f8dc8532010b4fbebe782c77f9a0817e93886e518c93bd6a44e9d", size = 3115628, upload-time = "2026-01-04T14:15:51.323Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b1/d6a353c9b147848122a0db370863601fdf56de2d983b5c4a6a11e6ee3cd7/cython-3.2.4-cp39-abi3-win32.whl", hash = "sha256:2b1f12c0e4798293d2754e73cd6f35fa5bbdf072bdc14bc6fc442c059ef2d290", size = 2437463, upload-time = "2026-01-04T14:15:53.787Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d8/319a1263b9c33b71343adfd407e5daffd453daef47ebc7b642820a8b68ed/cython-3.2.4-cp39-abi3-win_arm64.whl", hash = "sha256:3b8e62049afef9da931d55de82d8f46c9a147313b69d5ff6af6e9121d545ce7a", size = 2442754, upload-time = "2026-01-04T14:15:55.382Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/d3c15189f7c52aaefbaea76fb012119b04b9013f4bf446cb4eb4c26c4e6b/cython-3.2.4-py3-none-any.whl", hash = "sha256:732fc93bc33ae4b14f6afaca663b916c2fdd5dcbfad7114e17fb2434eeaea45c", size = 1257078, upload-time = "2026-01-04T14:14:12.373Z" }, +] + +[[package]] +name = "easydict" +version = "1.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/9f/d18d6b5e19244788a6d09c14a8406376b4f4bfcc008e6d17a4f4c15362e8/easydict-1.13.tar.gz", hash = "sha256:b1135dedbc41c8010e2bc1f77ec9744c7faa42bce1a1c87416791449d6c87780", size = 6809, upload-time = "2024-03-04T12:04:41.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/ec/fa6963f1198172c2b75c9ab6ecefb3045991f92f75f5eb41b6621b198123/easydict-1.13-py3-none-any.whl", hash = "sha256:6b787daf4dcaf6377b4ad9403a5cee5a86adbc0ca9a5bcf5410e9902002aeac2", size = 6804, upload-time = "2024-03-04T12:04:39.508Z" }, +] + +[[package]] +name = "fastapi" +version = "0.135.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/7b/f8e0211e9380f7195ba3f3d40c292594fd81ba8ec4629e3854c353aaca45/fastapi-0.135.1.tar.gz", hash = "sha256:d04115b508d936d254cea545b7312ecaa58a7b3a0f84952535b4c9afae7668cd", size = 394962, upload-time = "2026-03-01T18:18:29.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/72/42e900510195b23a56bde950d26a51f8b723846bfcaa0286e90287f0422b/fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", size = 116999, upload-time = "2026-03-01T18:18:30.831Z" }, +] + +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, +] + +[[package]] +name = "fonttools" +version = "4.62.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79", size = 2865155, upload-time = "2026-03-13T13:53:16.132Z" }, + { url = "https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe", size = 2412802, upload-time = "2026-03-13T13:53:18.878Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/e6ac4b44026de7786fe46e3bfa0c87e51d5d70a841054065d49cd62bb909/fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68", size = 5013926, upload-time = "2026-03-13T13:53:21.379Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1", size = 4964575, upload-time = "2026-03-13T13:53:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/46/76/7d051671e938b1881670528fec69cc4044315edd71a229c7fd712eaa5119/fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069", size = 4953693, upload-time = "2026-03-13T13:53:26.569Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920, upload-time = "2026-03-13T13:53:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928, upload-time = "2026-03-13T13:53:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514, upload-time = "2026-03-13T13:53:34.991Z" }, + { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" }, + { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" }, + { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" }, + { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" }, + { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" }, + { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" }, + { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" }, + { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" }, + { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" }, + { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "imageio" +version = "2.37.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/84/93bcd1300216ea50811cee96873b84a1bebf8d0489ffaf7f2a3756bab866/imageio-2.37.3.tar.gz", hash = "sha256:bbb37efbfc4c400fcd534b367b91fcd66d5da639aaa138034431a1c5e0a41451", size = 389673, upload-time = "2026-03-09T11:31:12.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl", hash = "sha256:46f5bb8522cd421c0f5ae104d8268f569d856b29eb1a13b92829d1970f32c9f0", size = 317646, upload-time = "2026-03-09T11:31:10.771Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "insightface" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "albumentations" }, + { name = "cython" }, + { name = "easydict" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "onnx" }, + { name = "pillow" }, + { name = "prettytable" }, + { name = "requests" }, + { name = "scikit-image" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/8d/0f4af90999ca96cf8cb846eb5ae27c5ef5b390f9c090dd19e4fa76364c13/insightface-0.7.3.tar.gz", hash = "sha256:f191f719612ebb37018f41936814500544cd0f86e6fcd676c023f354c668ddf7", size = 439490, upload-time = "2023-04-02T08:01:54.541Z" } + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, + { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, + { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" }, + { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" }, + { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, + { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, + { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, + { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, + { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, +] + +[[package]] +name = "lazy-loader" +version = "0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/ac/21a1f8aa3777f5658576777ea76bfb124b702c520bbe90edf4ae9915eafa/lazy_loader-0.5.tar.gz", hash = "sha256:717f9179a0dbed357012ddad50a5ad3d5e4d9a0b8712680d4e687f5e6e6ed9b3", size = 15294, upload-time = "2026-03-06T15:45:09.054Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl", hash = "sha256:ab0ea149e9c554d4ffeeb21105ac60bed7f3b4fd69b1d2360a4add51b170b005", size = 8044, upload-time = "2026-03-06T15:45:07.668Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, + { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, + { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, + { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, + { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, + { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, +] + +[[package]] +name = "ml-dtypes" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48", size = 676888, upload-time = "2025-11-17T22:31:56.907Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b7/dff378afc2b0d5a7d6cd9d3209b60474d9819d1189d347521e1688a60a53/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b", size = 5036993, upload-time = "2025-11-17T22:31:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d", size = 5010956, upload-time = "2025-11-17T22:31:59.931Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328", size = 212224, upload-time = "2025-11-17T22:32:01.349Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/dfc3775cb36367816e678f69a7843f6f03bd4e2bcd79941e01ea960a068e/ml_dtypes-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175", size = 160798, upload-time = "2025-11-17T22:32:02.864Z" }, + { url = "https://files.pythonhosted.org/packages/4f/74/e9ddb35fd1dd43b1106c20ced3f53c2e8e7fc7598c15638e9f80677f81d4/ml_dtypes-0.5.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6", size = 702083, upload-time = "2025-11-17T22:32:04.08Z" }, + { url = "https://files.pythonhosted.org/packages/74/f5/667060b0aed1aa63166b22897fdf16dca9eb704e6b4bbf86848d5a181aa7/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d", size = 5354111, upload-time = "2025-11-17T22:32:05.546Z" }, + { url = "https://files.pythonhosted.org/packages/40/49/0f8c498a28c0efa5f5c95a9e374c83ec1385ca41d0e85e7cf40e5d519a21/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298", size = 5366453, upload-time = "2025-11-17T22:32:07.115Z" }, + { url = "https://files.pythonhosted.org/packages/8c/27/12607423d0a9c6bbbcc780ad19f1f6baa2b68b18ce4bddcdc122c4c68dc9/ml_dtypes-0.5.4-cp313-cp313t-win_amd64.whl", hash = "sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6", size = 225612, upload-time = "2025-11-17T22:32:08.615Z" }, + { url = "https://files.pythonhosted.org/packages/e5/80/5a5929e92c72936d5b19872c5fb8fc09327c1da67b3b68c6a13139e77e20/ml_dtypes-0.5.4-cp313-cp313t-win_arm64.whl", hash = "sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1", size = 164145, upload-time = "2025-11-17T22:32:09.782Z" }, + { url = "https://files.pythonhosted.org/packages/72/4e/1339dc6e2557a344f5ba5590872e80346f76f6cb2ac3dd16e4666e88818c/ml_dtypes-0.5.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22", size = 673781, upload-time = "2025-11-17T22:32:11.364Z" }, + { url = "https://files.pythonhosted.org/packages/04/f9/067b84365c7e83bda15bba2b06c6ca250ce27b20630b1128c435fb7a09aa/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465", size = 5036145, upload-time = "2025-11-17T22:32:12.783Z" }, + { url = "https://files.pythonhosted.org/packages/c6/bb/82c7dcf38070b46172a517e2334e665c5bf374a262f99a283ea454bece7c/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f", size = 5010230, upload-time = "2025-11-17T22:32:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e9/93/2bfed22d2498c468f6bcd0d9f56b033eaa19f33320389314c19ef6766413/ml_dtypes-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56", size = 221032, upload-time = "2025-11-17T22:32:15.763Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/9c912fe6ea747bb10fe2f8f54d027eb265db05dfb0c6335e3e063e74e6e8/ml_dtypes-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049", size = 163353, upload-time = "2025-11-17T22:32:16.932Z" }, + { url = "https://files.pythonhosted.org/packages/cd/02/48aa7d84cc30ab4ee37624a2fd98c56c02326785750cd212bc0826c2f15b/ml_dtypes-0.5.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9", size = 702085, upload-time = "2025-11-17T22:32:18.175Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e7/85cb99fe80a7a5513253ec7faa88a65306be071163485e9a626fce1b6e84/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7", size = 5355358, upload-time = "2025-11-17T22:32:19.7Z" }, + { url = "https://files.pythonhosted.org/packages/79/2b/a826ba18d2179a56e144aef69e57fb2ab7c464ef0b2111940ee8a3a223a2/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf", size = 5366332, upload-time = "2025-11-17T22:32:21.193Z" }, + { url = "https://files.pythonhosted.org/packages/84/44/f4d18446eacb20ea11e82f133ea8f86e2bf2891785b67d9da8d0ab0ef525/ml_dtypes-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1", size = 236612, upload-time = "2025-11-17T22:32:22.579Z" }, + { url = "https://files.pythonhosted.org/packages/ad/3f/3d42e9a78fe5edf792a83c074b13b9b770092a4fbf3462872f4303135f09/ml_dtypes-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d", size = 168825, upload-time = "2025-11-17T22:32:23.766Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" }, + { url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" }, + { url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" }, + { url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" }, + { url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" }, + { url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" }, + { url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" }, + { url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" }, + { url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" }, + { url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" }, + { url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" }, + { url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" }, + { url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" }, + { url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" }, + { url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" }, + { url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" }, + { url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" }, +] + +[[package]] +name = "onnx" +version = "1.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ml-dtypes" }, + { name = "numpy" }, + { name = "protobuf" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/8a/335c03a8683a88a32f9a6bb98899ea6df241a41df64b37b9696772414794/onnx-1.20.1.tar.gz", hash = "sha256:ded16de1df563d51fbc1ad885f2a426f814039d8b5f4feb77febe09c0295ad67", size = 12048980, upload-time = "2026-01-10T01:40:03.043Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/4c/4b17e82f91ab9aa07ff595771e935ca73547b035030dc5f5a76e63fbfea9/onnx-1.20.1-cp312-abi3-macosx_12_0_universal2.whl", hash = "sha256:1d923bb4f0ce1b24c6859222a7e6b2f123e7bfe7623683662805f2e7b9e95af2", size = 17903547, upload-time = "2026-01-10T01:39:31.015Z" }, + { url = "https://files.pythonhosted.org/packages/64/5e/1bfa100a9cb3f2d3d5f2f05f52f7e60323b0e20bb0abace1ae64dbc88f25/onnx-1.20.1-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddc0b7d8b5a94627dc86c533d5e415af94cbfd103019a582669dad1f56d30281", size = 17412021, upload-time = "2026-01-10T01:39:33.885Z" }, + { url = "https://files.pythonhosted.org/packages/fb/71/d3fec0dcf9a7a99e7368112d9c765154e81da70fcba1e3121131a45c245b/onnx-1.20.1-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9336b6b8e6efcf5c490a845f6afd7e041c89a56199aeda384ed7d58fb953b080", size = 17510450, upload-time = "2026-01-10T01:39:36.589Z" }, + { url = "https://files.pythonhosted.org/packages/74/a7/edce1403e05a46e59b502fae8e3350ceeac5841f8e8f1561e98562ed9b09/onnx-1.20.1-cp312-abi3-win32.whl", hash = "sha256:564c35a94811979808ab5800d9eb4f3f32c12daedba7e33ed0845f7c61ef2431", size = 16238216, upload-time = "2026-01-10T01:39:39.46Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c7/8690c81200ae652ac550c1df52f89d7795e6cc941f3cb38c9ef821419e80/onnx-1.20.1-cp312-abi3-win_amd64.whl", hash = "sha256:9fe7f9a633979d50984b94bda8ceb7807403f59a341d09d19342dc544d0ca1d5", size = 16389207, upload-time = "2026-01-10T01:39:41.955Z" }, + { url = "https://files.pythonhosted.org/packages/01/a0/4fb0e6d36eaf079af366b2c1f68bafe92df6db963e2295da84388af64abc/onnx-1.20.1-cp312-abi3-win_arm64.whl", hash = "sha256:21d747348b1c8207406fa2f3e12b82f53e0d5bb3958bcd0288bd27d3cb6ebb00", size = 16344155, upload-time = "2026-01-10T01:39:45.536Z" }, + { url = "https://files.pythonhosted.org/packages/ea/bb/715fad292b255664f0e603f1b2ef7bf2b386281775f37406beb99fa05957/onnx-1.20.1-cp313-cp313t-macosx_12_0_universal2.whl", hash = "sha256:29197b768f5acdd1568ddeb0a376407a2817844f6ac1ef8c8dd2d974c9ab27c3", size = 17912296, upload-time = "2026-01-10T01:39:48.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c3/541af12c3d45e159a94ee701100ba9e94b7bd8b7a8ac5ca6838569f894f8/onnx-1.20.1-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f0371aa67f51917a09cc829ada0f9a79a58f833449e03d748f7f7f53787c43c", size = 17416925, upload-time = "2026-01-10T01:39:50.82Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/d5660a7d2ddf14f531ca66d409239f543bb290277c3f14f4b4b78e32efa3/onnx-1.20.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be1e5522200b203b34327b2cf132ddec20ab063469476e1f5b02bb7bd259a489", size = 17515602, upload-time = "2026-01-10T01:39:54.132Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b4/47225ab2a92562eff87ba9a1a028e3535d659a7157d7cde659003998b8e3/onnx-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:15c815313bbc4b2fdc7e4daeb6e26b6012012adc4d850f4e3b09ed327a7ea92a", size = 16395729, upload-time = "2026-01-10T01:39:57.577Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7d/1bbe626ff6b192c844d3ad34356840cc60fca02e2dea0db95e01645758b1/onnx-1.20.1-cp313-cp313t-win_arm64.whl", hash = "sha256:eb335d7bcf9abac82a0d6a0fda0363531ae0b22cfd0fc6304bff32ee29905def", size = 16348968, upload-time = "2026-01-10T01:40:00.491Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.24.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flatbuffers" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "sympy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/f0/8a21ec0a97e40abb7d8da1e8b20fb9e1af509cc6d191f6faa75f73622fb2/onnxruntime-1.24.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e99a48078baaefa2b50fe5836c319499f71f13f76ed32d0211f39109147a49e0", size = 17341922, upload-time = "2026-03-17T22:03:56.364Z" }, + { url = "https://files.pythonhosted.org/packages/8b/25/d7908de8e08cee9abfa15b8aa82349b79733ae5865162a3609c11598805d/onnxruntime-1.24.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4aaed1e5e1aaacf2343c838a30a7c3ade78f13eeb16817411f929d04040a13", size = 15172290, upload-time = "2026-03-17T22:03:37.124Z" }, + { url = "https://files.pythonhosted.org/packages/7f/72/105ec27a78c5aa0154a7c0cd8c41c19a97799c3b12fc30392928997e3be3/onnxruntime-1.24.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e30c972bc02e072911aabb6891453ec73795386c0af2b761b65444b8a4c4745f", size = 17244738, upload-time = "2026-03-17T22:04:40.625Z" }, + { url = "https://files.pythonhosted.org/packages/05/fb/a592736d968c2f58e12de4d52088dda8e0e724b26ad5c0487263adb45875/onnxruntime-1.24.4-cp313-cp313-win_amd64.whl", hash = "sha256:3b6ba8b0181a3aa88edab00eb01424ffc06f42e71095a91186c2249415fcff93", size = 12597435, upload-time = "2026-03-17T22:05:43.826Z" }, + { url = "https://files.pythonhosted.org/packages/ad/04/ae2479e9841b64bd2eb44f8a64756c62593f896514369a11243b1b86ca5c/onnxruntime-1.24.4-cp313-cp313-win_arm64.whl", hash = "sha256:71d6a5c1821d6e8586a024000ece458db8f2fc0ecd050435d45794827ce81e19", size = 12269852, upload-time = "2026-03-17T22:05:33.353Z" }, + { url = "https://files.pythonhosted.org/packages/b4/af/a479a536c4398ffaf49fbbe755f45d5b8726bdb4335ab31b537f3d7149b8/onnxruntime-1.24.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1700f559c8086d06b2a4d5de51e62cb4ff5e2631822f71a36db8c72383db71ee", size = 15176861, upload-time = "2026-03-17T22:03:40.143Z" }, + { url = "https://files.pythonhosted.org/packages/be/13/19f5da70c346a76037da2c2851ecbf1266e61d7f0dcdb887c667210d4608/onnxruntime-1.24.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c74e268dc808e61e63784d43f9ddcdaf50a776c2819e8bd1d1b11ef64bf7e36", size = 17247454, upload-time = "2026-03-17T22:04:46.643Z" }, + { url = "https://files.pythonhosted.org/packages/89/db/b30dbbd6037847b205ab75d962bc349bf1e46d02a65b30d7047a6893ffd6/onnxruntime-1.24.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:fbff2a248940e3398ae78374c5a839e49a2f39079b488bc64439fa0ec327a3e4", size = 17343300, upload-time = "2026-03-17T22:03:59.223Z" }, + { url = "https://files.pythonhosted.org/packages/61/88/1746c0e7959961475b84c776d35601a21d445f463c93b1433a409ec3e188/onnxruntime-1.24.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2b7969e72d8cb53ffc88ab6d49dd5e75c1c663bda7be7eb0ece192f127343d1", size = 15175936, upload-time = "2026-03-17T22:03:43.671Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ba/4699cde04a52cece66cbebc85bd8335a0d3b9ad485abc9a2e15946a1349d/onnxruntime-1.24.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14ed1f197fab812b695a5eaddb536c635e58a2fbbe50a517c78f082cc6ce9177", size = 17246432, upload-time = "2026-03-17T22:04:49.58Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/4590910841bb28bd3b4b388a9efbedf4e2d2cca99ddf0c863642b4e87814/onnxruntime-1.24.4-cp314-cp314-win_amd64.whl", hash = "sha256:311e309f573bf3c12aa5723e23823077f83d5e412a18499d4485c7eb41040858", size = 12903276, upload-time = "2026-03-17T22:05:46.349Z" }, + { url = "https://files.pythonhosted.org/packages/7f/6f/60e2c0acea1e1ac09b3e794b5a19c166eebf91c0b860b3e6db8e74983fda/onnxruntime-1.24.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f0b910e86b759a4732663ec61fd57ac42ee1b0066f68299de164220b660546d", size = 12594365, upload-time = "2026-03-17T22:05:35.795Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/0c05d10f8f6c40fe0912ebec0d5a33884aaa2af2053507e864dab0883208/onnxruntime-1.24.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa12ddc54c9c4594073abcaa265cd9681e95fb89dae982a6f508a794ca42e661", size = 15176889, upload-time = "2026-03-17T22:03:48.021Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1d/1666dc64e78d8587d168fec4e3b7922b92eb286a2ddeebcf6acb55c7dc82/onnxruntime-1.24.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1cc6a518255f012134bc791975a6294806be9a3b20c4a54cca25194c90cf731", size = 17247021, upload-time = "2026-03-17T22:04:52.377Z" }, +] + +[[package]] +name = "onnxruntime-gpu" +version = "1.24.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flatbuffers" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "sympy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/4e/56d11203d7a35e7d6a5ea735f5fecb8673537038c07323e8d3090a896547/onnxruntime_gpu-1.24.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbdaa73f9055fb2a177425edbed651a1843a6239f9d5430e284f4e5f65440a33", size = 252763446, upload-time = "2026-03-17T22:04:09.515Z" }, + { url = "https://files.pythonhosted.org/packages/fa/bc/35f3a37226d7a28c84b8b456f52237ccd39eb7111114bcf9ac340178e1ec/onnxruntime_gpu-1.24.4-cp313-cp313-win_amd64.whl", hash = "sha256:6be8bf2048777c517fca33eb61e114969fa326619feaa789d8c75f24337ea762", size = 207198775, upload-time = "2026-03-17T21:58:48.768Z" }, + { url = "https://files.pythonhosted.org/packages/37/83/0c851882051b38f245f44b4a51d6232b95b8cd5d334b2c1260f2d796834f/onnxruntime_gpu-1.24.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4b348a078ced73fc577d21b83992fd2187edd10c233729c8d01b000b8543525", size = 252774594, upload-time = "2026-03-17T22:04:24.957Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5b/82b27f766b64f97c9a98b772dc07b608e900bd2faafdfa176b86d20be7f8/onnxruntime_gpu-1.24.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af9dd7ef92d94c75e5523cf070e180f3d8cdbb2fc007dcea97ba71b03e3b96d6", size = 252765395, upload-time = "2026-03-17T22:04:37.305Z" }, + { url = "https://files.pythonhosted.org/packages/5d/95/fa8c48e03790c979167d08164b34a8442c7074bca4c7253b4455497025de/onnxruntime_gpu-1.24.4-cp314-cp314-win_amd64.whl", hash = "sha256:4dde3d2f1039060c42b12fd446fc0da5b836cc65dceb4020ca60a04cffa1d90d", size = 209597109, upload-time = "2026-03-17T21:58:58.136Z" }, + { url = "https://files.pythonhosted.org/packages/1a/98/7707edefcecf69d6c45b83a83f13ac58257017b4eaf58772668d302f849f/onnxruntime_gpu-1.24.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:097c6f53e99ee35f21d0fdba76ca283b92465a0e364c6f0209cb9653c424e2a4", size = 252776951, upload-time = "2026-03-17T22:04:49.715Z" }, +] + +[[package]] +name = "opencv-python-headless" +version = "4.13.0.92" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/42/2310883be3b8826ac58c3f2787b9358a2d46923d61f88fedf930bc59c60c/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:1a7d040ac656c11b8c38677cc8cccdc149f98535089dbe5b081e80a4e5903209", size = 46247192, upload-time = "2026-02-05T07:01:35.187Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1e/6f9e38005a6f7f22af785df42a43139d0e20f169eb5787ce8be37ee7fcc9/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:3e0a6f0a37994ec6ce5f59e936be21d5d6384a4556f2d2da9c2f9c5dc948394c", size = 32568914, upload-time = "2026-02-05T07:01:51.989Z" }, + { url = "https://files.pythonhosted.org/packages/21/76/9417a6aef9def70e467a5bf560579f816148a4c658b7d525581b356eda9e/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c8cfc8e87ed452b5cecb9419473ee5560a989859fe1d10d1ce11ae87b09a2cb", size = 33703709, upload-time = "2026-02-05T10:24:46.469Z" }, + { url = "https://files.pythonhosted.org/packages/92/ce/bd17ff5772938267fd49716e94ca24f616ff4cb1ff4c6be13085108037be/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0525a3d2c0b46c611e2130b5fdebc94cf404845d8fa64d2f3a3b679572a5bd22", size = 56016764, upload-time = "2026-02-05T10:26:48.904Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b4/b7bcbf7c874665825a8c8e1097e93ea25d1f1d210a3e20d4451d01da30aa/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb60e36b237b1ebd40a912da5384b348df8ed534f6f644d8e0b4f103e272ba7d", size = 35010236, upload-time = "2026-02-05T10:28:11.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/33/b5db29a6c00eb8f50708110d8d453747ca125c8b805bc437b289dbdcc057/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0bd48544f77c68b2941392fcdf9bcd2b9cdf00e98cb8c29b2455d194763cf99e", size = 60391106, upload-time = "2026-02-05T10:30:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c3/52cfea47cd33e53e8c0fbd6e7c800b457245c1fda7d61660b4ffe9596a7f/opencv_python_headless-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:a7cf08e5b191f4ebb530791acc0825a7986e0d0dee2a3c491184bd8599848a4b", size = 30812232, upload-time = "2026-02-05T07:02:29.594Z" }, + { url = "https://files.pythonhosted.org/packages/4a/90/b338326131ccb2aaa3c2c85d00f41822c0050139a4bfe723cfd95455bd2d/opencv_python_headless-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:77a82fe35ddcec0f62c15f2ba8a12ecc2ed4207c17b0902c7a3151ae29f37fb6", size = 40070414, upload-time = "2026-02-05T07:02:26.448Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pillow" +version = "12.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, + { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, + { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, + { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, + { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, + { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, + { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, + { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, + { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, + { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, + { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "prettytable" +version = "3.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/45/b0847d88d6cfeb4413566738c8bbf1e1995fad3d42515327ff32cc1eb578/prettytable-3.17.0.tar.gz", hash = "sha256:59f2590776527f3c9e8cf9fe7b66dd215837cca96a9c39567414cbc632e8ddb0", size = 67892, upload-time = "2025-11-14T17:33:20.212Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl", hash = "sha256:aad69b294ddbe3e1f95ef8886a060ed1666a0b83018bbf56295f6f226c43d287", size = 34433, upload-time = "2025-11-14T17:33:19.093Z" }, +] + +[[package]] +name = "protobuf" +version = "7.34.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/6b/a0e95cad1ad7cc3f2c6821fcab91671bd5b78bd42afb357bb4765f29bc41/protobuf-7.34.1.tar.gz", hash = "sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280", size = 454708, upload-time = "2026-03-20T17:34:47.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/11/3325d41e6ee15bf1125654301211247b042563bcc898784351252549a8ad/protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7", size = 429247, upload-time = "2026-03-20T17:34:37.024Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9d/aa69df2724ff63efa6f72307b483ce0827f4347cc6d6df24b59e26659fef/protobuf-7.34.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b", size = 325753, upload-time = "2026-03-20T17:34:38.751Z" }, + { url = "https://files.pythonhosted.org/packages/92/e8/d174c91fd48e50101943f042b09af9029064810b734e4160bbe282fa1caa/protobuf-7.34.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a", size = 340198, upload-time = "2026-03-20T17:34:39.871Z" }, + { url = "https://files.pythonhosted.org/packages/53/1b/3b431694a4dc6d37b9f653f0c64b0a0d9ec074ee810710c0c3da21d67ba7/protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4", size = 324267, upload-time = "2026-03-20T17:34:41.1Z" }, + { url = "https://files.pythonhosted.org/packages/85/29/64de04a0ac142fb685fd09999bc3d337943fb386f3a0ec57f92fd8203f97/protobuf-7.34.1-cp310-abi3-win32.whl", hash = "sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a", size = 426628, upload-time = "2026-03-20T17:34:42.536Z" }, + { url = "https://files.pythonhosted.org/packages/4d/87/cb5e585192a22b8bd457df5a2c16a75ea0db9674c3a0a39fc9347d84e075/protobuf-7.34.1-cp310-abi3-win_amd64.whl", hash = "sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c", size = 437901, upload-time = "2026-03-20T17:34:44.112Z" }, + { url = "https://files.pythonhosted.org/packages/88/95/608f665226bca68b736b79e457fded9a2a38c4f4379a4a7614303d9db3bc/protobuf-7.34.1-py3-none-any.whl", hash = "sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11", size = 170715, upload-time = "2026-03-20T17:34:45.384Z" }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" }, + { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" }, + { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" }, + { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" }, + { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" }, + { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "respx" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/7c/96bd0bc759cf009675ad1ee1f96535edcb11e9666b985717eb8c87192a95/respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91", size = 28439, upload-time = "2024-12-19T22:33:59.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127, upload-time = "2024-12-19T22:33:57.837Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/22/9e4f66ee588588dc6c9af6a994e12d26e19efbe874d1a909d09a6dac7a59/ruff-0.15.7.tar.gz", hash = "sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac", size = 4601277, upload-time = "2026-03-19T16:26:22.605Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/2f/0b08ced94412af091807b6119ca03755d651d3d93a242682bf020189db94/ruff-0.15.7-py3-none-linux_armv6l.whl", hash = "sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e", size = 10489037, upload-time = "2026-03-19T16:26:32.47Z" }, + { url = "https://files.pythonhosted.org/packages/91/4a/82e0fa632e5c8b1eba5ee86ecd929e8ff327bbdbfb3c6ac5d81631bef605/ruff-0.15.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:722d165bd52403f3bdabc0ce9e41fc47070ac56d7a91b4e0d097b516a53a3477", size = 10955433, upload-time = "2026-03-19T16:27:00.205Z" }, + { url = "https://files.pythonhosted.org/packages/ab/10/12586735d0ff42526ad78c049bf51d7428618c8b5c467e72508c694119df/ruff-0.15.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e", size = 10269302, upload-time = "2026-03-19T16:26:26.183Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5d/32b5c44ccf149a26623671df49cbfbd0a0ae511ff3df9d9d2426966a8d57/ruff-0.15.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf", size = 10607625, upload-time = "2026-03-19T16:27:03.263Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f1/f0001cabe86173aaacb6eb9bb734aa0605f9a6aa6fa7d43cb49cbc4af9c9/ruff-0.15.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85", size = 10324743, upload-time = "2026-03-19T16:27:09.791Z" }, + { url = "https://files.pythonhosted.org/packages/7a/87/b8a8f3d56b8d848008559e7c9d8bf367934d5367f6d932ba779456e2f73b/ruff-0.15.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb0511670002c6c529ec66c0e30641c976c8963de26a113f3a30456b702468b0", size = 11138536, upload-time = "2026-03-19T16:27:06.101Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f2/4fd0d05aab0c5934b2e1464784f85ba2eab9d54bffc53fb5430d1ed8b829/ruff-0.15.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912", size = 11994292, upload-time = "2026-03-19T16:26:48.718Z" }, + { url = "https://files.pythonhosted.org/packages/64/22/fc4483871e767e5e95d1622ad83dad5ebb830f762ed0420fde7dfa9d9b08/ruff-0.15.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036", size = 11398981, upload-time = "2026-03-19T16:26:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/b0/99/66f0343176d5eab02c3f7fcd2de7a8e0dd7a41f0d982bee56cd1c24db62b/ruff-0.15.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5", size = 11242422, upload-time = "2026-03-19T16:26:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/5d/3a/a7060f145bfdcce4c987ea27788b30c60e2c81d6e9a65157ca8afe646328/ruff-0.15.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12", size = 11232158, upload-time = "2026-03-19T16:26:42.321Z" }, + { url = "https://files.pythonhosted.org/packages/a7/53/90fbb9e08b29c048c403558d3cdd0adf2668b02ce9d50602452e187cd4af/ruff-0.15.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c", size = 10577861, upload-time = "2026-03-19T16:26:57.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/aa/5f486226538fe4d0f0439e2da1716e1acf895e2a232b26f2459c55f8ddad/ruff-0.15.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4", size = 10327310, upload-time = "2026-03-19T16:26:35.909Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/271afdffb81fe7bfc8c43ba079e9d96238f674380099457a74ccb3863857/ruff-0.15.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b4705e0e85cedc74b0a23cf6a179dbb3df184cb227761979cc76c0440b5ab0d", size = 10840752, upload-time = "2026-03-19T16:26:45.723Z" }, + { url = "https://files.pythonhosted.org/packages/bf/29/a4ae78394f76c7759953c47884eb44de271b03a66634148d9f7d11e721bd/ruff-0.15.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580", size = 11336961, upload-time = "2026-03-19T16:26:39.076Z" }, + { url = "https://files.pythonhosted.org/packages/26/6b/8786ba5736562220d588a2f6653e6c17e90c59ced34a2d7b512ef8956103/ruff-0.15.7-py3-none-win32.whl", hash = "sha256:6d39e2d3505b082323352f733599f28169d12e891f7dd407f2d4f54b4c2886de", size = 10582538, upload-time = "2026-03-19T16:26:15.992Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e9/346d4d3fffc6871125e877dae8d9a1966b254fbd92a50f8561078b88b099/ruff-0.15.7-py3-none-win_amd64.whl", hash = "sha256:4d53d712ddebcd7dace1bc395367aec12c057aacfe9adbb6d832302575f4d3a1", size = 11755839, upload-time = "2026-03-19T16:26:19.897Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304, upload-time = "2026-03-19T16:26:51.669Z" }, +] + +[[package]] +name = "scikit-image" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "imageio" }, + { name = "lazy-loader" }, + { name = "networkx" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "scipy" }, + { name = "tifffile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/b4/2528bb43c67d48053a7a649a9666432dc307d66ba02e3a6d5c40f46655df/scikit_image-0.26.0.tar.gz", hash = "sha256:f5f970ab04efad85c24714321fcc91613fcb64ef2a892a13167df2f3e59199fa", size = 22729739, upload-time = "2025-12-20T17:12:21.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/48/02357ffb2cca35640f33f2cfe054a4d6d5d7a229b88880a64f1e45c11f4e/scikit_image-0.26.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a2e852eccf41d2d322b8e60144e124802873a92b8d43a6f96331aa42888491c7", size = 12346329, upload-time = "2025-12-20T17:11:11.599Z" }, + { url = "https://files.pythonhosted.org/packages/67/b9/b792c577cea2c1e94cda83b135a656924fc57c428e8a6d302cd69aac1b60/scikit_image-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:98329aab3bc87db352b9887f64ce8cdb8e75f7c2daa19927f2e121b797b678d5", size = 12031726, upload-time = "2025-12-20T17:11:13.871Z" }, + { url = "https://files.pythonhosted.org/packages/07/a9/9564250dfd65cb20404a611016db52afc6268b2b371cd19c7538ea47580f/scikit_image-0.26.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:915bb3ba66455cf8adac00dc8fdf18a4cd29656aec7ddd38cb4dda90289a6f21", size = 13094910, upload-time = "2025-12-20T17:11:16.2Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b8/0d8eeb5a9fd7d34ba84f8a55753a0a3e2b5b51b2a5a0ade648a8db4a62f7/scikit_image-0.26.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b36ab5e778bf50af5ff386c3ac508027dc3aaeccf2161bdf96bde6848f44d21b", size = 13660939, upload-time = "2025-12-20T17:11:18.464Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d6/91d8973584d4793d4c1a847d388e34ef1218d835eeddecfc9108d735b467/scikit_image-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:09bad6a5d5949c7896c8347424c4cca899f1d11668030e5548813ab9c2865dcb", size = 14138938, upload-time = "2025-12-20T17:11:20.919Z" }, + { url = "https://files.pythonhosted.org/packages/39/9a/7e15d8dc10d6bbf212195fb39bdeb7f226c46dd53f9c63c312e111e2e175/scikit_image-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:aeb14db1ed09ad4bee4ceb9e635547a8d5f3549be67fc6c768c7f923e027e6cd", size = 14752243, upload-time = "2025-12-20T17:11:23.347Z" }, + { url = "https://files.pythonhosted.org/packages/8f/58/2b11b933097bc427e42b4a8b15f7de8f24f2bac1fd2779d2aea1431b2c31/scikit_image-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:ac529eb9dbd5954f9aaa2e3fe9a3fd9661bfe24e134c688587d811a0233127f1", size = 11906770, upload-time = "2025-12-20T17:11:25.297Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ec/96941474a18a04b69b6f6562a5bd79bd68049fa3728d3b350976eccb8b93/scikit_image-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:a2d211bc355f59725efdcae699b93b30348a19416cc9e017f7b2fb599faf7219", size = 11342506, upload-time = "2025-12-20T17:11:27.399Z" }, + { url = "https://files.pythonhosted.org/packages/03/e5/c1a9962b0cf1952f42d32b4a2e48eed520320dbc4d2ff0b981c6fa508b6b/scikit_image-0.26.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9eefb4adad066da408a7601c4c24b07af3b472d90e08c3e7483d4e9e829d8c49", size = 12663278, upload-time = "2025-12-20T17:11:29.358Z" }, + { url = "https://files.pythonhosted.org/packages/ae/97/c1a276a59ce8e4e24482d65c1a3940d69c6b3873279193b7ebd04e5ee56b/scikit_image-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6caec76e16c970c528d15d1c757363334d5cb3069f9cea93d2bead31820511f3", size = 12405142, upload-time = "2025-12-20T17:11:31.282Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4a/f1cbd1357caef6c7993f7efd514d6e53d8fd6f7fe01c4714d51614c53289/scikit_image-0.26.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a07200fe09b9d99fcdab959859fe0f7db8df6333d6204344425d476850ce3604", size = 12942086, upload-time = "2025-12-20T17:11:33.683Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6f/74d9fb87c5655bd64cf00b0c44dc3d6206d9002e5f6ba1c9aeb13236f6bf/scikit_image-0.26.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92242351bccf391fc5df2d1529d15470019496d2498d615beb68da85fe7fdf37", size = 13265667, upload-time = "2025-12-20T17:11:36.11Z" }, + { url = "https://files.pythonhosted.org/packages/a7/73/faddc2413ae98d863f6fa2e3e14da4467dd38e788e1c23346cf1a2b06b97/scikit_image-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:52c496f75a7e45844d951557f13c08c81487c6a1da2e3c9c8a39fcde958e02cc", size = 14001966, upload-time = "2025-12-20T17:11:38.55Z" }, + { url = "https://files.pythonhosted.org/packages/02/94/9f46966fa042b5d57c8cd641045372b4e0df0047dd400e77ea9952674110/scikit_image-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:20ef4a155e2e78b8ab973998e04d8a361d49d719e65412405f4dadd9155a61d9", size = 14359526, upload-time = "2025-12-20T17:11:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b4/2840fe38f10057f40b1c9f8fb98a187a370936bf144a4ac23452c5ef1baf/scikit_image-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c9087cf7d0e7f33ab5c46d2068d86d785e70b05400a891f73a13400f1e1faf6a", size = 12287629, upload-time = "2025-12-20T17:11:43.11Z" }, + { url = "https://files.pythonhosted.org/packages/22/ba/73b6ca70796e71f83ab222690e35a79612f0117e5aaf167151b7d46f5f2c/scikit_image-0.26.0-cp313-cp313t-win_arm64.whl", hash = "sha256:27d58bc8b2acd351f972c6508c1b557cfed80299826080a4d803dd29c51b707e", size = 11647755, upload-time = "2025-12-20T17:11:45.279Z" }, + { url = "https://files.pythonhosted.org/packages/51/44/6b744f92b37ae2833fd423cce8f806d2368859ec325a699dc30389e090b9/scikit_image-0.26.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:63af3d3a26125f796f01052052f86806da5b5e54c6abef152edb752683075a9c", size = 12365810, upload-time = "2025-12-20T17:11:47.357Z" }, + { url = "https://files.pythonhosted.org/packages/40/f5/83590d9355191f86ac663420fec741b82cc547a4afe7c4c1d986bf46e4db/scikit_image-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ce00600cd70d4562ed59f80523e18cdcc1fae0e10676498a01f73c255774aefd", size = 12075717, upload-time = "2025-12-20T17:11:49.483Z" }, + { url = "https://files.pythonhosted.org/packages/72/48/253e7cf5aee6190459fe136c614e2cbccc562deceb4af96e0863f1b8ee29/scikit_image-0.26.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6381edf972b32e4f54085449afde64365a57316637496c1325a736987083e2ab", size = 13161520, upload-time = "2025-12-20T17:11:51.58Z" }, + { url = "https://files.pythonhosted.org/packages/73/c3/cec6a3cbaadfdcc02bd6ff02f3abfe09eaa7f4d4e0a525a1e3a3f4bce49c/scikit_image-0.26.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6624a76c6085218248154cc7e1500e6b488edcd9499004dd0d35040607d7505", size = 13684340, upload-time = "2025-12-20T17:11:53.708Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0d/39a776f675d24164b3a267aa0db9f677a4cb20127660d8bf4fd7fef66817/scikit_image-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f775f0e420faac9c2aa6757135f4eb468fb7b70e0b67fa77a5e79be3c30ee331", size = 14203839, upload-time = "2025-12-20T17:11:55.89Z" }, + { url = "https://files.pythonhosted.org/packages/ee/25/2514df226bbcedfe9b2caafa1ba7bc87231a0c339066981b182b08340e06/scikit_image-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede4d6d255cc5da9faeb2f9ba7fedbc990abbc652db429f40a16b22e770bb578", size = 14770021, upload-time = "2025-12-20T17:11:58.014Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5b/0671dc91c0c79340c3fe202f0549c7d3681eb7640fe34ab68a5f090a7c7f/scikit_image-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:0660b83968c15293fd9135e8d860053ee19500d52bf55ca4fb09de595a1af650", size = 12023490, upload-time = "2025-12-20T17:12:00.013Z" }, + { url = "https://files.pythonhosted.org/packages/65/08/7c4cb59f91721f3de07719085212a0b3962e3e3f2d1818cbac4eeb1ea53e/scikit_image-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:b8d14d3181c21c11170477a42542c1addc7072a90b986675a71266ad17abc37f", size = 11473782, upload-time = "2025-12-20T17:12:01.983Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/65c4258137acef3d73cb561ac55512eacd7b30bb4f4a11474cad526bc5db/scikit_image-0.26.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cde0bbd57e6795eba83cb10f71a677f7239271121dc950bc060482834a668ad1", size = 12686060, upload-time = "2025-12-20T17:12:03.886Z" }, + { url = "https://files.pythonhosted.org/packages/e7/32/76971f8727b87f1420a962406388a50e26667c31756126444baf6668f559/scikit_image-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:163e9afb5b879562b9aeda0dd45208a35316f26cc7a3aed54fd601604e5cf46f", size = 12422628, upload-time = "2025-12-20T17:12:05.921Z" }, + { url = "https://files.pythonhosted.org/packages/37/0d/996febd39f757c40ee7b01cdb861867327e5c8e5f595a634e8201462d958/scikit_image-0.26.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724f79fd9b6cb6f4a37864fe09f81f9f5d5b9646b6868109e1b100d1a7019e59", size = 12962369, upload-time = "2025-12-20T17:12:07.912Z" }, + { url = "https://files.pythonhosted.org/packages/48/b4/612d354f946c9600e7dea012723c11d47e8d455384e530f6daaaeb9bf62c/scikit_image-0.26.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3268f13310e6857508bd87202620df996199a016a1d281b309441d227c822394", size = 13272431, upload-time = "2025-12-20T17:12:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/26c00b466e06055a086de2c6e2145fe189ccdc9a1d11ccc7de020f2591ad/scikit_image-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fac96a1f9b06cd771cbbb3cd96c5332f36d4efd839b1d8b053f79e5887acde62", size = 14016362, upload-time = "2025-12-20T17:12:12.793Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/00a90402e1775634043c2a0af8a3c76ad450866d9fa444efcc43b553ba2d/scikit_image-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c1e7bd342f43e7a97e571b3f03ba4c1293ea1a35c3f13f41efdc8a81c1dc8f2", size = 14364151, upload-time = "2025-12-20T17:12:14.909Z" }, + { url = "https://files.pythonhosted.org/packages/da/ca/918d8d306bd43beacff3b835c6d96fac0ae64c0857092f068b88db531a7c/scikit_image-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b702c3bb115e1dcf4abf5297429b5c90f2189655888cbed14921f3d26f81d3a4", size = 12413484, upload-time = "2025-12-20T17:12:17.046Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cd/4da01329b5a8d47ff7ec3c99a2b02465a8017b186027590dc7425cee0b56/scikit_image-0.26.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0608aa4a9ec39e0843de10d60edb2785a30c1c47819b67866dd223ebd149acaf", size = 11769501, upload-time = "2025-12-20T17:12:19.339Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" }, + { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" }, + { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, + { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, + { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "simsimd" +version = "6.5.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/8c/070a179eb509b689509dacbd0bc81aa2e36614aff2c8aa6dc6c440886206/simsimd-6.5.16.tar.gz", hash = "sha256:0a005c6e2dacec83f235a747f7dbecca46b5d4d1e183ecc1929ca556ee7d7564", size = 187216, upload-time = "2026-03-07T14:36:23.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/f2/e1dedb4b3644c76467c84ffb57fc6e7784f46f312c34be9d6b52144e3d90/simsimd-6.5.16-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0af914ab13741744ea1bd3521e719226633f2ab082dc5b07790c61685d88558", size = 105157, upload-time = "2026-03-07T14:35:04.455Z" }, + { url = "https://files.pythonhosted.org/packages/46/21/a52af2040ad608cc236583ada58b0bfa5ffbfdc83b1d3565f4793f28cade/simsimd-6.5.16-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:683f758d0261b3d8790f8c9fc63fdc64b7af4db66b59ba7a31556a755cb38df7", size = 94604, upload-time = "2026-03-07T14:35:05.982Z" }, + { url = "https://files.pythonhosted.org/packages/e1/39/c6c7f66368204f0aa544aa074fa84b42a4146cf9e4bc79c3896c155d9abc/simsimd-6.5.16-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc1e29d8fed1c2b89338062fa17283b78181c84d2b024cc9bf7ed75402810bfc", size = 385102, upload-time = "2026-03-07T14:35:07.32Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f2/6d84388c6e0f0637321149bc84bbbfa54a12f65f29bc6a007dd1403bf6f7/simsimd-6.5.16-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ec7e92323c820935475bc9ec84938eecc9d9bc625055ff057a6d0dcfffb7eb2a", size = 583601, upload-time = "2026-03-07T14:35:08.799Z" }, + { url = "https://files.pythonhosted.org/packages/14/53/26bf42b6f8ec1f5680d91e95e276e49662bc1b8e0522c4861a0c3349b7ba/simsimd-6.5.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5a4be386421726204f70e9f8601dc8818fc2df0032ef6dcd218cdf224a9fce18", size = 421445, upload-time = "2026-03-07T14:35:10.298Z" }, + { url = "https://files.pythonhosted.org/packages/67/05/31b5247c0e17cd82482fd1724881d49ce442ad6affb2776efae8f9cc4835/simsimd-6.5.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe922886957645e041618fddf242a89f5f7ded0c4bee13dc6537f749ccf75ba2", size = 619612, upload-time = "2026-03-07T14:35:12.109Z" }, + { url = "https://files.pythonhosted.org/packages/5d/54/dbc23d585a57c9b0e71ab10705c4121ce91a807df374d433dd86fb438caa/simsimd-6.5.16-cp313-cp313-win_amd64.whl", hash = "sha256:fe7a0fa49b09651cc1721f5928fa68665f4957c492937241bbdd6ed040dc4a5d", size = 87460, upload-time = "2026-03-07T14:35:13.948Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3c/62a41c182ab6f7abfbfe8941fa12d08b8235b4498e988e5d1f29ac21504f/simsimd-6.5.16-cp313-cp313-win_arm64.whl", hash = "sha256:3fc01992b9d3be84d4826c0d9f8a894668ad931285c09f74bdbe61a5400c9f4d", size = 62922, upload-time = "2026-03-07T14:35:15.252Z" }, + { url = "https://files.pythonhosted.org/packages/df/df/6a1b62074968bbd2976611ac9f89fa60bde2c0c3171f1eb303314bd2bb40/simsimd-6.5.16-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:22624893c86cb9f07968a7e471ed81b2e59f68ba4941cea69ee7418b5cc6fe8e", size = 105335, upload-time = "2026-03-07T14:35:16.839Z" }, + { url = "https://files.pythonhosted.org/packages/09/4f/43bf19becc155e5efdd31dc220c1bd34f866172739a7a081a8bfa2cae840/simsimd-6.5.16-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10d8b32ecee86a86fe30abb35a7c47c1d76756838355bc4377b73bdc69d16ed4", size = 94782, upload-time = "2026-03-07T14:35:18.233Z" }, + { url = "https://files.pythonhosted.org/packages/27/91/c31085edffdc81343f81b937fb2930cd0e105cfab1b9b97845c45b3621c3/simsimd-6.5.16-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b5a632299ee145fa2eab53906922d1596ee63f5a182e3741cde9b18745afe68", size = 387117, upload-time = "2026-03-07T14:35:19.573Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a6/faaf1633cf9d3fc5ebe46d2f145f42257accc6bd25420d722702b6b5adfb/simsimd-6.5.16-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:40a7e14e02acebd0cdadc88c3eeb262c6cbff550a10d4bce2c7771756cf68658", size = 275340, upload-time = "2026-03-07T15:13:14.926Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c8/3c3fa982272ab7a5943ceacc04fd64a38d408fce2cd45e7890eb932e92d1/simsimd-6.5.16-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4b878a28a338c30768cb401f4fbb79bd5b911d95ca024717077f1c57746ad78", size = 297257, upload-time = "2026-03-07T15:13:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/81e83b57992f1ae1bb3fa3d55cd1c4a5bd5dafcec6bd44273eb59c8f8f79/simsimd-6.5.16-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:639bb66dbb15da8727267dc7b7fbf7cc59c18ccef901dd83cdff4f12651f0244", size = 286880, upload-time = "2026-03-07T15:13:19.428Z" }, + { url = "https://files.pythonhosted.org/packages/5e/96/de52bf9ffff59c71b9bc672d7a539c431d81a17d909c4ee734f7731b51d2/simsimd-6.5.16-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:999acb24a43c619af6217b513536ae28bfe23c8fa170a4120a3cca7fdd22acff", size = 585133, upload-time = "2026-03-07T14:35:21.53Z" }, + { url = "https://files.pythonhosted.org/packages/84/e8/190aead5370bc3e0bd0f5fbd938a27cac4678dd903e81ff16acae7d7c6e4/simsimd-6.5.16-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8524c7fd12f7ef9b97e824c65db4e89919b7cc8d530780119b3417ce8643a3c2", size = 422963, upload-time = "2026-03-07T14:35:23.137Z" }, + { url = "https://files.pythonhosted.org/packages/c0/26/d6ecb102a16f01ea22e98bbf8da37b9a8cb4fb38459b939367afb401f1c4/simsimd-6.5.16-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:973460e647b3f769e714caa40b64f56dcf95a4afca98cdd19e2c3c1c9527e438", size = 320199, upload-time = "2026-03-07T15:13:21.823Z" }, + { url = "https://files.pythonhosted.org/packages/07/08/920d1619df54ed2c377dbfb10e0a561e27731091995ba1093b600ed3f00c/simsimd-6.5.16-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:141437e4d727872ab50fe3b19098816aee23b8c3519ee04c9831ef0326e444e1", size = 340041, upload-time = "2026-03-07T15:13:23.91Z" }, + { url = "https://files.pythonhosted.org/packages/0d/3e/995e875eca129b1acb35e4824f1f4fab30b8393da80d51883552e2edd60f/simsimd-6.5.16-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3daee137ffc2dd8bbe64b7f0f95ca2b2302b2985c35a6a7be61626052aa74e5d", size = 317465, upload-time = "2026-03-07T15:13:25.697Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ce/892865784240c167624bf55f835ff74d52e24c7d7f1b9aa79f77358397ac/simsimd-6.5.16-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:03f4d0a8aff48160e3b0acb44ac5525a39d26348db907d6d5ef516369b309973", size = 620749, upload-time = "2026-03-07T14:35:25.077Z" }, + { url = "https://files.pythonhosted.org/packages/54/0d/b74a391fefe7d349230b58b1d0fe6d401d1625553b5375a99608f9d228a9/simsimd-6.5.16-cp313-cp313t-win_amd64.whl", hash = "sha256:01ef2ff8cf99fc3a8e23fb2cadc06b6aa4df9b5e6d001b184d42cf403b1cdc16", size = 87630, upload-time = "2026-03-07T14:35:26.598Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ac/004dc381de9ac6634c785d0284dba8d1f12018584ddd992c09d9f85454b9/simsimd-6.5.16-cp313-cp313t-win_arm64.whl", hash = "sha256:a152c559298bae402ed8205b604e5b0418a2ce8a61a6a87f14973e53b68d5f6a", size = 63126, upload-time = "2026-03-07T14:35:28.295Z" }, + { url = "https://files.pythonhosted.org/packages/6b/5a/b70d670c67ca3d0284b4a52e32d65eb9767df51c0ff5b968db6a2bdc406c/simsimd-6.5.16-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c70924ce14c7ed1663ff131f34bdf3987042f569b41a4ed756a1ad65109de760", size = 105215, upload-time = "2026-03-07T14:35:29.677Z" }, + { url = "https://files.pythonhosted.org/packages/cd/16/59a7d17719a49d453d35a21d2fc40bd7915f78046f82b3325f1f5629505a/simsimd-6.5.16-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cfa1237885074a8e8aba7c203d82e189b84760ffa946fb53e82ece762f40f36c", size = 94618, upload-time = "2026-03-07T14:35:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/02/75/8cb99c018b1c68b5048e19df9d4552d5f41f0512f2e32fdd6a5e58a5b2d1/simsimd-6.5.16-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7ecf8eb87e39a72e23126bf7ffa1a454830ec2daddd00ac89cef96aefce788a7", size = 385337, upload-time = "2026-03-07T14:35:32.863Z" }, + { url = "https://files.pythonhosted.org/packages/79/48/0fd0017b306422d950758e8077e00295d5d9dc2add4680c0aad437774128/simsimd-6.5.16-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0029256c39bafc3930884b47280628ff84a8eda3b7b55e64465f0e051df93cb8", size = 583769, upload-time = "2026-03-07T14:35:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/03/4e/803bffa17b5d52bd545b906f28d947630f271d6a4dc53324d5177464babe/simsimd-6.5.16-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9afa80898b89cdb65317ca6f36efedb3320a000205a82b70dd2ea82872482d08", size = 421581, upload-time = "2026-03-07T14:35:36.719Z" }, + { url = "https://files.pythonhosted.org/packages/59/59/93bbf9c1a6b554b4cf21b32f436fc0de082fe929c4c459d295292ee8bcce/simsimd-6.5.16-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fc6b72bf5a62afa66a9b51f6a01d751d8f217c9f7d4b1ea094e495c3dce87c33", size = 619710, upload-time = "2026-03-07T14:35:38.338Z" }, + { url = "https://files.pythonhosted.org/packages/04/4c/207158749eb6ad8576a1b3cd4e80b7f1e2a0fc59fb2b0730f8df43b3d4a9/simsimd-6.5.16-cp314-cp314-win_amd64.whl", hash = "sha256:96fdb750432ad6478177fb80612b3aea2da002dff613f1fddd19334da9b7f25e", size = 90117, upload-time = "2026-03-07T14:35:40.495Z" }, + { url = "https://files.pythonhosted.org/packages/66/67/38ab856761cc62fbb92b328350a6652f87b27ab2ca1d49fa934aaeca0d3c/simsimd-6.5.16-cp314-cp314-win_arm64.whl", hash = "sha256:2e3981bfa3f09fa9fac845037df7c3a684e0538ff297d3b2ccd26a2eed243f80", size = 64908, upload-time = "2026-03-07T14:35:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/62/49/df617f9e5605b48b75d921b5361c88475879b95a43dd3f2b77fb4659382a/simsimd-6.5.16-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:864a0497c8d4bdc6948bedb016836ba777d14a93300c3735c6e84444241cd66e", size = 105371, upload-time = "2026-03-07T14:35:43.552Z" }, + { url = "https://files.pythonhosted.org/packages/45/3f/e0b8064146919d40436503032f331fc92fbd3d8e5b29ca01c40a675432cf/simsimd-6.5.16-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:492b86704d942fa3ec627523ba7f40e87203e4222d498aa6fc880a865e13fa76", size = 94790, upload-time = "2026-03-07T14:35:44.914Z" }, + { url = "https://files.pythonhosted.org/packages/74/6f/b3811e96e6582e4f04793b688eb1f85e2a74722f00089dc5c7932023d523/simsimd-6.5.16-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c4e0e257e191c2e1ac94737901ec3771b076f7b9c032b620c0bfb747ecefcd9", size = 387243, upload-time = "2026-03-07T14:35:46.408Z" }, + { url = "https://files.pythonhosted.org/packages/47/5b/46b52cd8df732e73799adb91af16e2bc872e597349b01f456df3008d4dd7/simsimd-6.5.16-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ed0eec1d7d5124bc86256a8d7ac81b1c6363149e1f1cc957007418da04e8ed", size = 585270, upload-time = "2026-03-07T14:35:48.885Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0b/92c7dc6b6478032cde9d65f997e8135f5e178c455b8585877b3a9f996bf7/simsimd-6.5.16-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b331c7c2222bc03139e0821c076103ea50f9fab5750571b4cd1e53c2ba3cb0d6", size = 423066, upload-time = "2026-03-07T14:35:50.63Z" }, + { url = "https://files.pythonhosted.org/packages/4d/03/ad761cc350e0f30cd52f798e39434ce68bd09a741e931f4458ddafd0d099/simsimd-6.5.16-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c51b74b8f9b096ddd98beea66e18751ad079c398600d8c877a5d228a1f23d20", size = 620824, upload-time = "2026-03-07T14:35:52.638Z" }, + { url = "https://files.pythonhosted.org/packages/bf/aa/b059b409ae311d4d5e936c07c506c62d5f547597933822fe8c54d32e276b/simsimd-6.5.16-cp314-cp314t-win_amd64.whl", hash = "sha256:4aedebecab2c776177c2db2cdd2f311892d9b1b71bcf66d889539ab1e22ad9a6", size = 90323, upload-time = "2026-03-07T14:35:54.438Z" }, + { url = "https://files.pythonhosted.org/packages/07/3a/2d0a48ef00dd495b5ded82a476ec4300ae3f67496cbd7c7fe2777de89a3c/simsimd-6.5.16-cp314-cp314t-win_arm64.whl", hash = "sha256:d63af5fbd32b0346ef949794451b6c1ec58a66139d3ca22177f93cf7c4be7877", size = 65109, upload-time = "2026-03-07T14:35:55.863Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sqlite-vec" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/50/7ad59cfd3003a2110cc366e526293de4c2520486f5ddaa8dc78b265f8d3e/sqlite_vec-0.1.7-py3-none-macosx_10_6_x86_64.whl", hash = "sha256:c34a136caecff4ae17d4c0cc268fcda89764ee870039caa21431e8e3fb2f4d48", size = 131171, upload-time = "2026-03-17T07:42:50.438Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c9/1cd2f59b539096cd2ce6b540247b2dfe3c47ba04d9368b5e8e3dc86498d4/sqlite_vec-0.1.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d272593d1b45ec7ea289b160ee6e5fafbaa6e1f5ba15f1305c012b0bda43653", size = 165434, upload-time = "2026-03-17T07:42:51.555Z" }, + { url = "https://files.pythonhosted.org/packages/75/91/30c3c382140dcc7bc6e3a07eac7ca610a2b5b70eb9bc7066dc3e7f748d58/sqlite_vec-0.1.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d27746d8e254a390bd15574aed899a0b9bb915b5321eb130a9c09722898cc03", size = 160076, upload-time = "2026-03-17T07:42:52.451Z" }, + { url = "https://files.pythonhosted.org/packages/59/56/6ff304d917ee79da769708dad0aed5fd34c72cbd0ae5e38bcc56cdc652a4/sqlite_vec-0.1.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux1_x86_64.whl", hash = "sha256:ad654283cb9c059852ce2d82018c757b06a705ada568f8b126022a131189818e", size = 163388, upload-time = "2026-03-17T07:42:53.516Z" }, + { url = "https://files.pythonhosted.org/packages/8b/27/fb1b6e3f9072854fe405f7aa99c46d4b465e84c9cec2ff7778edf29ecbbd/sqlite_vec-0.1.7-py3-none-win_amd64.whl", hash = "sha256:0c67877a87cb49426237b950237e82dbeb77778ab2ba89bea859f391fd169382", size = 292804, upload-time = "2026-03-17T07:42:54.325Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, +] + +[[package]] +name = "stringzilla" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/68/475518f6f4af8273ecd619a5d37d715d36908973f9970faf21571a296821/stringzilla-4.6.0.tar.gz", hash = "sha256:640c0fb5b6a2ad77b7721bff98f00a3c524ca60dc202f552e486831a751d4bbd", size = 646335, upload-time = "2025-12-26T23:44:43.956Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/39/ee92df4c6ffc19d763824c061e47b358e1ab8e0724f35d0a41d17dd4e850/stringzilla-4.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0123f724dc63c7e7dbb6eda5f821593effc5fda889ba392e0f7c676924d1131b", size = 212271, upload-time = "2025-12-26T23:43:30.676Z" }, + { url = "https://files.pythonhosted.org/packages/b6/21/641914f44f195fb718e1ebf111f0f70150cf5e4272b3577d6b832fd085f6/stringzilla-4.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:41ec1735fdeaf4e276be0a531d4dd7ad4cb1e7071fd8729c2ca579a5ea49f92a", size = 199360, upload-time = "2025-12-26T23:43:31.993Z" }, + { url = "https://files.pythonhosted.org/packages/8e/20/561b7199cfeacf6ef68d26a8846a0e6f521e96e4be4f9d20098d80b6314d/stringzilla-4.6.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1105de1b97b7b8f2c8e4dda0cb370cef882fdb5df8f945c141bc022ca3ba65f", size = 689260, upload-time = "2025-12-26T23:43:33.58Z" }, + { url = "https://files.pythonhosted.org/packages/ba/71/70bbbf4e59f4dd2e097f36a4e96482a546a0f081721068e32a7d02f60014/stringzilla-4.6.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e5f8d1d72e23981964aec25a1eb41a9c42814d1a4e90467c0e27fb1f4c2fe948", size = 657134, upload-time = "2025-12-26T23:43:35.103Z" }, + { url = "https://files.pythonhosted.org/packages/f5/26/1e98779e66990d15ad1bb025f24ddef736712333b173926c06977159613c/stringzilla-4.6.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ac8fc7c55b612a52855143eff0981c11c80d0a64472bf77b68339bdea01737c0", size = 640353, upload-time = "2025-12-26T23:43:36.509Z" }, + { url = "https://files.pythonhosted.org/packages/30/44/e73fd1c47f23ee7513bf86451464db2eda3d8835e9be408f9dd9ca06a0af/stringzilla-4.6.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:899974a4e8231abe3486f7e47f0cc3047d75da99e447153738efd5097db731d1", size = 2053771, upload-time = "2025-12-26T23:43:38.038Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5c/d274c820da68254c105c6c18b31001c1624f78c9cc7c36f6550ca17cc6ef/stringzilla-4.6.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:221f53f809d7beac636d369a255fd5d9729a6a8b5fe4e38afc30b41f59eccc39", size = 643972, upload-time = "2025-12-26T23:43:39.679Z" }, + { url = "https://files.pythonhosted.org/packages/42/65/5f2a826c7091405db189aac751db6cee5e254fb9b35a99f053889b2d3615/stringzilla-4.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7f890db95eb00e10df429e5d85fdacb4904fbe9be20f59d87c2981b06b2caf1", size = 653518, upload-time = "2025-12-26T23:43:41.188Z" }, + { url = "https://files.pythonhosted.org/packages/78/40/30f450e4c8edc981c72e155ec41419712701e81eb38a5dda7cd07d7bd24e/stringzilla-4.6.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b035ee7301921cd064421cbd08c84e6adb7244472c7679155716d7ac57a95299", size = 586823, upload-time = "2025-12-26T23:43:42.944Z" }, + { url = "https://files.pythonhosted.org/packages/86/ff/ad0c3b795a3454e13d9dd59552029713ccec87bdc5ee62e48f5dd63f2e56/stringzilla-4.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fd7d125408f372aee425b0d0dd998199c0dbca599be9942ce41b745f21c98198", size = 626121, upload-time = "2025-12-26T23:43:44.188Z" }, + { url = "https://files.pythonhosted.org/packages/08/12/9ecda6d1972ddc2a91a857e5305066e2784392d9e27eafbf99b84c79052a/stringzilla-4.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a50fc1237f64fdf79bea5d55c397ac88abc740284ddbfd03bdeab0a0b43711fe", size = 618785, upload-time = "2025-12-26T23:43:45.481Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f0/a744c287f1bad754a4f03d4c1b64be8939ff7ea32f345352f3c09ed15fb0/stringzilla-4.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b21708b71907a3f625e37f9248c37c67542f5665740f2205b9050e1fc67b4d87", size = 613150, upload-time = "2025-12-26T23:43:46.746Z" }, + { url = "https://files.pythonhosted.org/packages/46/04/4246dc812a5da8d97176476f79796cfd02e4e1c1ad8ae27f1ca0e869c4eb/stringzilla-4.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dac46e84bcb28b344206dbc21b892c4ec5fa8506ee7027412819be55e75f91e9", size = 1909233, upload-time = "2025-12-26T23:43:48.295Z" }, + { url = "https://files.pythonhosted.org/packages/fb/99/fab077604f45c8ff1d330c26d1cccf94a58ea0a9155c8ba63c038117543a/stringzilla-4.6.0-cp313-cp313-win32.whl", hash = "sha256:51d3c8504a8038b3d1699f366a56a8d4571741c3993af9562b6f227d56c5ad67", size = 114753, upload-time = "2025-12-26T23:43:49.71Z" }, + { url = "https://files.pythonhosted.org/packages/0e/21/b9bd35292a126a0c2fde103ad5ce5bfe7384cb6eba4ca34fc5cbcaa7ac96/stringzilla-4.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:0e18442332b72d37465f7959bb72f0be480f4d604c86d165863587113401bb45", size = 162434, upload-time = "2025-12-26T23:43:51.486Z" }, + { url = "https://files.pythonhosted.org/packages/6b/89/1f5a6b4f8fc405e1e76125be9629ad7390bfcf1be9683a6e2e5602911b68/stringzilla-4.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:33192fe820704803a52b90c566838373f45b295e4064e0679ba4f76bb76b6ce7", size = 123350, upload-time = "2025-12-26T23:43:53.097Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tifffile" +version = "2026.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/cb/2f6d79c7576e22c116352a801f4c3c8ace5957e9aced862012430b62e14f/tifffile-2026.3.3.tar.gz", hash = "sha256:d9a1266bed6f2ee1dd0abde2018a38b4f8b2935cb843df381d70ac4eac5458b7", size = 388745, upload-time = "2026-03-03T19:14:38.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/e4/e804505f87627cd8cdae9c010c47c4485fd8c1ce31a7dd0ab7fcc4707377/tifffile-2026.3.3-py3-none-any.whl", hash = "sha256:e8be15c94273113d31ecb7aa3a39822189dd11c4967e3cc88c178f1ad2fd1170", size = 243960, upload-time = "2026-03-03T19:14:35.808Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "ty" +version = "0.0.24" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/96/652a425030f95dc2c9548d9019e52502e17079e1daeefbc4036f1c0905b4/ty-0.0.24.tar.gz", hash = "sha256:9fe42f6b98207bdaef51f71487d6d087f2cb02555ee3939884d779b2b3cc8bfc", size = 5354286, upload-time = "2026-03-19T16:55:57.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/e5/34457ee11708e734ba81ad65723af83030e484f961e281d57d1eecf08951/ty-0.0.24-py3-none-linux_armv6l.whl", hash = "sha256:1ab4f1f61334d533a3fdf5d9772b51b1300ac5da4f3cdb0be9657a3ccb2ce3e7", size = 10394877, upload-time = "2026-03-19T16:55:54.246Z" }, + { url = "https://files.pythonhosted.org/packages/44/81/bc9a1b1a87f43db15ab64ad781a4f999734ec3b470ad042624fa875b20e6/ty-0.0.24-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:facbf2c4aaa6985229e08f8f9bf152215eb078212f22b5c2411f35386688ab42", size = 10211109, upload-time = "2026-03-19T16:55:28.554Z" }, + { url = "https://files.pythonhosted.org/packages/e4/63/cfc805adeaa61d63ba3ea71127efa7d97c40ba36d97ee7bd957341d05107/ty-0.0.24-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b6d2a3b6d4470c483552a31e9b368c86f154dcc964bccb5406159dc9cd362246", size = 9694769, upload-time = "2026-03-19T16:55:34.309Z" }, + { url = "https://files.pythonhosted.org/packages/33/09/edc220726b6ec44a58900401f6b27140997ef15026b791e26b69a6e69eb5/ty-0.0.24-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c94c25d0500939fd5f8f16ce41cbed5b20528702c1d649bf80300253813f0a2", size = 10176287, upload-time = "2026-03-19T16:55:37.17Z" }, + { url = "https://files.pythonhosted.org/packages/f8/bf/cbe2227be711e65017655d8ee4d050f4c92b113fb4dc4c3bd6a19d3a86d8/ty-0.0.24-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:89cbe7bc7df0fab02dbd8cda79b737df83f1ef7fb573b08c0ee043dc68cffb08", size = 10214832, upload-time = "2026-03-19T16:56:08.518Z" }, + { url = "https://files.pythonhosted.org/packages/af/1d/d15803ee47e9143d10e10bd81ccc14761d08758082bda402950685f0ddfe/ty-0.0.24-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db2c5d269bcc9b764850c99f457b5018a79b3ef40ecfbc03344e65effd6cf743", size = 10709892, upload-time = "2026-03-19T16:56:05.727Z" }, + { url = "https://files.pythonhosted.org/packages/36/12/6db0d86c477147f67b9052de209421d76c3e855197b000c25fcbbe86b3a2/ty-0.0.24-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba44512db5b97c3bbd59d93e11296e8548d0c9a3bdd1280de36d7ff22d351896", size = 11280872, upload-time = "2026-03-19T16:56:02.899Z" }, + { url = "https://files.pythonhosted.org/packages/1b/fc/155fe83a97c06d33ccc9e0f428258b32df2e08a428300c715d34757f0111/ty-0.0.24-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a52b7f589c3205512a9c50ba5b2b1e8c0698b72e51b8b9285c90420c06f1cae8", size = 11060520, upload-time = "2026-03-19T16:55:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f1/32c05a1c4c3c2a95c5b7361dee03a9bf1231d4ad096b161c838b45bce5a0/ty-0.0.24-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7981df5c709c054da4ac5d7c93f8feb8f45e69e829e4461df4d5f0988fe67d04", size = 10791455, upload-time = "2026-03-19T16:55:25.728Z" }, + { url = "https://files.pythonhosted.org/packages/17/2c/53c1ea6bedfa4d4ab64d4de262d8f5e405ecbffefd364459c628c0310d33/ty-0.0.24-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b2860151ad95a00d0f0280b8fef79900d08dcd63276b57e6e5774f2c055979c5", size = 10156708, upload-time = "2026-03-19T16:55:45.563Z" }, + { url = "https://files.pythonhosted.org/packages/45/39/7d2919cf194707169474d80720a5f3d793e983416f25e7ffcf80504c9df2/ty-0.0.24-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5674a1146d927ab77ff198a88e0c4505134ced342a0e7d1beb4a076a728b7496", size = 10236263, upload-time = "2026-03-19T16:55:31.474Z" }, + { url = "https://files.pythonhosted.org/packages/cf/7f/48eac722f2fd12a5b7aae0effdcb75c46053f94b783d989e3ef0d7380082/ty-0.0.24-py3-none-musllinux_1_2_i686.whl", hash = "sha256:438ecbf1608a9b16dd84502f3f1b23ef2ef32bbd0ab3e0ca5a82f0e0d1cd41ea", size = 10402559, upload-time = "2026-03-19T16:55:39.602Z" }, + { url = "https://files.pythonhosted.org/packages/75/e0/8cf868b9749ce1e5166462759545964e95b02353243594062b927d8bff2a/ty-0.0.24-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ddeed3098dd92a83964e7aa7b41e509ba3530eb539fc4cd8322ff64a09daf1f5", size = 10893684, upload-time = "2026-03-19T16:55:51.439Z" }, + { url = "https://files.pythonhosted.org/packages/17/9f/f54bf3be01d2c2ed731d10a5afa3324dc66f987a6ae0a4a6cbfa2323d080/ty-0.0.24-py3-none-win32.whl", hash = "sha256:83013fb3a4764a8f8bcc6ca11ff8bdfd8c5f719fc249241cb2b8916e80778eb1", size = 9781542, upload-time = "2026-03-19T16:56:11.588Z" }, + { url = "https://files.pythonhosted.org/packages/fb/49/c004c5cc258b10b3a145666e9a9c28ae7678bc958c8926e8078d5d769081/ty-0.0.24-py3-none-win_amd64.whl", hash = "sha256:748a60eb6912d1cf27aaab105ffadb6f4d2e458a3fcadfbd3cf26db0d8062eeb", size = 10764801, upload-time = "2026-03-19T16:55:42.752Z" }, + { url = "https://files.pythonhosted.org/packages/e2/59/006a074e185bfccf5e4c026015245ab4fcd2362b13a8d24cf37a277909a9/ty-0.0.24-py3-none-win_arm64.whl", hash = "sha256:280a3d31e86d0721947238f17030c33f0911cae851d108ea9f4e3ab12a5ed01f", size = 10194093, upload-time = "2026-03-19T16:55:48.303Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.42.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/ad/4a96c425be6fb67e0621e62d86c402b4a17ab2be7f7c055d9bd2f638b9e2/uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775", size = 85393, upload-time = "2026-03-16T06:19:50.077Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/89/f8827ccff89c1586027a105e5630ff6139a64da2515e24dafe860bd9ae4d/uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359", size = 68830, upload-time = "2026-03-16T06:19:48.325Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] diff --git a/docs/specs/4-architecture/features/030-ai-vision-service/tasks.md b/docs/specs/4-architecture/features/030-ai-vision-service/tasks.md index 13e112ce3d6..620dd932f7e 100644 --- a/docs/specs/4-architecture/features/030-ai-vision-service/tasks.md +++ b/docs/specs/4-architecture/features/030-ai-vision-service/tasks.md @@ -12,7 +12,11 @@ _Last updated: 2026-03-21_ ### I1 – Python Service: Project Setup & Face Detection +<<<<<<< HEAD +- [x] T-030-01 – Create Python service project structure with uv, ruff, ty. +======= - [ ] T-030-01 – Create Python service project structure with uv, ruff, ty. +>>>>>>> master _Intent:_ Create `ai-vision-service/` directory with: `pyproject.toml` (uv project config, ruff settings, ty config), `app/` (main application with `__init__.py`), `app/detection/`, `app/embeddings/`, `app/api/`, `app/clustering/`, `app/matching/`, `tests/`, `Dockerfile`, `README.md`. Configure ruff lint rules (E, W, F, I, N, UP, ANN, B, A, SIM, TCH, RUF) and ty in `pyproject.toml`. Create Pydantic `AppSettings` (BaseSettings) in `app/config.py` with all `VISION_FACE_`-prefixed env vars. Create Pydantic request/response schemas in `app/api/schemas.py`: `DetectRequest`, `FaceResult`, `DetectCallbackPayload`, `MatchResult`, `MatchResponse`, `HealthResponse`. All code fully type-annotated. _Verification commands:_ - `uv sync` @@ -20,13 +24,21 @@ _Last updated: 2026-03-21_ - `uv run ruff check` - `uv run ty check` +<<<<<<< HEAD +- [x] T-030-02 – Implement face detection and crop generation with InsightFace. +======= - [ ] T-030-02 – Implement face detection and crop generation with InsightFace. +>>>>>>> master _Intent:_ `app/detection/detector.py`: typed wrapper around InsightFace (ONNX Runtime backend, `buffalo_l` model). Accept photo filesystem path (shared Docker volume — Q-030-07 resolved), return list of `FaceResult` with bounding box coordinates as 0.0–1.0 relative values and confidence scores. `app/detection/cropper.py`: generate 150x150px JPEG face crop per detected face using Pillow, returned as base64 string (Q-030-09 resolved: server-side crop). Full type annotations; no `Any` types. _Verification commands:_ - `uv run pytest tests/test_detection.py tests/test_cropper.py` - `uv run ty check` +<<<<<<< HEAD +- [x] T-030-03 – Implement embedding generation and storage layer. +======= - [ ] T-030-03 – Implement embedding generation and storage layer. +>>>>>>> master _Intent:_ `app/embeddings/store.py`: abstract `EmbeddingStore` protocol (typed). `app/embeddings/sqlite_store.py`: SQLite+sqlite-vec implementation. `app/embeddings/pgvector_store.py`: PostgreSQL+pgvector implementation. CRUD operations for embeddings. Vector similarity search for matching. Configurable via `VISION_FACE_STORAGE_BACKEND` env var. Pydantic validation on all inputs. _Verification commands:_ - `uv run pytest tests/test_embeddings.py` @@ -34,19 +46,31 @@ _Last updated: 2026-03-21_ ### I2 – Python Service: Clustering, Matching & Callback +<<<<<<< HEAD +- [x] T-030-04 – Implement face clustering with scikit-learn DBSCAN. +======= - [ ] T-030-04 – Implement face clustering with scikit-learn DBSCAN. +>>>>>>> master _Intent:_ `app/clustering/clusterer.py`: cluster similar face embeddings using scikit-learn DBSCAN. Configurable distance threshold (eps). Returns cluster labels for each embedding. Typed interface. No need to pre-specify cluster count (Q-030-03 resolved: auto-cluster with manual confirmation). _Verification commands:_ - `uv run pytest tests/test_clustering.py` - `uv run ty check` +<<<<<<< HEAD +- [x] T-030-05 – Implement similarity matching. +======= - [ ] T-030-05 – Implement similarity matching. +>>>>>>> master _Intent:_ `app/matching/matcher.py`: `POST /match` endpoint logic (Q-030-12 resolved: dedicated endpoint). Accepts image file (multipart via FastAPI `UploadFile`), detects face, compares embedding against stored embeddings via `EmbeddingStore.similarity_search()`, returns list of `MatchResult` with confidence scores. Selfie image discarded after match — no temp file persisted (Q-030-11 resolved). Full type annotations. _Verification commands:_ - `uv run pytest tests/test_matching.py` - `uv run ty check` +<<<<<<< HEAD +- [x] T-030-06 – Implement FastAPI REST API, scan callback flow, and API key auth. +======= - [ ] T-030-06 – Implement FastAPI REST API, scan callback flow, and API key auth. +>>>>>>> master _Intent:_ `app/main.py`: FastAPI app factory with lifespan handler (model loading on startup). `app/api/routes.py`: `POST /detect`, `POST /match`, `GET /health` — all using Pydantic request/response models. `app/api/dependencies.py`: API key auth as FastAPI dependency (validates `X-API-Key` header against `VISION_FACE_API_KEY`). Scan callback flow: receive `DetectRequest` → detect faces → generate embeddings + base64 crops → store embeddings → POST `DetectCallbackPayload` back to Lychee via httpx. `HealthResponse` includes model_loaded status and embedding_count. _Verification commands:_ - `uv run pytest tests/test_api.py` @@ -56,18 +80,30 @@ _Last updated: 2026-03-21_ ### I3 – Python Service: Docker Image, Deployment & CI/CD +<<<<<<< HEAD +- [x] T-030-07 – Create Dockerfile and docker-compose integration. +======= - [ ] T-030-07 – Create Dockerfile and docker-compose integration. +>>>>>>> master _Intent:_ Multi-stage Dockerfile: builder stage uses `uv sync --frozen --no-dev`, runtime stage uses `python:3.13-slim`. Minimal image size. GPU support optional. Model (`buffalo_l`) baked into image at build time — lifespan handler loads it on startup, no runtime download (Q-030-32 resolved). Workers count via CMD shell form to honour `VISION_FACE_WORKERS`. All env vars `VISION_FACE_`-prefixed (see Pydantic `AppSettings`). Add service to Lychee's docker-compose example with shared photos volume and internal network. _Verification commands:_ - `docker build -t lychee-ai-vision .` - `docker-compose up -d` +<<<<<<< HEAD +- [x] T-030-08 – Create GitHub Actions CI/CD workflow for Python service. +======= - [ ] T-030-08 – Create GitHub Actions CI/CD workflow for Python service. +>>>>>>> master _Intent:_ `.github/workflows/python_ai_vision.yml`: triggers on push/PR when `ai-vision-service/**` changes. Jobs: lint (`uv run ruff format --check`, `uv run ruff check`), typecheck (`uv run ty check`), test (`uv run pytest --cov=app --cov-report=xml`, Python 3.13+3.14 matrix), docker-build (`docker build .`). Uses `astral-sh/setup-uv@v5`. Follow existing Lychee CI patterns: pinned action versions, `step-security/harden-runner`, concurrency groups. _Verification commands:_ - Push branch and verify workflow runs green +<<<<<<< HEAD +- [x] T-030-09 – End-to-end smoke test in Docker. +======= - [ ] T-030-09 – End-to-end smoke test in Docker. +>>>>>>> master _Intent:_ docker-compose up → health check passes → detect endpoint responds → callback delivers results to mock Lychee endpoint. Verify shared volume photo access works. _Verification commands:_ - Integration test suite From ca1524393735aebe3614501729f09d9a40a144df Mon Sep 17 00:00:00 2001 From: ildyria Date: Sat, 21 Mar 2026 15:15:02 +0100 Subject: [PATCH 02/14] Fix tasks --- .../features/030-ai-vision-service/tasks.md | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/docs/specs/4-architecture/features/030-ai-vision-service/tasks.md b/docs/specs/4-architecture/features/030-ai-vision-service/tasks.md index 620dd932f7e..4e3bb522903 100644 --- a/docs/specs/4-architecture/features/030-ai-vision-service/tasks.md +++ b/docs/specs/4-architecture/features/030-ai-vision-service/tasks.md @@ -12,11 +12,7 @@ _Last updated: 2026-03-21_ ### I1 – Python Service: Project Setup & Face Detection -<<<<<<< HEAD - [x] T-030-01 – Create Python service project structure with uv, ruff, ty. -======= -- [ ] T-030-01 – Create Python service project structure with uv, ruff, ty. ->>>>>>> master _Intent:_ Create `ai-vision-service/` directory with: `pyproject.toml` (uv project config, ruff settings, ty config), `app/` (main application with `__init__.py`), `app/detection/`, `app/embeddings/`, `app/api/`, `app/clustering/`, `app/matching/`, `tests/`, `Dockerfile`, `README.md`. Configure ruff lint rules (E, W, F, I, N, UP, ANN, B, A, SIM, TCH, RUF) and ty in `pyproject.toml`. Create Pydantic `AppSettings` (BaseSettings) in `app/config.py` with all `VISION_FACE_`-prefixed env vars. Create Pydantic request/response schemas in `app/api/schemas.py`: `DetectRequest`, `FaceResult`, `DetectCallbackPayload`, `MatchResult`, `MatchResponse`, `HealthResponse`. All code fully type-annotated. _Verification commands:_ - `uv sync` @@ -24,21 +20,13 @@ _Last updated: 2026-03-21_ - `uv run ruff check` - `uv run ty check` -<<<<<<< HEAD - [x] T-030-02 – Implement face detection and crop generation with InsightFace. -======= -- [ ] T-030-02 – Implement face detection and crop generation with InsightFace. ->>>>>>> master _Intent:_ `app/detection/detector.py`: typed wrapper around InsightFace (ONNX Runtime backend, `buffalo_l` model). Accept photo filesystem path (shared Docker volume — Q-030-07 resolved), return list of `FaceResult` with bounding box coordinates as 0.0–1.0 relative values and confidence scores. `app/detection/cropper.py`: generate 150x150px JPEG face crop per detected face using Pillow, returned as base64 string (Q-030-09 resolved: server-side crop). Full type annotations; no `Any` types. _Verification commands:_ - `uv run pytest tests/test_detection.py tests/test_cropper.py` - `uv run ty check` -<<<<<<< HEAD - [x] T-030-03 – Implement embedding generation and storage layer. -======= -- [ ] T-030-03 – Implement embedding generation and storage layer. ->>>>>>> master _Intent:_ `app/embeddings/store.py`: abstract `EmbeddingStore` protocol (typed). `app/embeddings/sqlite_store.py`: SQLite+sqlite-vec implementation. `app/embeddings/pgvector_store.py`: PostgreSQL+pgvector implementation. CRUD operations for embeddings. Vector similarity search for matching. Configurable via `VISION_FACE_STORAGE_BACKEND` env var. Pydantic validation on all inputs. _Verification commands:_ - `uv run pytest tests/test_embeddings.py` @@ -46,31 +34,19 @@ _Last updated: 2026-03-21_ ### I2 – Python Service: Clustering, Matching & Callback -<<<<<<< HEAD - [x] T-030-04 – Implement face clustering with scikit-learn DBSCAN. -======= -- [ ] T-030-04 – Implement face clustering with scikit-learn DBSCAN. ->>>>>>> master _Intent:_ `app/clustering/clusterer.py`: cluster similar face embeddings using scikit-learn DBSCAN. Configurable distance threshold (eps). Returns cluster labels for each embedding. Typed interface. No need to pre-specify cluster count (Q-030-03 resolved: auto-cluster with manual confirmation). _Verification commands:_ - `uv run pytest tests/test_clustering.py` - `uv run ty check` -<<<<<<< HEAD - [x] T-030-05 – Implement similarity matching. -======= -- [ ] T-030-05 – Implement similarity matching. ->>>>>>> master _Intent:_ `app/matching/matcher.py`: `POST /match` endpoint logic (Q-030-12 resolved: dedicated endpoint). Accepts image file (multipart via FastAPI `UploadFile`), detects face, compares embedding against stored embeddings via `EmbeddingStore.similarity_search()`, returns list of `MatchResult` with confidence scores. Selfie image discarded after match — no temp file persisted (Q-030-11 resolved). Full type annotations. _Verification commands:_ - `uv run pytest tests/test_matching.py` - `uv run ty check` -<<<<<<< HEAD - [x] T-030-06 – Implement FastAPI REST API, scan callback flow, and API key auth. -======= -- [ ] T-030-06 – Implement FastAPI REST API, scan callback flow, and API key auth. ->>>>>>> master _Intent:_ `app/main.py`: FastAPI app factory with lifespan handler (model loading on startup). `app/api/routes.py`: `POST /detect`, `POST /match`, `GET /health` — all using Pydantic request/response models. `app/api/dependencies.py`: API key auth as FastAPI dependency (validates `X-API-Key` header against `VISION_FACE_API_KEY`). Scan callback flow: receive `DetectRequest` → detect faces → generate embeddings + base64 crops → store embeddings → POST `DetectCallbackPayload` back to Lychee via httpx. `HealthResponse` includes model_loaded status and embedding_count. _Verification commands:_ - `uv run pytest tests/test_api.py` @@ -80,30 +56,18 @@ _Last updated: 2026-03-21_ ### I3 – Python Service: Docker Image, Deployment & CI/CD -<<<<<<< HEAD - [x] T-030-07 – Create Dockerfile and docker-compose integration. -======= -- [ ] T-030-07 – Create Dockerfile and docker-compose integration. ->>>>>>> master _Intent:_ Multi-stage Dockerfile: builder stage uses `uv sync --frozen --no-dev`, runtime stage uses `python:3.13-slim`. Minimal image size. GPU support optional. Model (`buffalo_l`) baked into image at build time — lifespan handler loads it on startup, no runtime download (Q-030-32 resolved). Workers count via CMD shell form to honour `VISION_FACE_WORKERS`. All env vars `VISION_FACE_`-prefixed (see Pydantic `AppSettings`). Add service to Lychee's docker-compose example with shared photos volume and internal network. _Verification commands:_ - `docker build -t lychee-ai-vision .` - `docker-compose up -d` -<<<<<<< HEAD - [x] T-030-08 – Create GitHub Actions CI/CD workflow for Python service. -======= -- [ ] T-030-08 – Create GitHub Actions CI/CD workflow for Python service. ->>>>>>> master _Intent:_ `.github/workflows/python_ai_vision.yml`: triggers on push/PR when `ai-vision-service/**` changes. Jobs: lint (`uv run ruff format --check`, `uv run ruff check`), typecheck (`uv run ty check`), test (`uv run pytest --cov=app --cov-report=xml`, Python 3.13+3.14 matrix), docker-build (`docker build .`). Uses `astral-sh/setup-uv@v5`. Follow existing Lychee CI patterns: pinned action versions, `step-security/harden-runner`, concurrency groups. _Verification commands:_ - Push branch and verify workflow runs green -<<<<<<< HEAD - [x] T-030-09 – End-to-end smoke test in Docker. -======= -- [ ] T-030-09 – End-to-end smoke test in Docker. ->>>>>>> master _Intent:_ docker-compose up → health check passes → detect endpoint responds → callback delivers results to mock Lychee endpoint. Verify shared volume photo access works. _Verification commands:_ - Integration test suite From 9cd10d6068a5a55afe0bfd6091ba3b172f54ecbf Mon Sep 17 00:00:00 2001 From: ildyria Date: Sat, 21 Mar 2026 15:20:14 +0100 Subject: [PATCH 03/14] Fix astral install --- .github/workflows/python_ai_vision.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python_ai_vision.yml b/.github/workflows/python_ai_vision.yml index 3610caeca51..f83b38b0dc1 100644 --- a/.github/workflows/python_ai_vision.yml +++ b/.github/workflows/python_ai_vision.yml @@ -41,7 +41,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up uv - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd9f5de38368cc08bae2cc2fb # v5.4.1 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 with: enable-cache: true @@ -70,7 +70,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up uv - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd9f5de38368cc08bae2cc2fb # v5.4.1 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 with: enable-cache: true @@ -103,7 +103,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up uv - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd9f5de38368cc08bae2cc2fb # v5.4.1 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 with: enable-cache: true python-version: ${{ matrix.python-version }} From a1475df4855e71369416f1ab378c47bb7fffea2e Mon Sep 17 00:00:00 2001 From: ildyria Date: Sat, 21 Mar 2026 15:22:05 +0100 Subject: [PATCH 04/14] more fixes --- .github/workflows/python_ai_vision.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python_ai_vision.yml b/.github/workflows/python_ai_vision.yml index f83b38b0dc1..826493c1154 100644 --- a/.github/workflows/python_ai_vision.yml +++ b/.github/workflows/python_ai_vision.yml @@ -116,7 +116,7 @@ jobs: - name: Upload coverage if: matrix.python-version == '3.13' - uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 + uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 with: files: ai-vision-service/coverage.xml flags: ai-vision-service @@ -138,10 +138,10 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@b5ca514318bd2bf3c1fb87b4f2a0afaa4d2a7c4e # v3.10.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd #v4.0.0 - name: Build Docker image (no model bake in CI to save time) - uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: ai-vision-service push: false From cc8c44e6e65db0240101c06f289bdf5c99bb1e51 Mon Sep 17 00:00:00 2001 From: ildyria Date: Sat, 21 Mar 2026 15:31:32 +0100 Subject: [PATCH 05/14] Fix dockerfile --- ai-vision-service/Dockerfile | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ai-vision-service/Dockerfile b/ai-vision-service/Dockerfile index 1ecfb6a4cc7..8cd264c9581 100644 --- a/ai-vision-service/Dockerfile +++ b/ai-vision-service/Dockerfile @@ -8,11 +8,18 @@ FROM python:3.13-slim AS builder # Install uv from the official image. COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv +# Build tools required to compile insightface's Cython extension (mesh_core_cython). +RUN apt-get update && apt-get install -y --no-install-recommends \ + g++ \ + libgl1 \ + libglib2.0-0 \ + && rm -rf /var/lib/apt/lists/* + WORKDIR /app # Install dependencies only (no source code) so layer cache is reused when # only application code changes. -COPY pyproject.toml uv.lock ./ +COPY pyproject.toml uv.lock README.md ./ RUN uv sync --frozen --no-dev # Bake buffalo_l model weights into the image at build time (~300 MB download). From d8a029ffbeea3e148f410226ac9951867c8bc50b Mon Sep 17 00:00:00 2001 From: ildyria Date: Sat, 21 Mar 2026 15:42:32 +0100 Subject: [PATCH 06/14] Accept CVE --- .github/workflows/dependency-review.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 1d8a2052c05..86f421ec23d 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -25,3 +25,8 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: 'Dependency Review' uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0 + allow-ghsas: + # No fix available yet + # Note that the model is directly baked into the inage + # So the risk is limited. + - GHSA-hqmj-h5c6-369m From 8d06e947889669a24d2095cfd819aa486a10f103 Mon Sep 17 00:00:00 2001 From: ildyria Date: Sat, 21 Mar 2026 15:44:17 +0100 Subject: [PATCH 07/14] pin --- ai-vision-service/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ai-vision-service/Dockerfile b/ai-vision-service/Dockerfile index 8cd264c9581..391dfe0bd84 100644 --- a/ai-vision-service/Dockerfile +++ b/ai-vision-service/Dockerfile @@ -3,7 +3,7 @@ # --------------------------------------------------------------------------- # Stage 1 – builder: install dependencies and bake the model weights. # --------------------------------------------------------------------------- -FROM python:3.13-slim AS builder +FROM python:3.13-slim@sha256:739e7213785e88c0f702dcdc12c0973afcbd606dbf021a589cab77d6b00b579d AS builder # Install uv from the official image. COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv @@ -34,7 +34,7 @@ RUN uv run python -c \ # --------------------------------------------------------------------------- # Stage 2 – runtime: copy only what's needed to run. # --------------------------------------------------------------------------- -FROM python:3.13-slim AS runtime +FROM python:3.13-slim@sha256:739e7213785e88c0f702dcdc12c0973afcbd606dbf021a589cab77d6b00b579d AS runtime WORKDIR /app From 6783eccf4d2b70c2a334398c8f6fcec6c4682d34 Mon Sep 17 00:00:00 2001 From: ildyria Date: Sat, 21 Mar 2026 15:45:41 +0100 Subject: [PATCH 08/14] fix workflow --- .github/workflows/dependency-review.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 86f421ec23d..eecffbd60b3 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -25,8 +25,9 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: 'Dependency Review' uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0 - allow-ghsas: - # No fix available yet - # Note that the model is directly baked into the inage - # So the risk is limited. - - GHSA-hqmj-h5c6-369m + with: + allow-ghsas: + # No fix available yet + # Note that the model is directly baked into the inage + # So the risk is limited. + - GHSA-hqmj-h5c6-369m From 6600c376e740afb939127d07b21646d2cabf0a04 Mon Sep 17 00:00:00 2001 From: ildyria Date: Sat, 21 Mar 2026 15:52:49 +0100 Subject: [PATCH 09/14] fix workflow --- .github/workflows/dependency-review.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index eecffbd60b3..79b62357093 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -26,8 +26,7 @@ jobs: - name: 'Dependency Review' uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0 with: - allow-ghsas: - # No fix available yet - # Note that the model is directly baked into the inage - # So the risk is limited. - - GHSA-hqmj-h5c6-369m + # No fix available yet + # Note that the model is directly baked into the inage + # So the risk is limited. + allow-ghsas: GHSA-hqmj-h5c6-369m From 4bce868e996fd08991ca0c4d846636f6af053109 Mon Sep 17 00:00:00 2001 From: ildyria Date: Sun, 22 Mar 2026 21:28:02 +0100 Subject: [PATCH 10/14] Delete stupid cache files --- .gitignore | 2 ++ .../app/__pycache__/__init__.cpython-313.pyc | Bin 206 -> 0 bytes .../app/__pycache__/config.cpython-313.pyc | Bin 2518 -> 0 bytes .../app/__pycache__/main.cpython-313.pyc | Bin 3594 -> 0 bytes .../api/__pycache__/__init__.cpython-313.pyc | Bin 227 -> 0 bytes .../__pycache__/dependencies.cpython-313.pyc | Bin 2007 -> 0 bytes .../app/api/__pycache__/routes.cpython-313.pyc | Bin 10860 -> 0 bytes .../app/api/__pycache__/schemas.cpython-313.pyc | Bin 4630 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 227 -> 0 bytes .../__pycache__/clusterer.cpython-313.pyc | Bin 3367 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 226 -> 0 bytes .../__pycache__/cropper.cpython-313.pyc | Bin 4496 -> 0 bytes .../__pycache__/detector.cpython-313.pyc | Bin 6952 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 216 -> 0 bytes .../__pycache__/sqlite_store.cpython-313.pyc | Bin 7847 -> 0 bytes .../__pycache__/store.cpython-313.pyc | Bin 2474 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 225 -> 0 bytes .../__pycache__/matcher.cpython-313.pyc | Bin 2765 -> 0 bytes .../tests/__pycache__/__init__.cpython-313.pyc | Bin 220 -> 0 bytes .../conftest.cpython-313-pytest-9.0.2.pyc | Bin 5910 -> 0 bytes .../test_api.cpython-313-pytest-9.0.2.pyc | Bin 18415 -> 0 bytes ...test_clustering.cpython-313-pytest-9.0.2.pyc | Bin 11128 -> 0 bytes .../test_cropper.cpython-313-pytest-9.0.2.pyc | Bin 10435 -> 0 bytes .../test_detection.cpython-313-pytest-9.0.2.pyc | Bin 19853 -> 0 bytes ...test_embeddings.cpython-313-pytest-9.0.2.pyc | Bin 15020 -> 0 bytes .../test_matching.cpython-313-pytest-9.0.2.pyc | Bin 9622 -> 0 bytes 26 files changed, 2 insertions(+) delete mode 100644 ai-vision-service/app/__pycache__/__init__.cpython-313.pyc delete mode 100644 ai-vision-service/app/__pycache__/config.cpython-313.pyc delete mode 100644 ai-vision-service/app/__pycache__/main.cpython-313.pyc delete mode 100644 ai-vision-service/app/api/__pycache__/__init__.cpython-313.pyc delete mode 100644 ai-vision-service/app/api/__pycache__/dependencies.cpython-313.pyc delete mode 100644 ai-vision-service/app/api/__pycache__/routes.cpython-313.pyc delete mode 100644 ai-vision-service/app/api/__pycache__/schemas.cpython-313.pyc delete mode 100644 ai-vision-service/app/clustering/__pycache__/__init__.cpython-313.pyc delete mode 100644 ai-vision-service/app/clustering/__pycache__/clusterer.cpython-313.pyc delete mode 100644 ai-vision-service/app/detection/__pycache__/__init__.cpython-313.pyc delete mode 100644 ai-vision-service/app/detection/__pycache__/cropper.cpython-313.pyc delete mode 100644 ai-vision-service/app/detection/__pycache__/detector.cpython-313.pyc delete mode 100644 ai-vision-service/app/embeddings/__pycache__/__init__.cpython-313.pyc delete mode 100644 ai-vision-service/app/embeddings/__pycache__/sqlite_store.cpython-313.pyc delete mode 100644 ai-vision-service/app/embeddings/__pycache__/store.cpython-313.pyc delete mode 100644 ai-vision-service/app/matching/__pycache__/__init__.cpython-313.pyc delete mode 100644 ai-vision-service/app/matching/__pycache__/matcher.cpython-313.pyc delete mode 100644 ai-vision-service/tests/__pycache__/__init__.cpython-313.pyc delete mode 100644 ai-vision-service/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc delete mode 100644 ai-vision-service/tests/__pycache__/test_api.cpython-313-pytest-9.0.2.pyc delete mode 100644 ai-vision-service/tests/__pycache__/test_clustering.cpython-313-pytest-9.0.2.pyc delete mode 100644 ai-vision-service/tests/__pycache__/test_cropper.cpython-313-pytest-9.0.2.pyc delete mode 100644 ai-vision-service/tests/__pycache__/test_detection.cpython-313-pytest-9.0.2.pyc delete mode 100644 ai-vision-service/tests/__pycache__/test_embeddings.cpython-313-pytest-9.0.2.pyc delete mode 100644 ai-vision-service/tests/__pycache__/test_matching.cpython-313-pytest-9.0.2.pyc diff --git a/.gitignore b/.gitignore index 0089dc9ee36..b0ec9675541 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,8 @@ clover.xml .NO_AUTO_COMPOSER_MIGRATE storage/bootstrap/cache/* storage/image-jobs/* +**/__pycache__/** +.coverage # used by Vite public/hot diff --git a/ai-vision-service/app/__pycache__/__init__.cpython-313.pyc b/ai-vision-service/app/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 7e58c5ec2d62a8fb9f3a002aee3755033166cd13..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 206 zcmey&%ge<81j}ab%Txf;k3k$5V1zP0a{w7r8G;##7}6OvnW`jxDw8u(QxzON6~Z!$ zGxPHlf>VpiGLuvF{4^PFvB$@!sapm%O1Btdlzw7Cfqr~^W?p7Ve7s&k Vrxk;o^?{j@k?|ITd=U$f0|0w!IHmvq diff --git a/ai-vision-service/app/__pycache__/config.cpython-313.pyc b/ai-vision-service/app/__pycache__/config.cpython-313.pyc deleted file mode 100644 index 91cfca1bf2b779ea4991ed6595e6a04e7af5cd18..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2518 zcma)8&2Jk;6kpq8J8_(^+NqN?NjFIYHKO(f5Z@^DOG&G?bVU)W(Ri{uwuh{Dmzl9? zA`X!_fW(1ZkT~!U@K5kJAX!M(oQyc2-cqI9c{95S=Hq}RzkR=XGwqhKe4x_E$cT8&VJcMIeiGqca~#u%Sk!uJE`3?Pup70+{x@Bj&`#=Yg>!fhUK7T z%gHv-tC@H$&o$Ccu915s?c|rOPtmK1OP+5}R3{IA*^VNgxkR!su-!0dv38HgsLzP~ z;J_n+WUl=_5!9hlvY;($g~GP)+aiHB;nem+;!)3T@vv*tpwD<1bZH>%KH-e)`Bc~u zr!Dpb=6ud%$8I(s?KnI4@8Mh9*KguxvsSqIgmfdHzG=%21v7~l-lqr6rrl!H_v|i- zge}9wI=*=O!4AH2^JAC^fpx%I;i=waHKitf-oq|&JG7cs#qm9;3un8zZZ>^`xzbIJ z#ys)~IDL@PN8hk`3XVVRq~U zCeLxf;KDhcGI%P%MP6)7!~E$4&+r+8XV3Ah!E*_o=W{0Bg#?%QyusxJFYpTnUrca? zm(2Rb1TXQju`i$F1%odoc!ggycs0RmykhX>1lxSk;Pn_cmVl5O&Zcw4*>YZZ1My|& zMSiJ$wOUnEK)kancv^&0JpAHe52)6q|Da(j-HHRB%uO(@p9>v3P@69WFt0EASNd%aeR_#yUHp&NSC#{ucmc1)Mid#dD7 zNnM>!SavuSozVBxY?ny4Gd55Li%SB`!rot_Pjw|wr7n4bTg0UTN0j5J6Uy)@kkC?d z+9C096ox()?2xMb=ON#xT&SG*%x99SSs_DC+7#~*cb^8{;auG#lGJH;k9wZYUR6As z5)s*f6bW78cS0dmK5ApFu-6wLm_|{^rJ5uR_sz+xDHzl4_MjS=2|Yo%vdnlygn+m4 z4vTR_^WFkX`u$-mIv8R>E7T#|*r$E!tKxVfAEFv$@L{$mTi0H{RYj@*5XW^xO=!Kv ze5&TpG)x0G)O%7hF1!#HG!RU(KK;?US6xvP*xU@ps(>-vRL`fN7cu^_iV9cRFTYN^}MYaVcPx4tkFsH=@J(^*GPZXwifGJY-*v4y(-3(iYu{Nz*y+uE+$fPw%3zD3e2l8^A!G; zx?b;uU0UB`{rZj2?dg&$>JNDMDKJpf?;1+i3A+~G-!-@`ojG+9MfEsDEjr+N@MMHe zL$L}}`^tKj&SWP}iq^v709A(QlhO3tmv@I~Z&a?Bf!&jwRoOV&JwKG8FA-V>}l`G%};N}p$H(Gq@=faVmQ}_*>&hgH`w5(~BU!vmWcrHNUgYdj&ev;q!-P zrA=(po;>JyV+9d<84KX%15@R$6X>?G!-U=H1*Wptx;HnQH)sSYF0Qtz1oFqtxoXzh zw}_C?7;PQ`XY39MJRizLt#H3jIcHwnVWXO7JU|{D;4ra>$K&okiQNJ3q-nn_(A0v$ z=kEXY0iZiN#DVtQb32fa|NR9}D^yc4*EE0QQ+y3HRU8k<-+-Ye{Do%Yk@bAeDptOE z2l!)F+^bIT8W_~1ISVL+g6q>(^HK7ta()sV!MMq)II4DRQfDmm#M^OUc$dEoR((}M zD<_#$D)lUlQs|GOm9l@g-hH0?AeG7;zx>?N&y!BtN>81n^Qood{Zk7brx_S{rU#7o FKLK@y=Bxk! diff --git a/ai-vision-service/app/__pycache__/main.cpython-313.pyc b/ai-vision-service/app/__pycache__/main.cpython-313.pyc deleted file mode 100644 index cc96017c9730f1b3c174f763d9027f9cfae20ec3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3594 zcmai1&2JRR6|bJ@p6{{8HrNIeR2U2lS!PTG0W3grz+lfTws!_+S(&8W?yi|>Xm^jQ zdJML57^0O>vWF-KB&1#C6h)i=AgA0TMG6KYk6J+x?T55C6SI+`T=J^A+e6ltw&m%1 zUG?hKt5@}Y@8|7oHih8%_3!>{z5?kB{t-S=Uy!!H2H`OpLsdjkRjNkHQYk_sn9I>} zlt!VI%W_Gfis)0yu~MAIg&Zp^~>@9dz(cyxLpM9z=KLbAb*Ws^+SF z)gIn=SE9prhDXuP*nw*1&cSN>1nAbI`F`yP7`m=8cXECPYt5!*>Y8ghHm+&9>(I4A zDm7)hbPYEh({^#qq1bH@JUN4ZYBH`@C3Mx)Nde;Ib6^3@#V%nmLDy_-+INZWVh!sp<~l1loR-oZ z+a(V`mFqVtIo))cEelp>U>qDMFc&_+`hC-F;1x%|hn;0^i-Aq%63xI)4O@<8n07tI zw*|gic*bUCz2RQhbOMtMVii)~@W|#RFRj_O14dy7Y!<%zZ#7_wG4D9m)B~coAY#xv zps_Vu54>B^Y^_dcKH_C>eKbF%UY(x0TArD`;YBCyHBgQR!3Qx$ofd=uRMV5qW|g=u z_{F?TowzCszCf*KlAFIlY(h0y(2K%WX#yP7gFC^UMRE8=BS_l*Lj<9t2*ZtD+!+sV zgDow>sQ*;tQ0lg$Xp!3yPgiq6s_^B$^HkE`>4490o$lO9W)H0(^QzKC39=&sSr#uP zY{U7%TUb zdTkjzl(~mtHrmSf*p7p3++8b_7L>}-Q0}&&$LE6hfahc3DA+k3%H6h_xG34N+v9to zhP6M`yFI_>_~6d?(w^fxF|8)g%d6-ypx;8QaweSBZL6tjnxEwN#3B<$4WoMUe6%Lz zv+F;Zr;gFm1p?kdpu<24u16nwn)YB1QonC@DaOC>B{I7Uc&9Um2ds z*8p5Nph0q_L{h6Z0FF0ch?_u3(+3w{rBM&yxE7}9~zC-U3%4V-a zhkRc%-Sv@OFpF8E+%i!Iitn%K4MK{V zd3sfpv(wBkXGI84aYZxjLUYaQQw>tnT9&JZSCak^R^Zsd{tK^fP#e9Cp&#Twoo!Ep zpky}Xp=a{YXZ7FLKYy@UoZKi*ZWeEB6mL8)&iwWK-!A>-((}W&pUEG63%Y0j**nr1 z7}*>+zA3T4=}(Q#D<5oJ z`QU|I@eO2#pA0|GjJBr&olfFXCz0Ds;Ee?CB(lFr|0?}S_HA5NVsCp;ZuIlUX5q#L z{Pj+^r(Yj9@@VGa%nNz2GdRMfkLNl&U0=${*D`Eo|AxH($+@SzJ+kA+>A%Z`|1cmd z=$BKyQ+?9z>Vu&A{2ay~qXo1K$+&>#ltuXVAE-S6t_jWqsJ{Tb>Y{8%79tfT)atg2 zd@1pCabqEJ4CNJV1`69%!8-&jK;?xTXi*;(_8>PvY9?u&O{Dl83D0x_&xO=P)Ne>o zpI!tN9J>x~0n%tpfDK%bkShr@@0tQK^kIY8z(Qsn8wkjIc%s&_^@*ij7ikHbj9WDd zJ4WVK38kh%0678@1PccoNN^aDGJ?|pT?pxm@F`#`sS0sh)DHGH$v5K1!@v`|r767OIhphl;bG)ukz#_@cs-`ATP=gA zK0l&KxCQwr9f!ut^5E^{B@ndku`qtU;8^G>u9)f)Q+gIUuECF8hF2SH4Wc9Y4Se>u z(=R5 z=*TNH{&h6>QZBqs<~EZDpCu1Ix$r!BtR35mp~(L2$e2{zj)JtsrGL*$`=p^a<0yCV QlV7x>?GIm|5#huC0F17fQ~&?~ diff --git a/ai-vision-service/app/api/__pycache__/__init__.cpython-313.pyc b/ai-vision-service/app/api/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index c16a732d677927b9ef2925bda7b305269ad0e8b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 227 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 diff --git a/ai-vision-service/app/api/__pycache__/schemas.cpython-313.pyc b/ai-vision-service/app/api/__pycache__/schemas.cpython-313.pyc deleted file mode 100644 index baa99be87bda084221c02f7eb470e06c70333533..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4630 zcmb_f&2JmW72hR?A6k;4zAf2~yR{wLtS!=YV-)^KB3Y4Q`9l`GP5?IqmaE+%x#^0_ z%r5OxCjmYr=t(HxLOm4lt@S@q6glM>fk43SNk$KaZcfFb*S!W0oA=)CXEKTe*B^fOC;RsklJr;n)4gKhV7~){=aMEdNt3n6UHMLgMeamdRF$Ts z(~=fFE@`9~xf&Jo8R6O(Xybj_7}q91o9xrZxpoM&N}o2twJFf1`?N`}&46~ePdmi5 zSR6s?Fshatj?+~A zW}VI&wVLZ2KDBPBHCHW_=4;JTiJQdKhDWWuvWG#+MS`JQhDYz?O9ind^k<6b2^oe` z+P@6{pG!rF$q@dCCbQ_P5+_5Y;PEB z8(-dXttRZO20LzcclERi8@Ft)Zun-^u2ob+EgL4CwfK4j^HX;XKrEF^!*SLO^Ot&q zIj}F=Q|+1$&R`B$nyRseuCH?xyYMk;SRSnXcv)M1u%a)`FD&Z!9xvQpT+|^p`bQ7% zfu}Na*Hv13q9WLJ*YiDywbyXq4q*k1Et|n!oMv7LlJ%he3o*d7z!t zbq_*no4Voq%wB8w6x0ll>^}UY;C7+vuG7Mr{iN`LYc|#a0k5#i+)v?!y@Fs)!LVna zaAMAexD^7Gx1@n;YIjyeXZ zLd-tInqIhzDnmIX7||M)3Y<8=;vtX6o(gU^ z3iGN5L~-B%+UvGsFiscD{4b|%{%?HB{KR`P23L^J6aKf)}@cR9ikswl` zLByef0*A6}TeN0Uj`GUO6idTwmO+8guq=uZ6r(7{P!JS90ujW@j%)aA92FQQfz%|9 zj&+3!1MeJ&&!xYRYr7N2TIASkg!S9BG;`|3_#el2re`!at00~p%0|+lh5l+78G%WaMi>%qr4QkzJ z0{&K>9fy?+S z)}_Gr$^<_JKkO0;1ZH3!z^F15xB(BTf#F@<9Ug@&J9YvBw9q0a+Ntc8vXeUT4LKoz z#ubBsz73Wi1T@y}UMlFzyGfpRCl(oV*-^BM@R0u%?XqDz4Msz>V-%mm5V1hvW8H*) zBuK17M2t$9fOA7{K6wWTn8QBkEWHEX847%V?<|OZuxA9=Gi|JFZSqMwGxF?SK;99E z1GXfS(( zhbE7=EzofS_ZxK!8bZ~ER;HKcRL^&zsd;MqRdG~&W6hz8=oMxScvJ<;XsL9#Y+|DX zjV?9J>VdDk!g8<;HiH7o-3m)}hXO-h`L3f51@8GsZ!tav38zi2h%y+E_r*G#JsA6+ zfaOhW=jkIM`cgqmB71moH`;51#L-X~-E?sX ze|81@gjM1{aFyo-4g@a{gSh%GIzoYO=3$G0T)n#snIN^3I{6JbDX_sQhp@qIAO<*u@?_3@Fuw3xG~>txlHJhZO$oPhkH=*9vbkW4t$%;uA>7LK9^WCFSv0W+#Hl+ zQ4a-QRJ0uFl;%)}ViSJeX%Gm+qM%5dEdN{O>Q$pTn0pACC@ApS|G~7$bYT!|`87D% zINc?W%9(-(qEy1lqZas{OQk%dFWh~vYJ3+Z=D@G$83s{e0j((T@gB}VBrfe{Mz=2R zWKOlnseqIOD{z(xzupxMNBcKn@TL>f`-V$E9pK+^&;%x79K({V#o16LkxK@{x2fUy zND6_*Z^1YijSm+Aj4GcEXo&2d<{!U0c4pKHvb|IU`Mv?)z!?3Nobo;Ld_dM**9oFf zy$6=o55W^df$!HVt(SJw*=Kj#P;*0P-wgP`;_<6qp@j5EY zGJIvpR z%2&6qbtJfTrc&~pyp_X6bFe5XzqS3iBf+gRl|Tnz!Fy+d$cBth!h2_0k#BB2{)YrN H{?7jbPvT7P diff --git a/ai-vision-service/app/clustering/__pycache__/__init__.cpython-313.pyc b/ai-vision-service/app/clustering/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index e573e9c3c6b49ea3c42bfc9bbbf28e694d952245..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 227 zcmey&%ge<81j}dc%hUqWk3k$5V1zP0a{w7r8G;##7}6OvnW~iC5|dLEQgf41Q&KYX z(iM_(N{dTUi$F{b7pGunM?XzHKTXD4?D6p_`N{F|D;Yk6Ou7}IpOK%Ns-KiurtgxU zT$-DjS5mAWP?VpQnha$4R3>Mnrs^kV>Xu~|XXfYW7N-`KWhMhf3JUZQR_MpaXXa&= p#K-FuRNmsS$<0qG%}KQ@Vgp(La&Ixntsj^f85wUeXcn;mIRN4QKwJO- diff --git a/ai-vision-service/app/clustering/__pycache__/clusterer.cpython-313.pyc b/ai-vision-service/app/clustering/__pycache__/clusterer.cpython-313.pyc deleted file mode 100644 index e30b78c545adbc961dede35b17ee2e9ce7a84f77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3367 zcmaJ@-)|g89iO|sUp~h_YR9xm95M+3d{OQ$wy_{?<2H3jn>4jrZyZG!Yj1CN?mYJ1 z>}F=yuKUy{umlgZB5GV7lJb=B7gR#3KtclXRFk5bg_o*DC=w4XhE{#!``z91ogKtT zH#;};ozM68_dC0NeMJJ}(B1d>`>^^1|FlOYUZi%xT@|nbCpm0_eU*&$;VWt%H z()0T4#koqc_=0o;Pf_K&lG(Je4?|Hde7B9Hs2TWf%k;TrI?l#fy3YM&H}I)sw*rog zivBXAA^;I2bsL>~fKg{udCcMs4qUjProCngi&g0NO>4PLNiA1OraV{JstA`%>hM+O zY*3pkp9{;U+-Ab({>IZ(xLh&nxgepUV~$y84z1P7hLd{vR0pnSVKW+Q}kjNDHAg!mQrl(~_&+7$UJ4#kED=C@Pd(a0f8JW|w zGT+RVieVobe>S!_laKlU&in_sDA`hv85V8VaYF13I(CT(1=^LLHx-yk_drzBp$Q23 z5UhI9^BnL%q(tNbI)|nxQLc*X%yG&p;6A$2l`+>QdKfR)nPmowO&81b`u}#6?J@O0r3El%Odw%ys+?o_P?0ew+6 z)ifYe$~-kqUp8Okt)SJ0I)kBvSZC}AKp*r(X&{_@Bfhrem?o+k~x|yJ{XgZi5 z{8qq{2w{FlQx5s$u7mC4Xr^P`eycMKq{yuf>K;X2rD!Gj6J-%rCitJ)9fieha&yp$ zwsE>i9Vk@sXhKHLJxGTc1N;;oGP-P0(!z{lP9xOtsJH7xAtP`{_-b|8ZLw;duT?L( zR?vd7psKG(cZFH7xVB*}GgdWud5tT`_OfDfja#tedDTQQP^7(Nvf^!o1;YRj_=XXF zZC^H5+WQoED@^S-$j;#>f7-J%`t3g#j_kq)v66C(Yf^w;WPpEyvbV`XYAo6�IJ= zIUSdFay2E=6i$c94edroX;q@q!{o(0Apy{N-&xRJNx$4fNH3v8WQ8U&i%FXxWImTb zc70C8SeYBS$FMFh)#HeqZa}IMYW|PD1~Cm zTAsfVDbQ{J2h0c9pm{`ADN4*q==!<1yTU4K`x_p6gpqK)1eFI`g9;0)jCo)t)r=kf z*|T9T?k6Qp9)dlIaUSO4UW0fFn~q=V4+qq0^qQ(9YRX}xObai}no^n@VLmeX`K{o;1Fz37Iqc9WA+Uwja^-9PI5)K?tH|D1` zxE-d9P~*0OirK1%`8BA5u2f+b*{LvX*Fge&A}NJHP(a&5Vrqu@M1gSRa`!u?t=DOw zQGbN#4f4e(89cEwJhFM^?}alTpFH^%M2(iA`<0fBbJlhu#>o9Ug=ug5?SM<+iTo!%auemMHfX71C0;h)dm(e7W`K0N+#VB&+q#8w z_Jo-^SFln>ehY|SKaT!d^aFS|EJBE!jRJKsy1KNqBqxz(g z4WSjZybXyxK1O^RS9@H-!ZWcJGjawFFy$1|ex{{Tsn65dROUbZB=wC?$k3O$@1-XH JMPQ0<{}-GDyz@1)MLl@bB&++9%76;6zP!*A1eJo@Bjb+ diff --git a/ai-vision-service/app/detection/__pycache__/cropper.cpython-313.pyc b/ai-vision-service/app/detection/__pycache__/cropper.cpython-313.pyc deleted file mode 100644 index 720dfb422aee4d35050f816167c0d5e8dca2346a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4496 zcmeHL&2Jmm5nq0byQIG5BzBx`Wjm1^QIurKk!2-@zARHxB!L^Es%T)bB%i1aNG|(! zDTz5ThoV+uz)s>e4xF|Dnu7!MV4ykYl7GMo4-#GzKtR$KJ~gP5duMjJ{3;FSSQLGL zX6DVjdHX(Q=J%+_!645;`{y6NP~R6g?py5G&1DIW$2URvEq9KSILb*5$+_%Ua#E*= zvTNBzT_$%^H*oi|XUR*wrp>eLTWX_iB6r<8P?=Nla_Hq<_Al|2hfzK%NNv{xlK&+b z@d79DG$;ws4^;XcQt%}Arkl1)ol?6gb);Q`U2hpzl_U|9RLhHLk|R_xR4peKbTyY2 z*VRl$+a2clb*iNbNurCR6C>iDNtf0a7EL#ONK7a?xi}soxuljNsUeXPB~=s+O(fX_ zNr`GsT$0vS#k^9?Xi7>S<{#lM1|3XLayiW~KdG-mKlsKk*B9iuYYTJBk<~>|cp_OP zO+aw3E5`Pt-+>!*sX2rB=b~%tvJ}}|NMA79msgl)3#K<%hrB*BKM(%$)tR|B)}l!d zey!5lfWvh-9v?aJD&n@Di$cG-^>Z<9!eMEn^|Mq4N)Fqa^Ta$+xRT~pfLZhDjT0g8 z&b4j4WODz;MU(RzizXLhd>q$hHd7Ob2~C<{9D>Sfl5^u`2!d^*vf^BOgYEYXwvKqv z_KSDeQp{_wd*huDC}RPn)%iMG`uM6o_e`H)gHIRizssJ##^XJ<9Ba4z>vSD0be%19 zUAAuAZA-cCSa-C~p1sE7Cv3SUjXUe^`B~duYbkD27St} zA#4||d?upOl%dUOyk(aFNc9<-)~9$8nu@6>=OOQ=#F>PyWeNs?95%KwvyE*c>RN$9 z?J&m;n=X4(;;KfoN=DTo?=xh}5J@UcL~YA-NN9yz3JXg@+Y<*yhDSo9!y|*1Yq7!A zfO7OxJ$Ka#QHTpYKGIBQE2V@Jspz?dRf)(3kTUyBIl*kcY@ zuX(u4YaJI<=4_gIfXKTR+lCz_ba~HWxIgHD4H?+-N(ybrip;q;%A7^`+ZzKuMlmOu zD^b|edqKj0Y;J5dysc$PIHB%@=e1-ZOLB%D2CRCMBtcjyCbtO*D{5#*)dAE(I-xsi z5;XaII7JK?1llm3U7pb4e31n!2uNh}Krg{}v0CUMNTw3^!%0r)ymkJa^S3w4ym-hD z9Pk78`^tQ{GmacdoNV>i>2AGn|n87wu9fD$;{&+UcZXna?zZ1V-j3Wz-hJ_Ye<^a{4t*_j9SWzu5>DNn`{T+- zEB98*!pn!k*nu$i;FYp)`JwyrV>mUgHSQw%J{@=CZkz#ID^#t=>Y#l}&(ra_6}uIZkH&(GPj*Lb{&wKb^$pQ~tF^J#^<5158Awh!YgU-9%&)yi7o*r%;EriDF>W(?~=l z_)?_!9HeKE;Byc#Rj%s=`HkSF3Qodhd;wgC)f3{Kfv)~jH?f)k82;^Lj?hnw$jej=3nhRS^C zke@ioSBBb|b?8w}8Mz$pG^wC)+v$+~P)GH=|J%xgyp zA9wf$WiEJK!UWT(^VEgP)2E=X;aBkT20NKgmQz|%mKiV0TLqYz$T9_ZuEZwBI`bOE zJd_%WHI}*7BP-U^)Q7(VGHSwFKZZB_?3U|(&BILiQlpytUI;MYIb@V%?&F}mh1SGv$ diff --git a/ai-vision-service/app/detection/__pycache__/detector.cpython-313.pyc b/ai-vision-service/app/detection/__pycache__/detector.cpython-313.pyc deleted file mode 100644 index 5ede44a9fc2549e0d77369f4a73882713f9ee28e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6952 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 diff --git a/ai-vision-service/app/embeddings/__pycache__/sqlite_store.cpython-313.pyc b/ai-vision-service/app/embeddings/__pycache__/sqlite_store.cpython-313.pyc deleted file mode 100644 index d5552f4bcb23979d6657c319362432acbdcece38..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7847 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 diff --git a/ai-vision-service/app/embeddings/__pycache__/store.cpython-313.pyc b/ai-vision-service/app/embeddings/__pycache__/store.cpython-313.pyc deleted file mode 100644 index c6820411d36dbf4f7b948796042fd3efb5629bd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2474 zcmbVNJCEB&5MD~8PU7j#kMk=|a@oWwPQZz>4Y)ZZ0SufC_;qp26aj35qR5p*nCgPB6%%8&jgwc*Wx!m+t=utgSD?aaUcFPjZGw5*0*kXp)&=$%bIps) zwH}$*s>SJRr8lSzYOSTcU#|D00zXjv#;7O4FiHllNYhmrLCC z2ZHzf;HgML$w!$~JV=v%3L~yku7;81QKAI&{XlR@E55=(*#3ANM}e=RG{F<0;My`2 zV{{}E#nV2Sk+T4V!n5UFA{j&pt`Acw635=B99l-c>?cX8v^&|VM-V+0s+!eQ`?dt9lGzRt7YQB0D&;2G|wfDn3LXKc@4bOiAi^$&^jbkxFWQrUM zsCxoI0^x^k>GuWyekc%3KaNGn`;d+_dKm!W3F28Mq_T&862=0gV+UpK8V3VM{BO)m zjl;lbRrdb8k9H9<>L&-JZ}O*Ng8q9G9x2K1P7qhYZxX$?wkgiOHcu2xbY|UdtW7*h z(Tl=vm-{h=JmKOwI+vU1hsXypj?p9}mBB!6*&LsvXScYkh{5@--7ekhXr~*fT~sW; zf2ZZ>qYE^*__Zrn+aYQok*GF5=GP(60r%q+)wnAH)RtrK{LEU_+>|2j=c`9A&KnDF zl2F6Q@_U_OIue~;wABx~V} zCwbKm!~FD(d2Q*+b8zhpUMI2q*$Xy3%^J-aPFYZ)v3%Y~-OC4Jl_ zEPq1gW#%U_b2xoHEq*UbqESW_Cb1p0%Nrs!;WD6}xpCRK+`i&mF2xvQ1PV|2 z&-JM!L!N2|51=p?&c?ZL-vq9lm3kZQAJ)GQcXK_>FX z;zVxgJSA~Ncoi(4I=I-A!iQj(AN#^M|FUSIhdKA3FSsYZ=g)h%-)kQtUg6gu<_AFQ zLJJRwl}?$8p;M9MuMF4-w^mq}KROtETmxSFsNP;qv!# zjMP2UJ?3#*v~k*MzFu3OTKJyU*qINeOEjyqlWTf!YBT%Ocgwi; z*G9!Ex`1}P(^`TJ^h=5LGej2?1M1FsO-;tAcpx1DkyS)FGorzGTYf8iqBcVGUYKj@+WH?k*&oM|$+&i7E95SY{Klu4LbR9IvNT{}~m zlR1{>Wtvbw5#p$&5Kq$eCZF`Ww5UsF!89nt or>gkh%Z0!aoe;~a_0dY{V_m@7jDx;kYx||2A*Ixl#Ee4u0s)giu>b%7 diff --git a/ai-vision-service/app/matching/__pycache__/matcher.cpython-313.pyc b/ai-vision-service/app/matching/__pycache__/matcher.cpython-313.pyc deleted file mode 100644 index dea4a3c1b638bc748424976f98ad96f97b16b65e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2765 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 diff --git a/ai-vision-service/tests/__pycache__/test_api.cpython-313-pytest-9.0.2.pyc b/ai-vision-service/tests/__pycache__/test_api.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index e75d20f9d72a97f246499f67a9e09f37ec75d32e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18415 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 diff --git a/ai-vision-service/tests/__pycache__/test_clustering.cpython-313-pytest-9.0.2.pyc b/ai-vision-service/tests/__pycache__/test_clustering.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index 7227414a98a9797779da42e46a0caa5400a36d97..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11128 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 m0^jkVJUasCazyaYM` diff --git a/ai-vision-service/tests/__pycache__/test_detection.cpython-313-pytest-9.0.2.pyc b/ai-vision-service/tests/__pycache__/test_detection.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index 512852b92f6984629df12240f894731818ddcb6a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19853 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 XIl2(?$a@zaaLSSYsB+5s)#LvL0Uv1- diff --git a/ai-vision-service/tests/__pycache__/test_embeddings.cpython-313-pytest-9.0.2.pyc b/ai-vision-service/tests/__pycache__/test_embeddings.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index 4f5a3db8bc831cf783021051771b1ce4e4cc60b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15020 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 TJ?(rEa*^--{FyespHBC`9W+;q From 8e23309f0c51248328bec585888fc62c54ba074b Mon Sep 17 00:00:00 2001 From: ildyria Date: Sun, 22 Mar 2026 21:32:17 +0100 Subject: [PATCH 11/14] oversight --- ai-vision-service/README.md | 4 +--- ai-vision-service/app/api/routes.py | 16 ++++++++++++---- ai-vision-service/app/config.py | 7 +++---- ai-vision-service/app/main.py | 10 ++++++++++ ai-vision-service/tests/conftest.py | 2 -- ai-vision-service/tests/test_smoke.py | 3 +-- 6 files changed, 27 insertions(+), 15 deletions(-) diff --git a/ai-vision-service/README.md b/ai-vision-service/README.md index 49fd8899989..b3564531404 100644 --- a/ai-vision-service/README.md +++ b/ai-vision-service/README.md @@ -59,8 +59,7 @@ All variables are prefixed `VISION_FACE_`. | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `VISION_FACE_LYCHEE_API_URL` | Yes | — | Lychee base URL for callbacks | -| `VISION_FACE_API_KEY` | Yes | — | API key expected on inbound requests from Lychee | -| `VISION_FACE_LYCHEE_API_KEY` | Yes | — | API key sent on outbound callbacks to Lychee | +| `VISION_FACE_API_KEY` | Yes | — | Shared API key: validated on inbound requests from Lychee, and sent on outbound callbacks to Lychee | | `VISION_FACE_MODEL_NAME` | No | `buffalo_l` | InsightFace model pack | | `VISION_FACE_DETECTION_THRESHOLD` | No | `0.5` | Confidence filter for detected faces | | `VISION_FACE_MATCH_THRESHOLD` | No | `0.5` | Cosine-similarity cutoff for selfie matching | @@ -108,7 +107,6 @@ docker build -t lychee-ai-vision . docker run --rm \ -e VISION_FACE_LYCHEE_API_URL=http://lychee \ -e VISION_FACE_API_KEY=changeme \ - -e VISION_FACE_LYCHEE_API_KEY=changeme \ -v /path/to/photos:/data/photos:ro \ -v ai-vision-embeddings:/data/embeddings \ -p 8000:8000 \ diff --git a/ai-vision-service/app/api/routes.py b/ai-vision-service/app/api/routes.py index 01fa6c43bee..eeb528bc88b 100644 --- a/ai-vision-service/app/api/routes.py +++ b/ai-vision-service/app/api/routes.py @@ -63,11 +63,11 @@ async def detect( Results are POSTed back to Lychee's results endpoint once detection completes. """ - resolved = Path(body.photo_path).resolve() + resolved = Path(settings.photos_path.removesuffix("/") + "/" + body.photo_path.removeprefix("/")).resolve() photos_root = Path(settings.photos_path).resolve() if not str(resolved).startswith(str(photos_root) + "/") and resolved != photos_root: - raise HTTPException(status_code=400, detail="photo_path is outside the allowed directory") + raise HTTPException(status_code=400, detail=f"photo_path {resolved} is outside the allowed directory") if not resolved.is_file(): raise HTTPException(status_code=400, detail="photo_path does not exist or is not a file") @@ -213,7 +213,11 @@ async def _run_detection_job( response = await client.post( callback_url, json=payload.model_dump(), - headers={"X-API-Key": settings.lychee_api_key}, + headers={ + "X-API-Key": settings.api_key, + "Content-Type": "application/json", + "Accept": "application/json", + }, timeout=30.0, ) response.raise_for_status() @@ -240,7 +244,11 @@ async def _send_error_callback(photo_id: str, error_code: str, message: str, set await client.post( callback_url, json=payload.model_dump(), - headers={"X-API-Key": settings.lychee_api_key}, + headers={ + "X-API-Key": settings.api_key, + "Content-Type": "application/json", + "Accept": "application/json", + }, timeout=10.0, ) except Exception: diff --git a/ai-vision-service/app/config.py b/ai-vision-service/app/config.py index 620754f90b5..e32d55d48ec 100644 --- a/ai-vision-service/app/config.py +++ b/ai-vision-service/app/config.py @@ -20,10 +20,9 @@ class AppSettings(BaseSettings): """Lychee instance base URL for callbacks (e.g. ``http://lychee``). No trailing slash.""" api_key: str - """API key this service validates on *inbound* requests from Lychee (``X-API-Key`` header).""" - - lychee_api_key: str - """API key sent as ``X-API-Key`` on *outbound* callbacks to Lychee.""" + """Shared API key used in both directions: validated on *inbound* requests from Lychee + (``X-API-Key`` header) and sent as ``X-API-Key`` on *outbound* callbacks to Lychee. + Must match ``AI_VISION_FACE_API_KEY`` in the Lychee ``.env``.""" # --- Model --- model_name: str = "buffalo_l" diff --git a/ai-vision-service/app/main.py b/ai-vision-service/app/main.py index a7fe018322e..826b553ddb7 100644 --- a/ai-vision-service/app/main.py +++ b/ai-vision-service/app/main.py @@ -9,7 +9,17 @@ from __future__ import annotations import logging +import warnings from concurrent.futures import ThreadPoolExecutor + +# insightface uses the deprecated SimilarityTransform.estimate() API from scikit-image >=0.26. +# Suppress until insightface ships a fix upstream. +warnings.filterwarnings( + "ignore", + message=r"`estimate` is deprecated", + category=FutureWarning, + module=r"skimage", +) from contextlib import asynccontextmanager from typing import TYPE_CHECKING, Any diff --git a/ai-vision-service/tests/conftest.py b/ai-vision-service/tests/conftest.py index e4faeb49ab7..179a06f774b 100644 --- a/ai-vision-service/tests/conftest.py +++ b/ai-vision-service/tests/conftest.py @@ -34,7 +34,6 @@ def mock_settings() -> AppSettings: m = MagicMock(spec=AppSettings) m.api_key = "test-api-key" m.lychee_api_url = "http://lychee-test" - m.lychee_api_key = "test-lychee-key" m.photos_path = "/tmp" # overridden where needed m.match_threshold = 0.5 m.max_faces_per_photo = 10 @@ -110,7 +109,6 @@ def _override_settings() -> AppSettings: m = MagicMock(spec=AppSettings) m.api_key = "test-api-key" m.lychee_api_url = "http://lychee-test" - m.lychee_api_key = "test-lychee-key" m.photos_path = str(tmp_path) m.match_threshold = 0.5 m.max_faces_per_photo = 10 diff --git a/ai-vision-service/tests/test_smoke.py b/ai-vision-service/tests/test_smoke.py index 8743e10aa9c..92672cef83c 100644 --- a/ai-vision-service/tests/test_smoke.py +++ b/ai-vision-service/tests/test_smoke.py @@ -11,8 +11,7 @@ Prerequisites: - Docker and docker-compose available in PATH - ``docker-compose.minimal.yaml`` with the ``ai-vision`` service block - - ``VISION_FACE_API_KEY`` and ``VISION_FACE_LYCHEE_API_KEY`` env vars set - (or defaults are used for local testing) + - ``VISION_FACE_API_KEY`` env var set (or the default is used for local testing) """ from __future__ import annotations From 5e48dbb7e6ed057bb0f15e74486715a4b9e394af Mon Sep 17 00:00:00 2001 From: ildyria Date: Sun, 22 Mar 2026 22:19:57 +0100 Subject: [PATCH 12/14] fix linting --- ai-vision-service/app/main.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ai-vision-service/app/main.py b/ai-vision-service/app/main.py index 826b553ddb7..80a84a94f7a 100644 --- a/ai-vision-service/app/main.py +++ b/ai-vision-service/app/main.py @@ -11,6 +11,13 @@ import logging import warnings from concurrent.futures import ThreadPoolExecutor +from contextlib import asynccontextmanager +from typing import TYPE_CHECKING, Any + +from fastapi import FastAPI + +from app.api.routes import router +from app.config import AppSettings, get_settings # insightface uses the deprecated SimilarityTransform.estimate() API from scikit-image >=0.26. # Suppress until insightface ships a fix upstream. @@ -20,13 +27,6 @@ category=FutureWarning, module=r"skimage", ) -from contextlib import asynccontextmanager -from typing import TYPE_CHECKING, Any - -from fastapi import FastAPI - -from app.api.routes import router -from app.config import AppSettings, get_settings if TYPE_CHECKING: from collections.abc import AsyncGenerator From 00b17a2691455f7b45b26a1ac1b5462e70d8c4a9 Mon Sep 17 00:00:00 2001 From: ildyria Date: Sun, 22 Mar 2026 22:33:21 +0100 Subject: [PATCH 13/14] fix tests --- ai-vision-service/tests/conftest.py | 8 ++++++++ ai-vision-service/tests/test_api.py | 16 ++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/ai-vision-service/tests/conftest.py b/ai-vision-service/tests/conftest.py index 179a06f774b..f0e7e557f9c 100644 --- a/ai-vision-service/tests/conftest.py +++ b/ai-vision-service/tests/conftest.py @@ -5,6 +5,7 @@ import io from concurrent.futures import ThreadPoolExecutor from contextlib import asynccontextmanager +from pathlib import Path from typing import TYPE_CHECKING, Any from unittest.mock import MagicMock @@ -127,6 +128,13 @@ def client(test_app: FastAPI) -> TestClient: return c +@pytest.fixture +def photos_path(test_app: FastAPI) -> Path: + """Return the photos_path directory configured in the test app's settings override.""" + settings: AppSettings = test_app.dependency_overrides[get_settings]() + return Path(settings.photos_path) + + # --------------------------------------------------------------------------- # Synthetic test image # --------------------------------------------------------------------------- diff --git a/ai-vision-service/tests/test_api.py b/ai-vision-service/tests/test_api.py index a02a03ea58f..3c3dc2b53a5 100644 --- a/ai-vision-service/tests/test_api.py +++ b/ai-vision-service/tests/test_api.py @@ -72,15 +72,15 @@ def test_match_requires_api_key(client: TestClient) -> None: # --------------------------------------------------------------------------- -def test_detect_returns_202(client: TestClient, tmp_path: Path) -> None: +def test_detect_returns_202(client: TestClient, photos_path: Path) -> None: """Valid request to POST /detect must return 202 Accepted.""" - photo = tmp_path / "photo.jpg" + photo = photos_path / "photo.jpg" photo.touch() with patch("app.api.routes._run_detection_job") as mock_job: response = client.post( "/detect", - json={"photo_id": "photo-123", "photo_path": str(photo)}, + json={"photo_id": "photo-123", "photo_path": "photo.jpg"}, headers={"X-API-Key": "test-api-key"}, ) assert response.status_code == 202 @@ -98,24 +98,24 @@ def test_detect_rejects_path_traversal(client: TestClient, tmp_path: Path) -> No assert response.status_code == 400 -def test_detect_rejects_nonexistent_file(client: TestClient, tmp_path: Path) -> None: +def test_detect_rejects_nonexistent_file(client: TestClient) -> None: """photo_path that does not exist must be rejected with 400.""" response = client.post( "/detect", - json={"photo_id": "p1", "photo_path": str(tmp_path / "missing.jpg")}, + json={"photo_id": "p1", "photo_path": "missing.jpg"}, headers={"X-API-Key": "test-api-key"}, ) assert response.status_code == 400 -def test_detect_background_task_called_with_correct_args(client: TestClient, tmp_path: Path) -> None: - photo = tmp_path / "photo.jpg" +def test_detect_background_task_called_with_correct_args(client: TestClient, photos_path: Path) -> None: + photo = photos_path / "photo.jpg" photo.touch() with patch("app.api.routes._run_detection_job") as mock_job: client.post( "/detect", - json={"photo_id": "photo-xyz", "photo_path": str(photo)}, + json={"photo_id": "photo-xyz", "photo_path": "photo.jpg"}, headers={"X-API-Key": "test-api-key"}, ) args = mock_job.call_args[0] From aea97c1f56dc151ab0344d6497bd8c55e8e6a6d8 Mon Sep 17 00:00:00 2001 From: ildyria Date: Sun, 22 Mar 2026 22:37:26 +0100 Subject: [PATCH 14/14] Fix linting --- ai-vision-service/tests/conftest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ai-vision-service/tests/conftest.py b/ai-vision-service/tests/conftest.py index f0e7e557f9c..1a4929602ca 100644 --- a/ai-vision-service/tests/conftest.py +++ b/ai-vision-service/tests/conftest.py @@ -20,7 +20,6 @@ if TYPE_CHECKING: from collections.abc import AsyncGenerator - from pathlib import Path from fastapi import FastAPI