Merge branch 'develop' — erster Production-Deploy-Pfad.

This commit is contained in:
Lars 2026-10-03 10:54:09 +02:00
commit d280575aca
29 changed files with 3062 additions and 3 deletions

16
.dockerignore Normal file
View File

@ -0,0 +1,16 @@
.git
.gitea
.specs
docs
.env
.env.*
!.env.example
.venv
.pytest_cache
.mypy_cache
.ruff_cache
__pycache__
*.pyc
*.pyo
*.md
!README.md

23
.env.example Normal file
View File

@ -0,0 +1,23 @@
# Copy to .env next to docker-compose*.yml. Never commit .env.
# Dev server: /home/lars/docker/buyingagent-dev/.env
# Prod server: /home/lars/docker/buyingagent/.env
# ─── local / production (docker-compose.yml) ────────────────────────────────
DB_NAME=buyingagent
DB_USER=buyingagent
DB_PASSWORD=change_me
ENVIRONMENT=production
ENGINE_BUILD=dev
BUYING_AGENT_API_BIND=127.0.0.1
BUYING_AGENT_API_PORT=8000
# Production on the Raspberry Pi (192.168.2.49):
# BUYING_AGENT_API_BIND=0.0.0.0
# BUYING_AGENT_API_PORT=8006
# ─── development server (docker-compose.dev-env.yml) ────────────────────────
# DB_NAME=buyingagent_dev
# DB_USER=buyingagent_dev
# DB_PASSWORD=dev_password_change_me
# ENVIRONMENT=development
# BUYING_AGENT_API_PORT=8095

View File

@ -0,0 +1,65 @@
name: Deploy Development
on:
push:
branches: [develop]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy BuyingAgent to development
run: |
set -e
echo "=== Deploying BuyingAgent to DEVELOPMENT ==="
REPO="http://192.168.2.144:3000/Lars/BuyingAgent.git"
TARGET="/home/lars/docker/buyingagent-dev"
mkdir -p "$TARGET"
cd "$TARGET"
if [ ! -d .git ]; then
git clone -b develop "$REPO" .
else
git fetch origin develop
git checkout develop
git reset --hard origin/develop
fi
if [ ! -f .env ]; then
cat > .env <<'EOF'
DB_NAME=buyingagent_dev
DB_USER=buyingagent_dev
DB_PASSWORD=dev_password_change_me
ENVIRONMENT=development
BUYING_AGENT_API_PORT=8095
EOF
echo "created development .env"
fi
if grep -q '^BUYING_AGENT_API_PORT=' .env; then
sed -i 's/^BUYING_AGENT_API_PORT=.*/BUYING_AGENT_API_PORT=8095/' .env
else
echo 'BUYING_AGENT_API_PORT=8095' >> .env
fi
export ENGINE_BUILD BUYING_AGENT_API_PORT
ENGINE_BUILD="$(git rev-parse --short HEAD)"
BUYING_AGENT_API_PORT=8095
docker compose -f docker-compose.dev-env.yml build --no-cache api
if ! ENGINE_BUILD="$ENGINE_BUILD" BUYING_AGENT_API_PORT="$BUYING_AGENT_API_PORT" \
docker compose -f docker-compose.dev-env.yml up -d --wait; then
echo "compose up --wait failed — api logs:"
docker compose -f docker-compose.dev-env.yml logs api --tail 150 || true
docker compose -f docker-compose.dev-env.yml ps || true
exit 1
fi
if ! curl -sf http://localhost:8095/health/live; then
echo "DEV /health/live not reachable — api logs:"
docker compose -f docker-compose.dev-env.yml logs api --tail 150 || true
exit 1
fi
echo "DEV /health/live OK"
if ! curl -sf http://localhost:8095/health/ready; then
echo "DEV /health/ready failed — api logs:"
docker compose -f docker-compose.dev-env.yml logs api --tail 150 || true
exit 1
fi
echo "DEV /health/ready OK"
echo "=== BuyingAgent DEV deploy complete (${ENGINE_BUILD}) ==="

View File

@ -0,0 +1,74 @@
name: Deploy Production
on:
push:
branches: [main]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy BuyingAgent to production
run: |
set -e
echo "=== Deploying BuyingAgent to PRODUCTION ==="
REPO="http://192.168.2.144:3000/Lars/BuyingAgent.git"
TARGET="/home/lars/docker/buyingagent"
mkdir -p "$TARGET"
cd "$TARGET"
if [ ! -d .git ]; then
git clone -b main "$REPO" .
else
git fetch origin main
git checkout main
git reset --hard origin/main
fi
if [ ! -f .env ]; then
cat > .env <<'EOF'
DB_NAME=buyingagent
DB_USER=buyingagent
DB_PASSWORD=change_me
ENVIRONMENT=production
BUYING_AGENT_API_BIND=0.0.0.0
BUYING_AGENT_API_PORT=8006
EOF
echo "created production .env — set a unique DB_PASSWORD after first start"
fi
if grep -q '^BUYING_AGENT_API_BIND=' .env; then
sed -i 's/^BUYING_AGENT_API_BIND=.*/BUYING_AGENT_API_BIND=0.0.0.0/' .env
else
echo 'BUYING_AGENT_API_BIND=0.0.0.0' >> .env
fi
if grep -q '^BUYING_AGENT_API_PORT=' .env; then
sed -i 's/^BUYING_AGENT_API_PORT=.*/BUYING_AGENT_API_PORT=8006/' .env
else
echo 'BUYING_AGENT_API_PORT=8006' >> .env
fi
export ENGINE_BUILD BUYING_AGENT_API_BIND BUYING_AGENT_API_PORT
ENGINE_BUILD="$(git rev-parse --short HEAD)"
BUYING_AGENT_API_BIND=0.0.0.0
BUYING_AGENT_API_PORT=8006
docker compose build --no-cache api
if ! ENGINE_BUILD="$ENGINE_BUILD" \
BUYING_AGENT_API_BIND="$BUYING_AGENT_API_BIND" \
BUYING_AGENT_API_PORT="$BUYING_AGENT_API_PORT" \
docker compose up -d --wait; then
echo "compose up --wait failed — api logs:"
docker compose logs api --tail 150 || true
docker compose ps || true
exit 1
fi
if ! curl -sf http://localhost:8006/health/live; then
echo "PROD /health/live not reachable — api logs:"
docker compose logs api --tail 150 || true
exit 1
fi
echo "PROD /health/live OK"
if ! curl -sf http://localhost:8006/health/ready; then
echo "PROD /health/ready failed — api logs:"
docker compose logs api --tail 150 || true
exit 1
fi
echo "PROD /health/ready OK"
echo "=== BuyingAgent PROD deploy complete (${ENGINE_BUILD}) ==="

35
.gitea/workflows/test.yml Normal file
View File

@ -0,0 +1,35 @@
name: Test Suite
on:
push:
branches: [develop]
workflow_run:
workflows: ["Deploy Development"]
types: [completed]
workflow_dispatch:
jobs:
smoke-dev:
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Non-destructive smoke against the running Dev API
run: |
set -e
echo "wait for live Dev API (buyingagent-dev)"
for i in $(seq 1 60); do
if curl -sf http://localhost:8095/health/live >/dev/null; then
echo "DEV /health/live ready (attempt $i)"
break
fi
if [ "$i" -eq 60 ]; then
echo "timeout waiting for DEV /health/live"
exit 1
fi
sleep 5
done
curl -sf http://localhost:8095/health/live
echo
curl -sf http://localhost:8095/health/ready
echo
echo "LAN smoke OK: http://192.168.2.49:8095"

22
.gitignore vendored Normal file
View File

@ -0,0 +1,22 @@
# Environment and secrets
.env
.env.*
!.env.example
# Python
.venv/
**/__pycache__/
*.py[cod]
.pytest_cache/
.mypy_cache/
.ruff_cache/
*.egg-info/
dist/
build/
# OS / editor
.DS_Store
Thumbs.db
*.swp
.idea/
.vscode/*

View File

@ -0,0 +1,671 @@
# Buying Agent v0.1 – Implementierungsspezifikation
Status: implementierungsbereit · Spezifikationsversion: 1.1 · Stand: 2026-10-03
## Änderungen gegenüber Spezifikationsversion 1.0
- Preference-Scoring erhält ein explizites `strategy`-Feld; v0.1 implementiert ausschließlich `numeric_linear`, ohne vorsorgliche Plugin-Infrastruktur.
- Produkt-Eignung und Angebots-Eignung werden getrennt als `product_eligibility` und `offer_eligibility` bewertet; `overall_eligibility` wird daraus abgeleitet.
- Die Preisbewertung wird konzeptionell von der Identität eines Offers entkoppelt: v0.1 nutzt weiterhin nur die Historie des konkreten Offers, spätere Scoring-Versionen dürfen jedoch Variant- oder Market-Level-Referenzserien konsumieren.
- `raw_value` wird bei JSONB als inhaltlich unverändert, nicht als bytegenau identisch beschrieben.
- Ein verbindlicher `Future Extension Contract` dokumentiert, welche späteren Erweiterungen v0.1 nicht verbauen darf, ohne sie bereits zu implementieren.
## 1. Auftrag und Zielbild
Implementiere ein eigenständig lauffähiges Backend für einen generischen Buying-/Deal-Agenten. Das System bewertet, wie gut eine konkrete Produktvariante zu einem Bedarfsprofil passt und wie attraktiv ein bestimmtes Angebot preislich ist. Es liefert nachvollziehbare Entscheidungen, keine automatischen Käufe.
Notebooks sind der erste Anwendungsfall, nicht das Datenmodell. Weitere Kategorien wie GPU-Server, Monitore oder Kameras müssen durch neue Attribute und Profile ohne Änderungen an der Engine oder zusätzliche produktspezifische DB-Spalten möglich sein.
Diese Datei ist die vollständige Arbeitsgrundlage. Ein Zugriff auf den ursprünglichen Chat ist nicht erforderlich. Die dort beschlossene Architektur wird beibehalten. Die hier definierten Formeln, Fehlerfälle, API-Verträge und Zeitregeln konkretisieren bislang offene Punkte als verbindliche v0.1-Entscheidungen. Die Beispielwerte sind synthetische Testdaten und keine Markt- oder Herstellerangaben.
### Walking Skeleton
Der MVP muss diesen vollständigen Ablauf tatsächlich ausführen und dauerhaft speichern:
```text
Profil → Produkt → Variante + Attribute → Angebot → Preisbeobachtungen
→ Constraints → ProductScore → PriceScore → DealScore
→ Klassifikation + Begründungen → gespeicherte Evaluation → API-Antwort
```
Ein manuell erfasster Kandidat genügt für den ersten vertikalen Durchstich. Danach werden Grenzfälle und drei Beispielkandidaten ergänzt. Keine bloßen Interfaces ohne funktionsfähigen End-to-End-Pfad.
### Explizit nicht enthalten
- Keepa, Idealo, Geizhals oder andere externe Datenquellen und Scraper.
- n8n, Scheduler, Benachrichtigungen und Hintergrundjobs.
- LLM-Normalisierung oder LLM-Bewertung.
- Web-UI; generierte OpenAPI-/Swagger-Dokumentation ist erlaubt.
- Auto-Discovery, automatische Produktsuche oder automatische Zuordnung fremder Produkte.
- Checkout, automatische Käufe, Nutzerkonten, Mehrmandantenfähigkeit und öffentliche Bereitstellung.
- Wechselkursumrechnung, marktweite Preisprognosen, ML und Optimierung von Gewichten.
Diese Funktionen dürfen später über Adapter und Services ergänzt werden. Keine vorsorglichen Frameworks, leeren Integrationen oder Infrastruktur dafür bauen.
## 2. Stack und Architekturgrenzen
Verbindlich: Python 3.13, FastAPI, Pydantic 2, SQLAlchemy 2, Alembic, PostgreSQL 17, pytest und Docker Compose. Verwende einen synchronen SQLAlchemy-Zugriff mit psycopg 3; ein asynchroner Stack ist für v0.1 nicht erforderlich. Abhängigkeiten mit kompatiblen Versionen und einem eingecheckten Lockfile reproduzierbar festlegen.
Abhängigkeiten zeigen nach innen:
```text
HTTP/API → Application Services → Domain / Constraint Engine / Scoring
↓
Repositories und Source-Adapter
↓
PostgreSQL
```
- `domain` und `scoring` importieren weder FastAPI noch SQLAlchemy oder konkrete Quellen.
- Die Engine ist eine reine Berechnung über explizite Eingaben. Kein Netzwerkzugriff, kein DB-Zugriff und kein implizites Lesen der Systemzeit.
- Services laden Daten, erzeugen einen konsistenten Snapshot, rufen die Engine auf und speichern das Ergebnis atomar.
- API-Schemas und ORM-Modelle sind getrennt von Domain-Modellen. Kleine Dataclasses oder Pydantic-Modelle sind für die Domain zulässig.
- Regeln liegen als validierte Daten vor; keine Ausführung von Python, SQL, `eval` oder Templates aus Profilen.
- n8n bleibt später ein externer Aufrufer. Die gesamte Buying Engine funktioniert ohne n8n.
## 3. Fachliche Entitäten und Identität
### Product
Produktfamilie bzw. Modell, beispielsweise „Example Mobile 13“. Felder: UUID `id`, `category`, `brand`, `name`, optional `model`, `created_at`. Keine RAM-, SSD- oder Preisfelder. Kategorie ist ein normalisierter String, kein Notebook-spezifisches Enum.
### ProductVariant
Eine eindeutig beschriebene technische Konfiguration eines Products. Felder: UUID `id`, `product_id`, `name`, optionale Hersteller-SKU `manufacturer_sku`, optionale Kennungen `identifiers` als JSON-Objekt, `created_at`.
RAM, Speicher, Farbe, Tastaturlayout und Ausstattung gehören zur Variante. Angebote unterschiedlicher Konfigurationen dürfen nicht zusammengeführt werden. In v0.1 verknüpft der Aufrufer Varianten explizit; keine heuristische Identitätsauflösung. Hersteller-SKUs sind nicht global eindeutig. Identische Namen sind kein Dublettenbeweis.
### Offer
Ein konkretes Händlerangebot für genau eine Variante. Felder: UUID `id`, `variant_id`, `source_id`, `external_id`, `seller`, optionale `url`, `market` (z. B. `DE`), `currency` (dreistelliger Großbuchstaben-Code), `condition` (`new`, `refurbished`, `used`), `created_at`.
Eindeutigkeit: `(source_id, external_id)`. Unterschiedliche Verkäufer, Zustände, Märkte oder Lieferumfänge sind unterschiedliche Angebote. Technisch abweichende Bundles erfordern eine passende Variante. Diese Identitätsfelder bleiben nach Erstellung unverändert. Ein Offer enthält keinen unabhängig veränderbaren „aktuellen Preis“.
### PriceObservation
Append-only-Beobachtung eines Offers zu einem Zeitpunkt. Felder: UUID `id`, `offer_id`, `source_id`, `external_observation_id`, `observed_at`, `ingested_at`, `item_price`, nullable `shipping_price`, `currency`, `availability` (`in_stock`, `out_of_stock`, `unknown`), `raw_payload` als JSON, `confidence`.
- Geld als `Decimal`/PostgreSQL `NUMERIC(12,2)`, nie binäre Floats. API-Geldwerte werden als Dezimalstrings übertragen.
- `item_price > 0`; bekannte Versandkosten `>= 0`; null bedeutet unbekannt, nicht kostenlos.
- Gesamtpreis = Bruttopreis inklusive bekannter obligatorischer Bestandteile + Versand. Nur direkt für jeden Käufer verfügbare Preise erfassen; keine Mitgliederrabatte, Finanzierung oder personalisierte Coupons modellieren.
- Die Währung muss der Offer-Währung entsprechen. `source_id` muss der Offer-Quelle entsprechen.
- Zeitzonen sind obligatorisch; Speicherung und Ausgabe in UTC. Zeitpunkte in der Zukunft relativ zur Ingestion werden abgewiesen.
- Eindeutigkeit `(offer_id, external_observation_id)`: identischer Wiederholungsimport liefert den vorhandenen Datensatz, widersprüchlicher Inhalt HTTP 409. `ingested_at` ist serverseitig und zählt beim Inhaltsvergleich nicht mit.
- Beobachtungen werden nicht überschrieben. Korrekturen werden als neue Beobachtung mit neuer externer ID erfasst; ein administrativer Korrekturworkflow ist nicht Bestandteil von v0.1.
## 4. Generische Attribute und Normalisierung
Attribute werden für v0.1 ausschließlich an Varianten erfasst. Es gibt keine implizite Vererbung von Product-Attributen. Eine spätere Erweiterung muss eine eigene, explizite Vorrangregel definieren.
### Attributdefinition
`attribute_definitions` enthält `key`, `value_type` (`number`, `string`, `boolean`, `string_list`), nullable `canonical_unit`, optionale `minimum`/`maximum` und `description`. Schlüssel sind stabile `snake_case`-Bezeichner. Neue Definitionen lassen sich per API ohne Migration anlegen. Bestehende Definitionen sind in v0.1 unveränderlich; geänderte Semantik erhält einen neuen Schlüssel.
Beispieldefinitionen:
| key | type | canonical_unit | Wertebereich |
|---|---|---|---|
| ram_gb | number | GB | >= 0 |
| storage_gb | number | GB | >= 0 |
| weight_kg | number | kg | > 0, im Eingabevalidator |
| battery_score | number | null | 0–100 |
| display_score | number | null | 0–100 |
| cpu_score | number | null | 0–100 |
| ports | string_list | null | beliebige Strings |
`*_score`-Attribute sind ausdrücklich manuelle Einschätzungen auf einer dokumentierten 0–100-Skala; die Engine erfindet keine Benchmarks oder Messwerte.
### Attributwert
Jede Zeile in `variant_attributes` ist eine unveränderliche Aussage einer Quelle:
```json
{
"attribute": "ram_gb",
"raw_value": "24GB LPDDR5X RAM",
"normalized_value": 24,
"unit": "GB",
"normalization_status": "normalized",
"normalizer_version": "manual-v1",
"confidence": 1.0,
"source_id": "manual",
"source_reference": "demo/notebook-a/ram",
"observed_at": "2026-10-03T10:00:00Z"
}
```
Zusätzlich serverseitig: UUID `id`, `variant_id`, `ingested_at`. `raw_value` ist verpflichtend und bleibt inhaltlich unverändert erhalten; es darf ein JSON-Skalar oder -Objekt sein. Bei Speicherung als JSONB ist keine bytegenaue Erhaltung der ursprünglichen JSON-Serialisierung garantiert. Falls eine spätere Quelle den exakten Response-Body benötigt, muss dieser separat als Rohtext gespeichert werden; das ist nicht Bestandteil von v0.1. `normalized_value` ist ein typisierter JSON-Wert. Bei `normalization_status=unknown` ist er null; ein optionales `normalization_error` erklärt den fehlenden Wert. Eine erfolglose Normalisierung ist kein Zahlenwert 0.
Manuelle Erfassung liefert Raw- und Normalized-Wert explizit. `ManualNormalizer` validiert Typ, Bereich und kanonische Einheit, analysiert aber keinen Freitext. Falsche Typen oder Einheiten werden mit 422 abgewiesen. Keine automatische Umwandlung von `"32"` zu 32 oder `true` zu 1. `confidence` liegt zwischen 0 und 1 und bezeichnet die Zuverlässigkeit der Aussage, nicht den Produktnutzen. Eine absichtlich unbekannte Aussage ist zulässig.
Bei mehreren Aussagen zu demselben Schlüssel verwendet der Snapshot die jüngste nach `(observed_at, ingested_at, id)` absteigend, jeweils nur bis zum Auswertungszeitpunkt bzw. Snapshot-Cutoff. Anschließend wird deren Confidence geprüft. Ein neuer unsicherer oder unbekannter Wert darf nicht stillschweigend durch einen älteren günstigen Wert ersetzt werden. Vorrang verschiedener Quellen ist damit zunächst bewusst einfach; spätere Auswahlstrategien benötigen eine neue Regelversion.
## 5. Product Profile und Versionierung
Ein Profil beschreibt Kategorie, Budget, harte Mindestanforderungen und gewichtete Präferenzen. Es besitzt eine stabile String-ID und eine positive Integer-Version. Jede `(id, version)` ist unveränderlich. Eine Änderung wird als neue Version gespeichert. Evaluationen müssen eine konkrete Version angeben; kein implizites „latest“.
Vollständiges Beispiel für `profiles/mobile-development-notebook.json`:
```json
{
"id": "mobile-development-notebook",
"version": 1,
"name": "Mobiler Entwicklungsrechner",
"category": "notebook",
"currency": "EUR",
"market": "DE",
"allowed_conditions": ["new"],
"budget": {"target": "1500.00", "maximum": "2000.00"},
"scoring_version": "generic-v1",
"constraints": [
{"id": "min_ram", "attribute": "ram_gb", "operator": ">=", "value": 16, "mandatory": true},
{"id": "min_storage", "attribute": "storage_gb", "operator": ">=", "value": 512, "mandatory": true},
{"id": "max_weight", "attribute": "weight_kg", "operator": "<=", "value": 1.6, "mandatory": true}
],
"preferences": [
{"id": "ram", "attribute": "ram_gb", "strategy": "numeric_linear", "direction": "higher", "acceptable": 16, "ideal": 32, "weight": 0.30},
{"id": "weight", "attribute": "weight_kg", "strategy": "numeric_linear", "direction": "lower", "acceptable": 1.6, "ideal": 1.0, "weight": 0.25},
{"id": "battery", "attribute": "battery_score", "strategy": "numeric_linear", "direction": "higher", "acceptable": 60, "ideal": 100, "weight": 0.20},
{"id": "display", "attribute": "display_score", "strategy": "numeric_linear", "direction": "higher", "acceptable": 60, "ideal": 100, "weight": 0.15},
{"id": "cpu", "attribute": "cpu_score", "strategy": "numeric_linear", "direction": "higher", "acceptable": 60, "ideal": 100, "weight": 0.10}
]
}
```
Jede Präferenz besitzt eine explizite `strategy`. v0.1 unterstützt ausschließlich `numeric_linear`. Das Profilformat ist jedoch so zu modellieren, dass spätere Scoring-Versionen zusätzliche klar benannte Strategien wie `boolean_preferred`, `categorical_match`, `set_contains` oder `categorical_rank` einführen können, ohne die Grundstruktur eines Profils zu ändern. Für v0.1 wird dafür weder ein Plugin-System noch dynamische Code-Ausführung gebaut; unbekannte Strategien werden bei der Profilvalidierung abgewiesen.
Validierung: `0 < target < maximum`, mindestens eine Präferenz, positive Gewichte, innerhalb der jeweiligen Liste eindeutige IDs, bekannte Attributschlüssel und eine vorhandene Scoring-Version. Die Summe der Gewichte muss nicht 1 sein, weil die Engine normalisiert. Für `numeric_linear` gilt: `higher` verlangt `ideal > acceptable`, `lower` verlangt `ideal < acceptable`, und das referenzierte Attribut muss numerisch sein. Kategorie, Währung, Markt und erlaubte Zustände sind verpflichtend.
Das Profil ist ein Beispiel aus der Architekturentscheidung, keine abschließend optimierte persönliche Kaufberatung. Die Auswahl eines Notebooks für mobile Entwicklung wird nicht mit Anforderungen an einen lokalen LLM-Server vermischt.
## 6. Constraint Engine und Eignungsstatus
Unterstütze ausschließlich folgende Operatoren; verwerfe ungültige Kombinationen bereits beim Anlegen des Profils:
| Operator | Attributtyp | Vergleichswert / Semantik |
|---|---|---|
| `>=`, `<=`, `>`, `<` | number | Zahl in kanonischer Einheit |
| `==`, `!=` | number, string, boolean | gleicher skalarer Typ, exakter Vergleich |
| `in` | number, string, boolean | nichtleere Liste gleichartiger skalarer Werte |
| `contains` | string_list | einzelner String, exakte Mitgliedschaft |
| `exists` | alle | kein `value`; verwertbarer normalisierter Wert vorhanden |
Keine impliziten Typkonvertierungen, String-Teilstrings oder case-insensitive Vergleiche. Leere String-Listen sind vorhandene Werte. Fehlend, null, nicht normalisiert oder Confidence unter der Regelgrenze ergibt `UNKNOWN`, auch bei `exists` und `!=`.
Jedes Ergebnis enthält Constraint-ID, Attribut, Operator, Sollwert, Istwert, Status `PASS|FAIL|UNKNOWN`, Mandatory-Flag, gewählte Attributzeilen-ID und Reason-Code.
### Product Eligibility
`product_eligibility` beschreibt ausschließlich, ob die konkrete Produktvariante fachlich zum Profil passt. Dazu gehören:
- Kategorie des Produkts/der Variante,
- alle mandatory Produkt-Constraints,
- technische bzw. produktbezogene Daten, die für diese Constraints benötigt werden.
Ableitung:
- mindestens ein bekannter produktbezogener harter `FAIL` → `product_eligibility=INELIGIBLE`;
- sonst mindestens ein mandatory produktbezogener `UNKNOWN` → `product_eligibility=UNKNOWN`;
- sonst → `product_eligibility=ELIGIBLE`.
Optionale Constraints ändern `product_eligibility` nicht. Optionales `FAIL` oder `UNKNOWN` reduziert ausschließlich den ProductScore um die im Scoring-Modell konfigurierte Punktzahl und bleibt in den Ergebnissen sichtbar.
Der ProductScore darf unabhängig von der aktuellen Angebotslage berechnet und als Diagnostik gespeichert werden, sofern das Profil und die vorhandenen Präferenzdaten dies zulassen. Ein technisch sehr gutes Produkt verliert seinen ProductScore nicht nur deshalb, weil das betrachtete Offer aktuell zu teuer oder nicht lieferbar ist.
### Offer Eligibility
`offer_eligibility` beschreibt ausschließlich die Eignung des konkret ausgewerteten Offers. Systemseitige Offer-Gates sind:
- Markt entspricht dem Profil,
- Zustand ist in `allowed_conditions`,
- Währung entspricht der Profilwährung,
- aktuelle Preisbeobachtung ist vorhanden und frisch,
- Preis-Confidence erfüllt die Mindestschwelle,
- obligatorische Versandkosten sind bekannt,
- Gesamtpreis <= Budgetmaximum,
- aktuelle Verfügbarkeit ist `in_stock`.
Ein bekannter Verstoß ergibt `offer_eligibility=INELIGIBLE`. Fehlende/zu alte Preisbeobachtung, geringe Preis-Confidence, unbekannte Versandkosten oder unbekannte Verfügbarkeit ergeben `offer_eligibility=UNKNOWN`. `out_of_stock` ergibt `INELIGIBLE`. Ein Preis oberhalb des Budgets ist ein Angebotsproblem und macht nicht das Produkt selbst ungeeignet.
### Overall Eligibility
`overall_eligibility` wird ausschließlich aus Product und Offer Eligibility abgeleitet:
- wenn mindestens einer der beiden Status `INELIGIBLE` ist → `overall_eligibility=INELIGIBLE`;
- sonst wenn mindestens einer `UNKNOWN` ist → `overall_eligibility=UNKNOWN`;
- sonst → `overall_eligibility=ELIGIBLE`.
Alle Gates werden ausgewertet, damit die Antwort sämtliche Gründe enthält. Ein bekannter harter Fehler hat innerhalb der jeweiligen Ebene Vorrang vor unbekannten Daten. Das API-Feld `eligible` ist eine Convenience-Abbildung von `overall_eligibility`: `ELIGIBLE` → true, `INELIGIBLE` → false, `UNKNOWN` → null.
## 7. Scoring-Modell `generic-v1`
Alle Scores liegen zwischen 0 und 100. Rechne intern mit Decimal, runde nur endgültige Scores und angezeigte Komponenten auf zwei Dezimalstellen, `ROUND_HALF_UP`. Klassifikation anhand des ungerundeten DealScore. Der Snapshot enthält auch ungerundete Dezimalstrings für eine eindeutige Grenzfallprüfung.
Die unveränderliche Modelldefinition enthält mindestens:
```json
{
"version": "generic-v1",
"algorithm_id": "linear-budget-history-v1",
"supported_preference_strategies": ["numeric_linear"],
"minimum_confidence": 0.8,
"maximum_price_age_hours": 48,
"optional_constraint_penalty_points": 5,
"missing_preference_score": 0,
"product_weight": 0.6,
"price_weight": 0.4,
"budget_weight": 0.4,
"history_weight": 0.6,
"history_window_days": 90,
"minimum_history_days": 3,
"history_discount_for_max_score": 0.2,
"classification_upper_bounds": [
{"upper": 60, "inclusive": false, "label": "IGNORE"},
{"upper": 75, "inclusive": false, "label": "WATCH"},
{"upper": 85, "inclusive": false, "label": "GOOD"},
{"upper": 92, "inclusive": true, "label": "VERY_GOOD"},
{"upper": 100, "inclusive": true, "label": "EXCEPTIONAL"}
]
}
```
`clamp(x)=min(100,max(0,x))`. Modellvalidierung prüft positive Zeitfenster, gültige Confidence, nichtnegative Gewichte mit jeweiliger Summe 1, aufsteigende lückenlose Klassifikationsgrenzen, unterstützte Algorithmus-ID und eine nichtleere Liste unterstützter Preference-Strategien. Die Definition wird als kanonisches JSON inklusive SHA-256 gespeichert. Eine Version darf niemals mit geändertem Inhalt wiederverwendet werden. Neue Algorithmen oder Preference-Strategien werden als Code ergänzt und unter einer neuen Scoring-Version bzw. Algorithmus-ID registriert; JSON darf keinen ausführbaren Code enthalten.
### ProductScore
v0.1 implementiert ausschließlich die Preference-Strategie `numeric_linear`. Für eine solche numerische Präferenz mit Istwert `x`, akzeptablem Wert `a` und Idealwert `i`:
```text
higher: component = clamp(50 + 50 × (x − a) / (i − a))
lower: component = clamp(50 + 50 × (a − x) / (a − i))
base_product_score = Σ(weight × component) / Σ(weight)
product_score = clamp(base_product_score − 5 × Anzahl optionaler FAIL/UNKNOWN)
```
Damit erhält ein akzeptabler Wert 50, ein idealer oder besserer Wert maximal 100. Ein fehlender oder unsicherer Präferenzwert erhält 0; sein Gewicht bleibt im Nenner. Fehlende Daten dürfen den Score nicht verbessern. `product_coverage` ist die Summe der Gewichte verwertbarer Präferenzen geteilt durch die Summe aller Gewichte. Confidence wird nicht nochmals als Score-Multiplikator verwendet.
Der ProductScore beschreibt Produkt-Fit und wird unabhängig von `offer_eligibility` berechnet. Auch bei `product_eligibility=INELIGIBLE` oder `UNKNOWN` darf er diagnostisch ausgegeben werden; Mandatory Gates bleiben jedoch autoritativ und können niemals durch einen hohen ProductScore überstimmt werden.
Die Strategy-Auswahl erfolgt explizit über das Preference-Feld `strategy`; es gibt keine dynamische Plugin-Suche. Unbekannte Strategien sind für `generic-v1` ein Validierungsfehler.
### Auswahl des aktuellen Preises und der Referenzhistorie
Der Evaluation-Request bezeichnet genau ein Offer. Kein implizites billigstes Angebot und kein Vermischen mehrerer Händler oder Varianten.
v0.1 verwendet als `reference_history` ausschließlich Preisbeobachtungen dieses konkreten Offers. Diese Bindung ist eine Entscheidung der Scoring-Version, nicht des generischen Preis-Scoring-Vertrags. Die reine Preis-Scoring-Funktion ist daher konzeptionell als Berechnung über `current_price`, `reference_history` und `budget` zu implementieren; sie darf fachlich nicht verlangen, dass die Referenzserie dauerhaft ein Offer-Objekt ist. Spätere Scoring-Versionen dürfen Variant-Level- oder Market-Level-Referenzserien bereitstellen, ohne Product, ProductVariant, Offer oder PriceObservation neu zu modellieren.
Für v0.1 gilt für die offer-bezogene Referenzserie:
1. Betrachte ausschließlich Beobachtungen dieses Offers mit `observed_at <= as_of` und `ingested_at <= snapshot_cutoff`.
2. Aktuell ist die jüngste Zeile nach `(observed_at, ingested_at, id)` absteigend. Auch eine nicht verfügbare oder unsichere jüngste Zeile darf nicht durch eine ältere günstigere ersetzt werden.
3. Frische: `as_of − observed_at <= 48 Stunden`, einschließlich Grenze.
4. Historie endet strikt vor dem UTC-Kalendertag der aktuellen Beobachtung. Dadurch wird der aktuelle Angebotspreis nicht selbst zur historischen Referenz.
5. Für jedes Fenster N ∈ {30,90,365} gilt `[current_day − N Tage, current_day)`. Wähle zunächst pro UTC-Tag die jüngste Zeile, danach filtere Tage mit `in_stock`, bekannter Gesamtsumme und Confidence >= 0.8. So verdrängt eine spätere unbekannte Beobachtung eine frühere bekannte desselben Tages.
6. Berechne `median_30d`, `median_90d`, `lowest_365d` aus diesen Tageswerten. Bei gerader Anzahl ist der Median das arithmetische Mittel der beiden mittleren Werte. Gib die Zahl der vorhandenen Tage pro Fenster mit aus; keine Interpolation fehlender Tage.
Historienaggregate werden berechnet und im Evaluation-Snapshot gespeichert, nicht als vom Client beliebig vorgegebene Wahrheit übernommen. Die v0.1-Historie ist angebotsbezogen und keine Aussage über den Gesamtmarkt. `lowest_365d` darf als Minimum weniger beobachteter Tage angezeigt werden, muss dann ausdrücklich als Minimum der vorhandenen Daten bezeichnet sein. Der Snapshot speichert zusätzlich `price_reference_scope="offer"` und die IDs der verwendeten Beobachtungen, damit spätere Scoring-Versionen andere Scopes eindeutig unterscheiden können.
### PriceScore
Seien `p` aktueller Gesamtpreis, `t` Budgetziel, `m` Budgetmaximum, `r` 90-Tage-Median der von der Scoring-Version bereitgestellten Referenzhistorie:
```text
budget_score = 100 falls p <= t
budget_score = clamp(100 × (m − p) / (m − t)) sonst
discount = (r − p) / r
history_score = clamp(50 + 50 × discount / 0.20)
price_score = 0.40 × budget_score + 0.60 × history_score
```
Historien-Score: beim Median 50, bei 20 % günstiger 100, bei 20 % teurer 0. Nur bei mindestens drei verfügbaren Tagen im 90-Tage-Fenster ist `history_score` vorhanden. Andernfalls gilt `price_score=budget_score`, `price_basis=BUDGET_ONLY`, `history_score=null` und ein expliziter Hinweis `INSUFFICIENT_HISTORY`. Bei ausreichender Historie `price_basis=BUDGET_AND_HISTORY`. Kein Bonus für ein vermeintliches Jahrestief; 30-Tage-Median und Minimum dienen zunächst nur der Erklärung.
Ohne frischen, hinreichend sicheren und vollständig bekannten Gesamtpreis in Profilwährung ist der PriceScore null. Ein bekannter Preis oberhalb des Budgets kann diagnostisch bewertet werden, während `offer_eligibility` wegen des Budget-Gates `INELIGIBLE` bleibt. Bei falscher Währung wird ohne Wechselkursumrechnung kein PriceScore berechnet. Ausverkaufte Angebote dürfen ihren bekannten Preis diagnostisch behalten; `offer_eligibility` bleibt dennoch `INELIGIBLE`.
### DealScore und Klassifikation
Nur bei `overall_eligibility=ELIGIBLE` und vorhandenem PriceScore:
```text
deal_score = 0.60 × product_score + 0.40 × price_score
```
| Bereich des ungerundeten DealScore | Klassifikation |
|---|---|
| < 60 | IGNORE |
| >= 60 und < 75 | WATCH |
| >= 75 und < 85 | GOOD |
| >= 85 und <= 92 | VERY_GOOD |
| > 92 | EXCEPTIONAL |
Bei `overall_eligibility=INELIGIBLE`: DealScore null, Klassifikation `INELIGIBLE`. Bei `overall_eligibility=UNKNOWN`: DealScore null, Klassifikation `INSUFFICIENT_DATA`. ProductScore und vorhandener PriceScore dürfen als Diagnostik ausgegeben werden, ersetzen aber niemals die Gate-Ergebnisse. Die getrennten Product-/Offer-Status müssen in Snapshot und API sichtbar bleiben, damit ein gutes Produkt mit momentan schlechtem Angebot weiterhin beobachtbar bleibt.
### Nachvollziehbarkeit und Wiederholbarkeit
Jede Evaluation speichert Profil und Version, Modellversion und Hash, Engine-Build-ID, `as_of`, `snapshot_cutoff`, `product_eligibility`, `offer_eligibility`, `overall_eligibility`, vollständige tatsächlich verwendete Eingaben und IDs sowie alle Komponenten, Gates, Aggregate, Coverage, Price-Reference-Scope, Reason-Codes und verständliche Texte. Persistierte Evaluationen sind unveränderlich. Ein erneuter Request erzeugt eine neue Evaluation; ein GET berechnet nichts neu.
Historische Abfragen mit `as_of` rekonstruieren nur den Datenstand zum aktuellen Snapshot-Cutoff, einschließlich später eingegangener Rückdatierungen. Sie behaupten nicht, den damaligen Wissensstand wiederherzustellen. Exakte Wiederholung einer alten Bewertung erfolgt intern aus deren gespeichertem Snapshot und zugehöriger Algorithmusversion. Alte Algorithmen bleiben verfügbar; kein Replay-HTTP-Endpunkt für v0.1 erforderlich.
## 8. Source-Adapter
Implementiere einen kleinen `Protocol` und `ManualSource`. Quell-DTOs enthalten Rohdaten, Quellenkennung, externe IDs, Zeitpunkte und Confidence; sie enthalten keine ORM-Objekte und keine Scoring-Logik.
```python
class ProductSource(Protocol):
source_key: str
def get_product(self, external_id: str) -> SourceProduct: ...
def get_offers(self, external_product_id: str) -> list[SourceOffer]: ...
def get_price_history(self, external_offer_id: str) -> list[SourcePriceObservation]: ...
```
`ManualSource` liest einen validierten lokalen Demo-Datensatz, keine externe Website. Ein Import-Service nimmt den Adapter entgegen und legt über dieselben Application Services wie die API Produkte, Varianten, Attribute, Angebote und Beobachtungen an. Explizite Variantenzuordnung aus der Fixture; keine Matching-Engine. Wiederholter Demo-Import erzeugt keine Duplikate: stabile UUIDs für Produkte/Varianten/Attribute, stabile externe Offer-/Beobachtungs-IDs und identischer Inhalt; Widersprüche melden statt überschreiben.
Adapter normalisieren keine Preise oder technischen Eigenschaften im Scoring-Modul. Der Pfad lautet Quelle → validierte DTOs → Normalisierung → Persistenz → Evaluation. Für spätere Quellen genügen zunächst Adapter und Import-Mapping. Pagination, Rate Limits und Retry-Strategien erst zusammen mit der ersten realen Quelle einführen.
## 9. HTTP-API
Alle fachlichen Endpunkte unter `/api/v1`. UUIDs für Entitäten, String-IDs für Profile, Attribute und Scoring-Modelle. Pydantic-Schemas mit `extra=forbid`; Pflichtfelder und Enums im OpenAPI-Schema dokumentieren. JSON-Ausgabe nutzt `snake_case`.
| Methode / Pfad | Vertrag |
|---|---|
| GET `/health/live` | Prozess lebt, kein DB-Zugriff |
| GET `/health/ready` | DB erreichbar und Migration auf Head; sonst 503 |
| POST `/api/v1/attribute-definitions` | unveränderliche Definition anlegen |
| GET `/api/v1/attribute-definitions` | Definitionen listen |
| GET `/api/v1/sources` | registrierte Quellen, einschließlich `manual` |
| GET `/api/v1/scoring-models/{version}` | vollständige Regeldefinition + Hash |
| POST `/api/v1/profiles` | vollständiges Profil inklusive Version anlegen |
| GET `/api/v1/profiles` | konkrete Profilversionen listen |
| GET `/api/v1/profiles/{id}?version=1` | konkrete Version; Query verpflichtend |
| POST `/api/v1/products` | Product anlegen |
| GET `/api/v1/products/{id}` | Product lesen |
| POST `/api/v1/products/{id}/variants` | Variante anlegen |
| GET `/api/v1/variants/{id}` | Variante mit Attributaussagen lesen |
| POST `/api/v1/variants/{id}/attributes` | einzelne unveränderliche Attributaussage hinzufügen |
| POST `/api/v1/variants/{id}/offers` | Offer anlegen |
| GET `/api/v1/offers/{id}` | Offer lesen |
| POST `/api/v1/offers/{id}/prices` | einzelne Beobachtung hinzufügen bzw. identisch wiederholen |
| GET `/api/v1/offers/{id}/prices` | gespeicherte Beobachtungen listen |
| POST `/api/v1/evaluate/{variant_id}` | auswerten und Ergebnis atomar speichern |
| GET `/api/v1/evaluations/{id}` | gespeichertes Ergebnis inklusive Snapshot lesen |
Listen verwenden `limit` (Default 50, maximal 200) und `offset` (>=0); Antwort `{items, limit, offset, total}`. Stabile Sortierung nach Primärschlüssel, bei Preisen `(observed_at, ingested_at, id)` aufsteigend. Keine Update-/Delete-Endpunkte in v0.1.
Evaluation-Request:
```json
{
"profile_id": "mobile-development-notebook",
"profile_version": 1,
"offer_id": "33333333-3333-4333-8333-333333333333",
"as_of": "2026-10-03T12:00:00Z"
}
```
`as_of` ist optional; der Service setzt es einmal auf Serverzeit UTC. Zukunftswerte sind ungültig. Offer muss zur angegebenen Variante gehören (422). Das Profil legt die Scoring-Version fest; kein stiller Override. Fachlich unpassende Kategorie wird im Product-Gate gespeichert; Markt, Zustand, Währung, Preis und Verfügbarkeit werden im Offer-Gate gespeichert. Keiner dieser fachlichen Fälle ist ein HTTP-Fehler.
Evaluation-Response enthält mindestens:
```json
{
"id": "44444444-4444-4444-8444-444444444444",
"profile_id": "mobile-development-notebook",
"profile_version": 1,
"scoring_version": "generic-v1",
"product_eligibility": "ELIGIBLE",
"offer_eligibility": "ELIGIBLE",
"overall_eligibility": "ELIGIBLE",
"eligible": true,
"product_score": 90.21,
"price_score": 98.24,
"deal_score": 93.42,
"classification": "EXCEPTIONAL",
"price_basis": "BUDGET_AND_HISTORY",
"product_coverage": 1.0,
"failed_constraints": [],
"unknown_constraints": [],
"reasons": [
{"code": "PRICE_BELOW_MEDIAN", "message": "Gesamtpreis 18,83 % unter dem Median der beobachteten 90-Tage-Historie."}
]
}
```
Zusätzlich verpflichtend: `variant_id`, `offer_id`, `as_of`, `snapshot_cutoff`, `scoring_hash`, `engine_build`, `product_eligibility`, `offer_eligibility`, `overall_eligibility`, `components`, sämtliche `constraint_results` und Offer-Gates, `price_statistics`, `price_reference_scope`, `input_snapshot` und `warnings`. Die Kurzantwort oben illustriert nur die Kernfelder. `failed_constraints` enthält alle `FAIL`-Ergebnisse; `unknown_constraints` alle `UNKNOWN`-Ergebnisse. Reason-Codes sind stabil, Texte dienen Menschen und dürfen nicht als Programmierschnittstelle ausgewertet werden.
POST liefert 201, identisch wiederholte Preisbeobachtung 200; GET liefert 200. Fehlende IDs: 404. Verletzte Eindeutigkeit/abweichender Wiederholungsinhalt: 409. Ungültige Payload/Referenzkombination: 422. Einheitliches Fehlerformat `{error: {code, message, details}}`, auch für Request-Validierung. Keine SQL-Details an Clients. Eine fachlich negative Evaluation ist eine erfolgreiche 201-Antwort.
## 10. Datenbank und Transaktionen
Alle Zeitstempel `TIMESTAMPTZ`; JSON-Daten `JSONB`; UUID-Primärschlüssel für Entitäten; Decimal-Geldfelder. `created_at`/`ingested_at` werden serverseitig gesetzt. Fremdschlüssel sind obligatorisch. Kein Cascade-Delete historischer Bewertungen oder Quelldaten.
| Tabelle | Wesentliche Spalten und Constraints |
|---|---|
| `attribute_definitions` | `key` PK, Typ, Einheit, Bereich, Beschreibung |
| `sources` | `id` String-PK, `name`, `adapter_type`; Seed `manual` |
| `scoring_models` | `version` PK, `algorithm_id`, `definition` JSONB, `definition_hash`, `created_at` |
| `product_profiles` | zusammengesetzter PK `(id, version)`, `definition` JSONB, `category`, FK `scoring_version`, `definition_hash`, `created_at` |
| `products` | `id`, Kategorie, Marke, Name, Modell, `created_at` |
| `product_variants` | `id`, FK Product, Name, Hersteller-SKU, Kennungen JSONB, `created_at` |
| `variant_attributes` | `id`, FK Variante/Definition/Quelle, Raw/Normalized JSONB, Einheit, Status, Normalizer-Version, Confidence, Source-Referenz, Beobachtungs-/Ingestion-Zeit, Fehler |
| `offers` | `id`, FK Variante/Quelle, externe ID, Händler, URL, Markt, Währung, Zustand; UNIQUE Quelle/externe ID |
| `price_observations` | `id`, FK Offer/Quelle, externe Beobachtungs-ID, Preise, Währung, Verfügbarkeit, Confidence, Raw JSONB, beide Zeiten; UNIQUE Offer/externe ID |
| `evaluations` | `id`, FK Variante/Offer/Profilversion/Scoring-Version, Zeitpunkte, Hash, Build-ID, `product_eligibility`, `offer_eligibility`, `overall_eligibility`, drei nullable Scores, Klassifikation, `price_reference_scope`, vollständige Eingabe-/Ergebnis-Snapshots JSONB |
| `evaluation_components` | `id`, FK Evaluation, `kind`, `component_key`, nullable Rohscore/Gewicht/Beitrag, `details` JSONB; UNIQUE Evaluation/kind/key |
`evaluation_components` enthält Präferenzen inklusive Strategy, optionale Abzüge, Budget-/History-Komponenten, Product-Gates und Offer-Gates. JSON-Snapshots sind die vollständige Replay-Grundlage; relationale Komponenten dienen Abfragen und dürfen bei Erstellung nicht davon abweichen.
DB-Checks mindestens für nichtnegative Preise, positiven Artikelpreis, Confidence 0–1, Scores 0–100, Profilversion >0 und gültige feste Enums. Typ-/Einheiten-/Profilvalidierung bleibt zusätzlich in der Application-Schicht. Redundante Indexspalten eines Profils müssen aus dessen validierter Definition erzeugt werden.
Indizes: Attribute `(variant_id, attribute_key, observed_at DESC, ingested_at DESC, id DESC)`, Preise `(offer_id, observed_at DESC, ingested_at DESC, id DESC)`, Offers `variant_id`, Evaluationen `(profile_id, profile_version, created_at DESC)` und `variant_id`.
Eine Evaluation liest und schreibt innerhalb einer PostgreSQL-Transaktion mit konsistentem Snapshot (`REPEATABLE READ`). Zu Beginn `snapshot_cutoff` festlegen; komplette Inputs, Ergebnis und Komponenten atomar committen. Bei Fehlern kein Teilergebnis. Insert-Konflikte über UNIQUE-Constraints behandeln, nicht allein per vorherigem SELECT.
Alembic ist die einzige produktive Schemaanlage; kein `metadata.create_all()` beim API-Start. Initialmigration enthält Tabellen, Constraints und Indizes. Bootstrap von Quellen und Regeldefinitionen separat, idempotent und inhaltlich geprüft. Ein Konflikt gleicher Version mit anderem Hash stoppt den Bootstrap sichtbar.
## 11. Repository und Lieferumfang
```text
buying-agent/
├── README.md
├── Dockerfile
├── docker-compose.yml
├── .dockerignore
├── .env.example
├── .gitignore
├── pyproject.toml
├── uv.lock
├── alembic.ini
├── migrations/
│ ├── env.py
│ └── versions/
├── app/
│ ├── main.py
│ ├── settings.py
│ ├── api/
│ │ ├── schemas.py
│ │ ├── profiles.py
│ │ ├── products.py
│ │ ├── offers.py
│ │ ├── evaluations.py
│ │ └── health.py
│ ├── domain/
│ │ ├── models.py
│ │ ├── attributes.py
│ │ ├── constraints.py
│ │ └── enums.py
│ ├── scoring/
│ │ ├── product.py
│ │ ├── price.py
│ │ ├── deal.py
│ │ └── engine.py
│ ├── normalization/
│ │ ├── base.py
│ │ └── manual.py
│ ├── sources/
│ │ ├── base.py
│ │ └── manual.py
│ ├── persistence/
│ │ ├── db.py
│ │ ├── models.py
│ │ └── repositories.py
│ └── services/
│ ├── ingestion_service.py
│ └── evaluation_service.py
├── rules/generic-v1.json
├── profiles/mobile-development-notebook.json
├── fixtures/manual-demo.json
├── scripts/
│ ├── bootstrap.py
│ └── demo.py
└── tests/
├── unit/
├── integration/
└── e2e/
```
`__init__.py` nach Bedarf ergänzen. Kleine Dateien dürfen sinnvoll zusammengelegt werden; Architekturgrenzen sind verbindlich. Kein generisches Repository-Framework und kein Plugin-Discovery-System erforderlich. Das Lockfile-Beispiel verwendet uv; uv als Build-/Entwicklungswerkzeug mit dokumentierter Version verwenden.
README enthält Setup, Architektur, Datenmodell, Formeln, Fehlersemantik, alle Start-/Testkommandos, vollständige Beispielrequests sowie Grenzen der synthetischen Preisbewertung. Diese Spezifikation ebenfalls im Repository unter `docs/buying-agent-v0.1-spec-v1.1.md` ablegen.
## 12. Docker und lokale Ausführung
Compose enthält nur zwei dauerhaft laufende Services: `api` und `postgres`. PostgreSQL 17 mit benanntem Datenvolume und Healthcheck. API basiert auf Python 3.13, läuft als Nicht-Root und bindet den veröffentlichten Port an `127.0.0.1:8000`. Die Datenbank braucht keinen veröffentlichten Host-Port. Secrets und lokale DB-Zugänge über `.env`, nur Platzhalter in `.env.example`.
Für das lokale Einzelinstanz-Skeleton darf das API-Entrypoint nach erfolgreichem DB-Healthcheck sequenziell `alembic upgrade head`, Bootstrap und Uvicorn ausführen. Fehlgeschlagene Migrationen verhindern den API-Start. Diese Startmigration ist keine Strategie für spätere Mehrinstanz-Deployments.
Verbindlicher Startpfad:
```sh
cp .env.example .env
docker compose up --build -d
docker compose exec api python -m scripts.demo
```
Für PowerShell zusätzlich `Copy-Item .env.example .env` dokumentieren. `scripts.demo` legt Definitionen, Profil und synthetische Kandidaten über die API an, wiederholt sich ohne Duplikate und ruft Evaluationen mit explizitem `as_of` auf. Die Demo umfasst mindestens die Fälle A, B, C und D aus Abschnitt 13. Statische Fixtures nutzen den unten definierten historischen Zeitpunkt, damit sie nicht mit der Kalenderzeit altern. Der Adapterpfad wird separat im Integrationstest geprüft; die Demo muss den HTTP-Pfad abdecken.
Testabhängigkeiten in einer Docker-Build-Stage `test` bereitstellen. README dokumentiert einen ausführbaren Compose-Test-Override oder gleichwertigen Befehl mit dieser Stage und separater Testdatenbank. Tests dürfen niemals das normale Datenvolume leeren. Kein dauerhafter dritter Service nötig.
## 13. Deterministische Beispiele und Tests
### Referenzfall A
`as_of=2026-10-03T12:00:00Z`, aktuelle Beobachtung um 10:00 UTC; alle technischen Attribute vorher beobachtet. Kategorie Notebook, Markt DE, Zustand neu, EUR, lieferbar, Confidence jeweils 1.0, Versand 0.00.
| Attribut | Wert | Präferenzscore |
|---|---:|---:|
| ram_gb | 32 | 100 |
| storage_gb | 1024 | nur Mindestconstraint |
| weight_kg | 1.2 | 83.333333… |
| battery_score | 90 | 87.5 |
| display_score | 90 | 87.5 |
| cpu_score | 90 | 87.5 |
Gesamtpreise der Historie: 1599.00 am 2026-09-01, 1599.00 am 2026-09-15, 1599.00 am 2026-10-01; jeweils 10:00 UTC. Aktuell 1298.00. Alle Zeilen betreffen dasselbe Offer und sind lieferbar. Fixtures werden vor Evaluation vollständig importiert.
Erwartungen:
```text
product_eligibility = ELIGIBLE
offer_eligibility = ELIGIBLE
overall_eligibility = ELIGIBLE; eligible = true
product_score = 90.21
median_30d = 1599.00 (2 Tage)
median_90d = 1599.00 (3 Tage)
lowest_365d = 1599.00 (3 Tage)
budget_score = 100
history_score = 97.06
price_score = 98.24
deal_score = 93.42
classification = EXCEPTIONAL
product_coverage = 1.0
```
Die Engine rechnet mit den ungerundeten Zwischenwerten. Die Response aus Abschnitt 9 bezieht sich auf diesen Fall. Das beobachtete Minimum von 1599.00 ist ausdrücklich kein belegtes Jahrestief des Marktes.
### Weitere Demokandidaten
- B: identisch zu A, aber 8 GB RAM → `product_eligibility=INELIGIBLE`, `offer_eligibility=ELIGIBLE`, `overall_eligibility=INELIGIBLE`, Mandatory RAM `FAIL`, ProductScore weiterhin diagnostisch vorhanden, kein DealScore.
- C: identisch zu A, aber keine verwertbare Gewichtsangabe → `product_eligibility=UNKNOWN`, `offer_eligibility=ELIGIBLE`, `overall_eligibility=UNKNOWN`, Mandatory Gewicht `UNKNOWN`, ProductScore diagnostisch vorhanden, Klassifikation `INSUFFICIENT_DATA`, kein DealScore.
- D: technisch identisch zu A, aber aktueller Gesamtpreis 2049.00 bei Budgetmaximum 2000.00 → `product_eligibility=ELIGIBLE`, `offer_eligibility=INELIGIBLE`, `overall_eligibility=INELIGIBLE`; ProductScore und diagnostischer PriceScore bleiben vorhanden, DealScore null. Dieser Fall beweist die Trennung von Produkt-Fit und Angebots-Fit.
### Unit-Tests
- Jeden Constraint-Operator mit PASS, FAIL, UNKNOWN; Gleichheitsgrenzen, strikte Typen, leere Listen und Confidence exakt 0.8 bzw. knapp darunter.
- Preference-Strategie `numeric_linear`: akzeptabel 50, ideal 100, Clamping, lower/higher, nicht auf 1 summierte Gewichte, fehlende Werte und optionale Abzüge; unbekannte `strategy` wird für `generic-v1` abgewiesen.
- Budget bei Ziel/Maximum/darüber; History-Score bei −20 %, 0 % und +20 % Rabatt; BUDGET_ONLY bei 0–2 Historientagen.
- Tagesauswahl vor Qualitätsfilter, Fenstergrenzen, Ausschluss des aktuellen Tages, Median gerader/ungerader Anzahl, identische Zeitstempel und stabile Tie-Breaks.
- Keine Vermischung von Offers, Varianten, Währungen oder Zuständen; aktuelle Zeile unbekannt/ausverkauft ohne Rückfall auf ältere Zeilen; ProductScore bleibt von Offer-Gates unabhängig.
- Frische exakt 48 Stunden/knapp darüber; unbekannter Versand; fehlender Preis; Kombination Mandatory FAIL und UNKNOWN.
- Klassifikationsgrenzen 60, 75, 85, 92 und unmittelbar darüber; Rundung darf keine Klasse verschieben.
- Referenzfall A einschließlich exakter gerundeter Resultate; andere Kategorie mit eigenen Attributen ohne Engine-Änderung; Fall D zur Trennung von Product/Offer Eligibility.
- Preis-Scoring-Funktion wird mit explizitem `current_price`, `reference_history` und `budget` getestet; Domain-Test darf dafür kein Offer-ORM-Objekt benötigen. `price_reference_scope=offer` ist für v0.1 verpflichtend.
- Replay aus gespeichertem Input trotz später hinzugefügter Attribute/Preise liefert identische fachliche Ergebnisse; generierte IDs und Speicherzeit sind kein Bestandteil des Vergleichs.
### Integration und End-to-End
- Gegen echtes PostgreSQL 17, nicht SQLite als Ersatz für JSONB, Numeric, Constraints oder Isolation.
- Migration auf leerer Test-DB; erneutes Upgrade ohne Änderung; initialer Downgrade/Upgrade nur in wegwerfbarer Test-DB.
- Bootstrap identisch wiederholbar, veränderte Regel mit gleicher Version wird abgewiesen.
- CRUD-Anlage-/Lesepfade, validierte Profilreferenzen, 404/409/422, Pagination, exakte Decimal- und UTC-Roundtrips.
- Doppelte Preisimporte auch bei konkurrierenden Requests: eine Beobachtung, kein stiller Content-Overwrite.
- Gescheiterte Evaluation-Persistenz hinterlässt weder Evaluation noch Komponenten.
- HTTP-End-to-End: Profil → Produkt → Variante → Attribute → Offer → Historie → Evaluation → GET; danach API-Neustart und erneutes GET desselben unveränderten Ergebnisses.
- ManualSource-Import nutzt dieselben Services und erzeugt bei Wiederholung keine Duplikate.
- Einfügen neuer Preisbeobachtungen ändert alte Evaluationen nicht; neue Evaluation verwendet neue Inputs.
## 14. Implementierungsreihenfolge
1. Projekt, Settings, Docker, PostgreSQL und initiale Migration aufsetzen.
2. Domain-Modelle, Definitions-/Profilvalidierung einschließlich `strategy`, getrennte Product-/Offer-Gates und reine Constraint-/Scoring-Funktionen samt Referenztests implementieren.
3. Preis-Scoring als reine Berechnung über `current_price`, `reference_history` und `budget` implementieren; v0.1-Assembler liefert dafür ausschließlich offer-bezogene Historie und markiert `price_reference_scope=offer`.
4. Repositories und transaktionalen Evaluation-Service integrieren.
5. API, ManualSource und Bootstrap anschließen; ersten vollständigen Durchstich ausführen.
6. Demo, Integrationstests, Fehlerfälle und nachvollziehbare Responses vervollständigen.
7. README und sauberen Start auf leerem Volume prüfen; tatsächliche Testergebnisse dokumentieren.
Routineentscheidungen innerhalb dieser Spezifikation selbst treffen. Falls eine Vorgabe technisch widersprüchlich ist, den konkreten Konflikt dokumentieren und minimal auflösen; keine stillen fachlichen Formeländerungen oder Scope-Erweiterungen. Insbesondere keine Infrastruktur für spätere Preference-Strategien, Variant-/Market-Level-Preisreihen, n8n, LLMs oder Discovery vorwegnehmen.
## 15. Definition of Done
- [ ] Ein frischer Checkout startet mit den dokumentierten Docker-Kommandos; Migration und Readiness funktionieren.
- [ ] PostgreSQL persistiert Daten über API-/Container-Neustarts hinweg.
- [ ] Alle vier Entitäten Product, ProductVariant, Offer, PriceObservation sind getrennt und korrekt verknüpft.
- [ ] Attribute sind generisch und bewahren Raw inhaltlich unverändert, Normalized, Quelle, Confidence und Normalizer-Version.
- [ ] Beispielprofil und `generic-v1` liegen versioniert als Dateien vor und werden validiert gespeichert; jede Preference besitzt `strategy`, v0.1 akzeptiert ausschließlich `numeric_linear`.
- [ ] Product-, Offer- und Overall-Eligibility werden getrennt berechnet, persistiert und über die API ausgegeben.
- [ ] Ein technisch passendes Produkt behält ProductScore/Product Eligibility unabhängig davon, ob das konkrete Offer aktuell zu teuer, ausverkauft oder anderweitig ungeeignet ist.
- [ ] Das Preis-Scoring ist fachlich über `current_price`, `reference_history` und `budget` entkoppelt; v0.1 verwendet ausschließlich `price_reference_scope=offer` und implementiert keine Variant-/Market-Level-Historie.
- [ ] Demo liefert für A, B, C und D die festgelegten Ergebnisse über die API.
- [ ] Scores, harte Gates, fehlende Daten, Historienbasis, Reference Scope und Begründungen sind sichtbar und reproduzierbar.
- [ ] Evaluationen enthalten vollständige unveränderliche Snapshots; spätere Daten und Regelversionen verändern sie nicht.
- [ ] Unit-, PostgreSQL-Integrations- und HTTP-End-to-End-Tests sind grün; Kommandos und Resultate sind dokumentiert.
- [ ] Neue Kategorie und neue Attribute funktionieren ohne Schema- oder Engine-Änderung, solange sie von den in der gewählten Scoring-Version unterstützten Strategies/Operatoren abgedeckt werden.
- [ ] Keine reale externe Quelle, kein n8n, keine LLM-Normalisierung, keine Web-UI, keine Auto-Discovery, keine Variant-/Market-Level-Preisaggregation und keine vorsorgliche Plugin-Infrastruktur wurden eingebaut.
- [ ] README ermöglicht einem anderen Entwickler Setup, Demo, Tests und Verständnis der Grenzen ohne Chat-Kontext.
Abschlussbericht des Coding-Agenten: implementierter Umfang, relevante Entscheidungen, ausgeführte Prüfungen mit Ergebnissen und gegebenenfalls konkrete verbleibende Abweichungen. Nicht ausgeführte Tests ausdrücklich als solche benennen.
## 16. Future Extension Contract
v0.1 ist ein Walking Skeleton des Zielsystems. Die folgenden Fähigkeiten sind ausdrücklich **keine v0.1-Deliverables**, die Architektur darf ihre spätere Ergänzung jedoch nicht durch produktspezifische Schemata, fest verdrahtete Scoring-Annahmen oder n8n-interne Fachlogik unnötig blockieren:
- mehrere externe Produkt- und Preisquellen,
- source-spezifische Search-/Discovery-Adapter,
- Variant-Level- und Market-Level-Preisreferenzserien,
- weitere Produktkategorien ohne produktspezifische DB-Spalten,
- weitere explizit versionierte Preference-Strategien wie `boolean_preferred`, `categorical_match`, `set_contains` oder `categorical_rank`,
- LLM-gestützte Normalisierung mit Raw-Wert, normalisiertem Wert, Confidence und Normalizer-Version,
- n8n für Scheduling, Orchestrierung und Benachrichtigungen,
- automatische Kandidatensuche und Kandidatenaufnahme,
- mehrere parallele Buying-Profile und Watchlists.
Dieser Vertrag verlangt **keine vorsorgliche Infrastruktur** für diese Funktionen. Insbesondere keine Plugin-Registry, keinen Event-Bus, keine Microservices, keine zusätzlichen Preisaggregationstabellen, keine LLM-Abstraktionsschicht und keine n8n-Workflows bauen, solange sie nicht für den aktuellen Slice erforderlich sind.
Die stabilen Erweiterungspunkte von v0.1 sind:
1. Source-Adapter liefern validierte DTOs in den bestehenden Importpfad.
2. Attribute und Constraints bleiben generisch und datengetrieben.
3. Preference-Strategien werden explizit benannt und durch versionierte Scoring-Modelle unterstützt.
4. PriceScore konsumiert eine von der Scoring-Version bereitgestellte Referenzserie statt dauerhaft an Offer-Identität gekoppelt zu sein.
5. Product-, Offer- und Overall-Eligibility bleiben getrennt, damit Monitoring später gute Produkte unabhängig von momentan schlechten Angeboten verfolgen kann.
6. n8n bleibt externer Orchestrator; die Buying Engine bleibt ohne n8n vollständig nutzbar und testbar.

42
Dockerfile Normal file
View File

@ -0,0 +1,42 @@
FROM python:3.13-slim-bookworm AS base
RUN apt-get update \
&& apt-get install -y --no-install-recommends libpq5 \
&& rm -rf /var/lib/apt/lists/*
COPY --from=ghcr.io/astral-sh/uv:0.10.11 /uv /usr/local/bin/uv
WORKDIR /app
ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PATH="/app/.venv/bin:$PATH"
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project
FROM base AS runtime
COPY . .
RUN uv sync --frozen --no-dev \
&& sed -i 's/\r$//' /app/scripts/startup.sh \
&& chmod +x /app/scripts/startup.sh \
&& useradd --create-home --uid 1000 app \
&& chown -R app:app /app
USER app
EXPOSE 8000
CMD ["/app/scripts/startup.sh"]
FROM base AS test
COPY . .
RUN uv sync --frozen --extra test \
&& useradd --create-home --uid 1000 app \
&& chown -R app:app /app
USER app
CMD ["pytest", "tests", "-ra"]

View File

@ -1,7 +1,42 @@
# BuyingAgent
Lokales Arbeitsverzeichnis, verbunden mit Gitea:
Backend für einen generischen Buying-/Deal-Agenten. Der erste fachliche Schnitt ist in `.specs/buying-agent-v0.1-spec.md` beschrieben.
`http://192.168.2.144:3000/Lars/BuyingAgent.git`
Entwicklung auf `develop`. Push nach `develop` baut die Dev-Umgebung auf dem Raspberry Pi. Ein Merge-Commit nach `main` deployt Production. Details: `docs/DEPLOYMENT.md`.
Die eigentliche Spezifikation und Projektstruktur folgen.
## Stack
Python 3.13, FastAPI, Pydantic 2, SQLAlchemy 2, Alembic, PostgreSQL 17, pytest, Docker Compose. Build-Tool: [uv](https://docs.astral.sh/uv/) 0.10.11.
## Lokal starten
```powershell
Copy-Item .env.example .env
docker compose up --build -d
```
```sh
cp .env.example .env
docker compose up --build -d
```
API: `http://127.0.0.1:8000`
Live: `GET /health/live`
Ready: `GET /health/ready` (PostgreSQL erreichbar und Alembic auf Head)
## Tests
```sh
docker compose -f docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from api
```
Die Testdatenbank liegt auf tmpfs und leert kein normales Datenvolume.
## Server
| Umgebung | Branch | Verzeichnis | API |
|---|---|---|---|
| Development | `develop` | `/home/lars/docker/buyingagent-dev` | `http://192.168.2.49:8095` |
| Production | `main` | `/home/lars/docker/buyingagent` | `http://192.168.2.49:8006` |
Repo: `http://192.168.2.144:3000/Lars/BuyingAgent.git`

38
alembic.ini Normal file
View File

@ -0,0 +1,38 @@
[alembic]
script_location = migrations
prepend_sys_path = .
sqlalchemy.url = driver://user:pass@localhost/dbname
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

1
app/__init__.py Normal file
View File

@ -0,0 +1 @@
__version__ = "0.1.0"

0
app/api/__init__.py Normal file
View File

45
app/api/health.py Normal file
View File

@ -0,0 +1,45 @@
from fastapi import APIRouter
from fastapi.responses import JSONResponse
from sqlalchemy import text
from alembic.config import Config
from alembic.runtime.migration import MigrationContext
from alembic.script import ScriptDirectory
from app.persistence.db import engine
from app.settings import get_settings
router = APIRouter()
def _alembic_at_head() -> bool:
config = Config("alembic.ini")
script = ScriptDirectory.from_config(config)
with engine.connect() as connection:
context = MigrationContext.configure(connection)
return set(context.get_current_heads()) == set(script.get_heads())
@router.get("/health/live")
def live() -> dict[str, str]:
return {"status": "ok"}
@router.get("/health/ready", response_model=None)
def ready() -> JSONResponse | dict[str, str]:
settings = get_settings()
try:
with engine.connect() as connection:
connection.execute(text("SELECT 1"))
if not _alembic_at_head():
return JSONResponse(
{"status": "not_ready", "reason": "migration_behind_head"},
status_code=503,
)
except Exception:
return JSONResponse({"status": "not_ready", "reason": "database"}, status_code=503)
return {
"status": "ready",
"environment": settings.environment,
"engine_build": settings.engine_build,
}

10
app/main.py Normal file
View File

@ -0,0 +1,10 @@
from fastapi import FastAPI
from app.api.health import router as health_router
app = FastAPI(
title="Buying Agent",
version="0.1.0",
description="Walking skeleton for the buying and deal evaluation API.",
)
app.include_router(health_router)

View File

18
app/persistence/db.py Normal file
View File

@ -0,0 +1,18 @@
from collections.abc import Generator
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from app.settings import get_settings
settings = get_settings()
engine = create_engine(settings.database_url, pool_pre_ping=True)
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
def get_session() -> Generator[Session, None, None]:
session = SessionLocal()
try:
yield session
finally:
session.close()

27
app/settings.py Normal file
View File

@ -0,0 +1,27 @@
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
db_host: str = "postgres"
db_port: int = 5432
db_name: str = "buyingagent"
db_user: str = "buyingagent"
db_password: str = "change_me"
environment: str = "development"
engine_build: str = "dev"
@property
def database_url(self) -> str:
return (
f"postgresql+psycopg://{self.db_user}:{self.db_password}"
f"@{self.db_host}:{self.db_port}/{self.db_name}"
)
@lru_cache
def get_settings() -> Settings:
return Settings()

View File

@ -0,0 +1,61 @@
# Server development stack: /home/lars/docker/buyingagent-dev
# No fixed container_name — Compose prefixes with the project name.
services:
postgres:
image: postgres:17-alpine
environment:
POSTGRES_DB: "${DB_NAME:-buyingagent_dev}"
POSTGRES_USER: "${DB_USER:-buyingagent_dev}"
POSTGRES_PASSWORD: "${DB_PASSWORD:-dev_password_change_me}"
volumes:
- dev-buyingagent-db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-buyingagent_dev} -d ${DB_NAME:-buyingagent_dev}"]
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped
networks:
- dev-buyingagent-network
api:
build:
context: .
dockerfile: Dockerfile
target: runtime
environment:
DB_HOST: postgres
DB_PORT: "5432"
DB_NAME: "${DB_NAME:-buyingagent_dev}"
DB_USER: "${DB_USER:-buyingagent_dev}"
DB_PASSWORD: "${DB_PASSWORD:-dev_password_change_me}"
ENVIRONMENT: "${ENVIRONMENT:-development}"
ENGINE_BUILD: "${ENGINE_BUILD:-dev}"
ports:
- "${BUYING_AGENT_API_PORT:-8095}:8000"
depends_on:
postgres:
condition: service_healthy
healthcheck:
test:
[
"CMD",
"python",
"-c",
"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/ready')",
]
interval: 10s
timeout: 5s
retries: 18
start_period: 90s
restart: unless-stopped
networks:
- dev-buyingagent-network
volumes:
dev-buyingagent-db-data:
networks:
dev-buyingagent-network:
driver: bridge

45
docker-compose.test.yml Normal file
View File

@ -0,0 +1,45 @@
# Disposable test stack. Uses tmpfs so the normal data volume is never touched.
# docker compose -f docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from api
name: buyingagent-test
services:
postgres:
image: postgres:17-alpine
environment:
POSTGRES_DB: buyingagent_test
POSTGRES_USER: buyingagent_test
POSTGRES_PASSWORD: test
tmpfs:
- /var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U buyingagent_test -d buyingagent_test"]
interval: 5s
timeout: 5s
retries: 10
networks:
- buyingagent-test-network
api:
build:
context: .
dockerfile: Dockerfile
target: test
environment:
DB_HOST: postgres
DB_PORT: "5432"
DB_NAME: buyingagent_test
DB_USER: buyingagent_test
DB_PASSWORD: test
ENVIRONMENT: test
ENGINE_BUILD: test
depends_on:
postgres:
condition: service_healthy
command: ["pytest", "tests", "-ra"]
networks:
- buyingagent-test-network
networks:
buyingagent-test-network:
driver: bridge

64
docker-compose.yml Normal file
View File

@ -0,0 +1,64 @@
# Local skeleton and production stack.
# Local default: 127.0.0.1:8000. Production on the Pi: 0.0.0.0:8006 via .env.
# Host path on the server: /home/lars/docker/buyingagent
services:
postgres:
image: postgres:17-alpine
container_name: buyingagent-db-prod
environment:
POSTGRES_DB: "${DB_NAME:-buyingagent}"
POSTGRES_USER: "${DB_USER:-buyingagent}"
POSTGRES_PASSWORD: "${DB_PASSWORD:-change_me}"
volumes:
- buyingagent-db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-buyingagent} -d ${DB_NAME:-buyingagent}"]
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped
networks:
- buyingagent-network
api:
build:
context: .
dockerfile: Dockerfile
target: runtime
container_name: buyingagent-api
environment:
DB_HOST: postgres
DB_PORT: "5432"
DB_NAME: "${DB_NAME:-buyingagent}"
DB_USER: "${DB_USER:-buyingagent}"
DB_PASSWORD: "${DB_PASSWORD:-change_me}"
ENVIRONMENT: "${ENVIRONMENT:-production}"
ENGINE_BUILD: "${ENGINE_BUILD:-dev}"
ports:
- "${BUYING_AGENT_API_BIND:-127.0.0.1}:${BUYING_AGENT_API_PORT:-8000}:8000"
depends_on:
postgres:
condition: service_healthy
healthcheck:
test:
[
"CMD",
"python",
"-c",
"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/ready')",
]
interval: 10s
timeout: 5s
retries: 12
start_period: 60s
restart: unless-stopped
networks:
- buyingagent-network
volumes:
buyingagent-db-data:
networks:
buyingagent-network:
driver: bridge

87
docs/DEPLOYMENT.md Normal file
View File

@ -0,0 +1,87 @@
# Deployment – BuyingAgent
**Stand:** 2026-10-03
**Server:** Raspberry Pi 5 (`192.168.2.49`) — gleicher Host wie Kanshō/Mitai/Shinkan/Kairo, **eigenes** Compose-Projekt
**Runner:** Gitea Actions (`/home/lars/gitea-runner/`, Label `ubuntu-latest` → Host)
**Repo:** `http://192.168.2.144:3000/Lars/BuyingAgent.git`
Fachliche Grundlage: `.specs/buying-agent-v0.1-spec.md` bzw. `docs/buying-agent-v0.1-spec-v1.1.md`.
v0.1 hat keine Web-UI; öffentlich wird nur die API bereitgestellt.
---
## Port- und Pfad-Übersicht
| | Production | Development |
|---|------------|-------------|
| **Git-Branch** | `main` | `develop` |
| **Server-Verzeichnis** | `/home/lars/docker/buyingagent` | `/home/lars/docker/buyingagent-dev` |
| **API-Port** | 8006 | 8095 |
| **PostgreSQL** | nur Compose-Netz, DB `buyingagent` | nur Compose-Netz, DB `buyingagent_dev` |
| **Compose** | `docker-compose.yml` | `docker-compose.dev-env.yml` |
| **Health** | `http://localhost:8006/health/ready` | `http://localhost:8095/health/ready` |
| **LAN** | `http://192.168.2.49:8006` | `http://192.168.2.49:8095` |
Lokal ohne Server: `127.0.0.1:8000` über `docker compose up --build -d` (siehe README).
8006/8095 waren auf dem Pi frei. 8000 ist durch Paperless belegt und darf auf dem Server nicht verwendet werden.
---
## Branch-Regel
- Entwicklung nur auf `develop`.
- Push auf `develop` startet den Dev-Build auf dem Pi.
- Produktiv nur über Merge-Commit `develop` → `main`:
```bash
git checkout main
git pull origin main
git merge --no-ff develop
git push origin main
```
Kein Fast-Forward, kein direkter Commit auf `main`, kein manuelles `git pull` auf dem Prod-Checkout. Gitea (`deploy-prod.yml`) ist der einzige Prod-Schreibpfad für Code.
---
## Einmalige Server-Einrichtung
Die Workflows legen das Verzeichnis an und klonen bei Bedarf selbst. Actions sind im Repo bereits aktiv. Derselbe Pi-Runner wie die Schwesterprodukte (`ubuntu-latest`).
Nach dem ersten erfolgreichen Deploy `.env` prüfen und für Production ein eigenes `DB_PASSWORD` setzen. PostgreSQL übernimmt User/Passwort nur beim **ersten** Start des Volumes.
```bash
mkdir -p /home/lars/docker/buyingagent /home/lars/docker/buyingagent-dev
```
Watchtower bleibt aus.
---
## Gitea Actions
| Workflow | Trigger | Zweck |
|----------|---------|--------|
| `deploy-dev.yml` | Push `develop` | Deploy nach `/home/lars/docker/buyingagent-dev`, Health `localhost:8095` |
| `deploy-prod.yml` | Push `main` | Deploy nach `/home/lars/docker/buyingagent`, Health `localhost:8006` |
| `test.yml` | nach Dev-Deploy bzw. Push `develop` | nicht-schreibender Smoke gegen `/health/live` und `/health/ready` |
---
## Manuelles Deploy
```bash
# Development
cd /home/lars/docker/buyingagent-dev
git fetch origin develop && git reset --hard origin/develop
docker compose -f docker-compose.dev-env.yml build --no-cache api
BUYING_AGENT_API_PORT=8095 docker compose -f docker-compose.dev-env.yml up -d --wait
curl -sf http://localhost:8095/health/ready
# Production
cd /home/lars/docker/buyingagent
git fetch origin main && git reset --hard origin/main
BUYING_AGENT_API_BIND=0.0.0.0 BUYING_AGENT_API_PORT=8006 docker compose build --no-cache api
BUYING_AGENT_API_BIND=0.0.0.0 BUYING_AGENT_API_PORT=8006 docker compose up -d --wait
curl -sf http://localhost:8006/health/ready
```

View File

@ -0,0 +1,671 @@
# Buying Agent v0.1 – Implementierungsspezifikation
Status: implementierungsbereit · Spezifikationsversion: 1.1 · Stand: 2026-10-03
## Änderungen gegenüber Spezifikationsversion 1.0
- Preference-Scoring erhält ein explizites `strategy`-Feld; v0.1 implementiert ausschließlich `numeric_linear`, ohne vorsorgliche Plugin-Infrastruktur.
- Produkt-Eignung und Angebots-Eignung werden getrennt als `product_eligibility` und `offer_eligibility` bewertet; `overall_eligibility` wird daraus abgeleitet.
- Die Preisbewertung wird konzeptionell von der Identität eines Offers entkoppelt: v0.1 nutzt weiterhin nur die Historie des konkreten Offers, spätere Scoring-Versionen dürfen jedoch Variant- oder Market-Level-Referenzserien konsumieren.
- `raw_value` wird bei JSONB als inhaltlich unverändert, nicht als bytegenau identisch beschrieben.
- Ein verbindlicher `Future Extension Contract` dokumentiert, welche späteren Erweiterungen v0.1 nicht verbauen darf, ohne sie bereits zu implementieren.
## 1. Auftrag und Zielbild
Implementiere ein eigenständig lauffähiges Backend für einen generischen Buying-/Deal-Agenten. Das System bewertet, wie gut eine konkrete Produktvariante zu einem Bedarfsprofil passt und wie attraktiv ein bestimmtes Angebot preislich ist. Es liefert nachvollziehbare Entscheidungen, keine automatischen Käufe.
Notebooks sind der erste Anwendungsfall, nicht das Datenmodell. Weitere Kategorien wie GPU-Server, Monitore oder Kameras müssen durch neue Attribute und Profile ohne Änderungen an der Engine oder zusätzliche produktspezifische DB-Spalten möglich sein.
Diese Datei ist die vollständige Arbeitsgrundlage. Ein Zugriff auf den ursprünglichen Chat ist nicht erforderlich. Die dort beschlossene Architektur wird beibehalten. Die hier definierten Formeln, Fehlerfälle, API-Verträge und Zeitregeln konkretisieren bislang offene Punkte als verbindliche v0.1-Entscheidungen. Die Beispielwerte sind synthetische Testdaten und keine Markt- oder Herstellerangaben.
### Walking Skeleton
Der MVP muss diesen vollständigen Ablauf tatsächlich ausführen und dauerhaft speichern:
```text
Profil → Produkt → Variante + Attribute → Angebot → Preisbeobachtungen
→ Constraints → ProductScore → PriceScore → DealScore
→ Klassifikation + Begründungen → gespeicherte Evaluation → API-Antwort
```
Ein manuell erfasster Kandidat genügt für den ersten vertikalen Durchstich. Danach werden Grenzfälle und drei Beispielkandidaten ergänzt. Keine bloßen Interfaces ohne funktionsfähigen End-to-End-Pfad.
### Explizit nicht enthalten
- Keepa, Idealo, Geizhals oder andere externe Datenquellen und Scraper.
- n8n, Scheduler, Benachrichtigungen und Hintergrundjobs.
- LLM-Normalisierung oder LLM-Bewertung.
- Web-UI; generierte OpenAPI-/Swagger-Dokumentation ist erlaubt.
- Auto-Discovery, automatische Produktsuche oder automatische Zuordnung fremder Produkte.
- Checkout, automatische Käufe, Nutzerkonten, Mehrmandantenfähigkeit und öffentliche Bereitstellung.
- Wechselkursumrechnung, marktweite Preisprognosen, ML und Optimierung von Gewichten.
Diese Funktionen dürfen später über Adapter und Services ergänzt werden. Keine vorsorglichen Frameworks, leeren Integrationen oder Infrastruktur dafür bauen.
## 2. Stack und Architekturgrenzen
Verbindlich: Python 3.13, FastAPI, Pydantic 2, SQLAlchemy 2, Alembic, PostgreSQL 17, pytest und Docker Compose. Verwende einen synchronen SQLAlchemy-Zugriff mit psycopg 3; ein asynchroner Stack ist für v0.1 nicht erforderlich. Abhängigkeiten mit kompatiblen Versionen und einem eingecheckten Lockfile reproduzierbar festlegen.
Abhängigkeiten zeigen nach innen:
```text
HTTP/API → Application Services → Domain / Constraint Engine / Scoring
↓
Repositories und Source-Adapter
↓
PostgreSQL
```
- `domain` und `scoring` importieren weder FastAPI noch SQLAlchemy oder konkrete Quellen.
- Die Engine ist eine reine Berechnung über explizite Eingaben. Kein Netzwerkzugriff, kein DB-Zugriff und kein implizites Lesen der Systemzeit.
- Services laden Daten, erzeugen einen konsistenten Snapshot, rufen die Engine auf und speichern das Ergebnis atomar.
- API-Schemas und ORM-Modelle sind getrennt von Domain-Modellen. Kleine Dataclasses oder Pydantic-Modelle sind für die Domain zulässig.
- Regeln liegen als validierte Daten vor; keine Ausführung von Python, SQL, `eval` oder Templates aus Profilen.
- n8n bleibt später ein externer Aufrufer. Die gesamte Buying Engine funktioniert ohne n8n.
## 3. Fachliche Entitäten und Identität
### Product
Produktfamilie bzw. Modell, beispielsweise „Example Mobile 13“. Felder: UUID `id`, `category`, `brand`, `name`, optional `model`, `created_at`. Keine RAM-, SSD- oder Preisfelder. Kategorie ist ein normalisierter String, kein Notebook-spezifisches Enum.
### ProductVariant
Eine eindeutig beschriebene technische Konfiguration eines Products. Felder: UUID `id`, `product_id`, `name`, optionale Hersteller-SKU `manufacturer_sku`, optionale Kennungen `identifiers` als JSON-Objekt, `created_at`.
RAM, Speicher, Farbe, Tastaturlayout und Ausstattung gehören zur Variante. Angebote unterschiedlicher Konfigurationen dürfen nicht zusammengeführt werden. In v0.1 verknüpft der Aufrufer Varianten explizit; keine heuristische Identitätsauflösung. Hersteller-SKUs sind nicht global eindeutig. Identische Namen sind kein Dublettenbeweis.
### Offer
Ein konkretes Händlerangebot für genau eine Variante. Felder: UUID `id`, `variant_id`, `source_id`, `external_id`, `seller`, optionale `url`, `market` (z. B. `DE`), `currency` (dreistelliger Großbuchstaben-Code), `condition` (`new`, `refurbished`, `used`), `created_at`.
Eindeutigkeit: `(source_id, external_id)`. Unterschiedliche Verkäufer, Zustände, Märkte oder Lieferumfänge sind unterschiedliche Angebote. Technisch abweichende Bundles erfordern eine passende Variante. Diese Identitätsfelder bleiben nach Erstellung unverändert. Ein Offer enthält keinen unabhängig veränderbaren „aktuellen Preis“.
### PriceObservation
Append-only-Beobachtung eines Offers zu einem Zeitpunkt. Felder: UUID `id`, `offer_id`, `source_id`, `external_observation_id`, `observed_at`, `ingested_at`, `item_price`, nullable `shipping_price`, `currency`, `availability` (`in_stock`, `out_of_stock`, `unknown`), `raw_payload` als JSON, `confidence`.
- Geld als `Decimal`/PostgreSQL `NUMERIC(12,2)`, nie binäre Floats. API-Geldwerte werden als Dezimalstrings übertragen.
- `item_price > 0`; bekannte Versandkosten `>= 0`; null bedeutet unbekannt, nicht kostenlos.
- Gesamtpreis = Bruttopreis inklusive bekannter obligatorischer Bestandteile + Versand. Nur direkt für jeden Käufer verfügbare Preise erfassen; keine Mitgliederrabatte, Finanzierung oder personalisierte Coupons modellieren.
- Die Währung muss der Offer-Währung entsprechen. `source_id` muss der Offer-Quelle entsprechen.
- Zeitzonen sind obligatorisch; Speicherung und Ausgabe in UTC. Zeitpunkte in der Zukunft relativ zur Ingestion werden abgewiesen.
- Eindeutigkeit `(offer_id, external_observation_id)`: identischer Wiederholungsimport liefert den vorhandenen Datensatz, widersprüchlicher Inhalt HTTP 409. `ingested_at` ist serverseitig und zählt beim Inhaltsvergleich nicht mit.
- Beobachtungen werden nicht überschrieben. Korrekturen werden als neue Beobachtung mit neuer externer ID erfasst; ein administrativer Korrekturworkflow ist nicht Bestandteil von v0.1.
## 4. Generische Attribute und Normalisierung
Attribute werden für v0.1 ausschließlich an Varianten erfasst. Es gibt keine implizite Vererbung von Product-Attributen. Eine spätere Erweiterung muss eine eigene, explizite Vorrangregel definieren.
### Attributdefinition
`attribute_definitions` enthält `key`, `value_type` (`number`, `string`, `boolean`, `string_list`), nullable `canonical_unit`, optionale `minimum`/`maximum` und `description`. Schlüssel sind stabile `snake_case`-Bezeichner. Neue Definitionen lassen sich per API ohne Migration anlegen. Bestehende Definitionen sind in v0.1 unveränderlich; geänderte Semantik erhält einen neuen Schlüssel.
Beispieldefinitionen:
| key | type | canonical_unit | Wertebereich |
|---|---|---|---|
| ram_gb | number | GB | >= 0 |
| storage_gb | number | GB | >= 0 |
| weight_kg | number | kg | > 0, im Eingabevalidator |
| battery_score | number | null | 0–100 |
| display_score | number | null | 0–100 |
| cpu_score | number | null | 0–100 |
| ports | string_list | null | beliebige Strings |
`*_score`-Attribute sind ausdrücklich manuelle Einschätzungen auf einer dokumentierten 0–100-Skala; die Engine erfindet keine Benchmarks oder Messwerte.
### Attributwert
Jede Zeile in `variant_attributes` ist eine unveränderliche Aussage einer Quelle:
```json
{
"attribute": "ram_gb",
"raw_value": "24GB LPDDR5X RAM",
"normalized_value": 24,
"unit": "GB",
"normalization_status": "normalized",
"normalizer_version": "manual-v1",
"confidence": 1.0,
"source_id": "manual",
"source_reference": "demo/notebook-a/ram",
"observed_at": "2026-10-03T10:00:00Z"
}
```
Zusätzlich serverseitig: UUID `id`, `variant_id`, `ingested_at`. `raw_value` ist verpflichtend und bleibt inhaltlich unverändert erhalten; es darf ein JSON-Skalar oder -Objekt sein. Bei Speicherung als JSONB ist keine bytegenaue Erhaltung der ursprünglichen JSON-Serialisierung garantiert. Falls eine spätere Quelle den exakten Response-Body benötigt, muss dieser separat als Rohtext gespeichert werden; das ist nicht Bestandteil von v0.1. `normalized_value` ist ein typisierter JSON-Wert. Bei `normalization_status=unknown` ist er null; ein optionales `normalization_error` erklärt den fehlenden Wert. Eine erfolglose Normalisierung ist kein Zahlenwert 0.
Manuelle Erfassung liefert Raw- und Normalized-Wert explizit. `ManualNormalizer` validiert Typ, Bereich und kanonische Einheit, analysiert aber keinen Freitext. Falsche Typen oder Einheiten werden mit 422 abgewiesen. Keine automatische Umwandlung von `"32"` zu 32 oder `true` zu 1. `confidence` liegt zwischen 0 und 1 und bezeichnet die Zuverlässigkeit der Aussage, nicht den Produktnutzen. Eine absichtlich unbekannte Aussage ist zulässig.
Bei mehreren Aussagen zu demselben Schlüssel verwendet der Snapshot die jüngste nach `(observed_at, ingested_at, id)` absteigend, jeweils nur bis zum Auswertungszeitpunkt bzw. Snapshot-Cutoff. Anschließend wird deren Confidence geprüft. Ein neuer unsicherer oder unbekannter Wert darf nicht stillschweigend durch einen älteren günstigen Wert ersetzt werden. Vorrang verschiedener Quellen ist damit zunächst bewusst einfach; spätere Auswahlstrategien benötigen eine neue Regelversion.
## 5. Product Profile und Versionierung
Ein Profil beschreibt Kategorie, Budget, harte Mindestanforderungen und gewichtete Präferenzen. Es besitzt eine stabile String-ID und eine positive Integer-Version. Jede `(id, version)` ist unveränderlich. Eine Änderung wird als neue Version gespeichert. Evaluationen müssen eine konkrete Version angeben; kein implizites „latest“.
Vollständiges Beispiel für `profiles/mobile-development-notebook.json`:
```json
{
"id": "mobile-development-notebook",
"version": 1,
"name": "Mobiler Entwicklungsrechner",
"category": "notebook",
"currency": "EUR",
"market": "DE",
"allowed_conditions": ["new"],
"budget": {"target": "1500.00", "maximum": "2000.00"},
"scoring_version": "generic-v1",
"constraints": [
{"id": "min_ram", "attribute": "ram_gb", "operator": ">=", "value": 16, "mandatory": true},
{"id": "min_storage", "attribute": "storage_gb", "operator": ">=", "value": 512, "mandatory": true},
{"id": "max_weight", "attribute": "weight_kg", "operator": "<=", "value": 1.6, "mandatory": true}
],
"preferences": [
{"id": "ram", "attribute": "ram_gb", "strategy": "numeric_linear", "direction": "higher", "acceptable": 16, "ideal": 32, "weight": 0.30},
{"id": "weight", "attribute": "weight_kg", "strategy": "numeric_linear", "direction": "lower", "acceptable": 1.6, "ideal": 1.0, "weight": 0.25},
{"id": "battery", "attribute": "battery_score", "strategy": "numeric_linear", "direction": "higher", "acceptable": 60, "ideal": 100, "weight": 0.20},
{"id": "display", "attribute": "display_score", "strategy": "numeric_linear", "direction": "higher", "acceptable": 60, "ideal": 100, "weight": 0.15},
{"id": "cpu", "attribute": "cpu_score", "strategy": "numeric_linear", "direction": "higher", "acceptable": 60, "ideal": 100, "weight": 0.10}
]
}
```
Jede Präferenz besitzt eine explizite `strategy`. v0.1 unterstützt ausschließlich `numeric_linear`. Das Profilformat ist jedoch so zu modellieren, dass spätere Scoring-Versionen zusätzliche klar benannte Strategien wie `boolean_preferred`, `categorical_match`, `set_contains` oder `categorical_rank` einführen können, ohne die Grundstruktur eines Profils zu ändern. Für v0.1 wird dafür weder ein Plugin-System noch dynamische Code-Ausführung gebaut; unbekannte Strategien werden bei der Profilvalidierung abgewiesen.
Validierung: `0 < target < maximum`, mindestens eine Präferenz, positive Gewichte, innerhalb der jeweiligen Liste eindeutige IDs, bekannte Attributschlüssel und eine vorhandene Scoring-Version. Die Summe der Gewichte muss nicht 1 sein, weil die Engine normalisiert. Für `numeric_linear` gilt: `higher` verlangt `ideal > acceptable`, `lower` verlangt `ideal < acceptable`, und das referenzierte Attribut muss numerisch sein. Kategorie, Währung, Markt und erlaubte Zustände sind verpflichtend.
Das Profil ist ein Beispiel aus der Architekturentscheidung, keine abschließend optimierte persönliche Kaufberatung. Die Auswahl eines Notebooks für mobile Entwicklung wird nicht mit Anforderungen an einen lokalen LLM-Server vermischt.
## 6. Constraint Engine und Eignungsstatus
Unterstütze ausschließlich folgende Operatoren; verwerfe ungültige Kombinationen bereits beim Anlegen des Profils:
| Operator | Attributtyp | Vergleichswert / Semantik |
|---|---|---|
| `>=`, `<=`, `>`, `<` | number | Zahl in kanonischer Einheit |
| `==`, `!=` | number, string, boolean | gleicher skalarer Typ, exakter Vergleich |
| `in` | number, string, boolean | nichtleere Liste gleichartiger skalarer Werte |
| `contains` | string_list | einzelner String, exakte Mitgliedschaft |
| `exists` | alle | kein `value`; verwertbarer normalisierter Wert vorhanden |
Keine impliziten Typkonvertierungen, String-Teilstrings oder case-insensitive Vergleiche. Leere String-Listen sind vorhandene Werte. Fehlend, null, nicht normalisiert oder Confidence unter der Regelgrenze ergibt `UNKNOWN`, auch bei `exists` und `!=`.
Jedes Ergebnis enthält Constraint-ID, Attribut, Operator, Sollwert, Istwert, Status `PASS|FAIL|UNKNOWN`, Mandatory-Flag, gewählte Attributzeilen-ID und Reason-Code.
### Product Eligibility
`product_eligibility` beschreibt ausschließlich, ob die konkrete Produktvariante fachlich zum Profil passt. Dazu gehören:
- Kategorie des Produkts/der Variante,
- alle mandatory Produkt-Constraints,
- technische bzw. produktbezogene Daten, die für diese Constraints benötigt werden.
Ableitung:
- mindestens ein bekannter produktbezogener harter `FAIL` → `product_eligibility=INELIGIBLE`;
- sonst mindestens ein mandatory produktbezogener `UNKNOWN` → `product_eligibility=UNKNOWN`;
- sonst → `product_eligibility=ELIGIBLE`.
Optionale Constraints ändern `product_eligibility` nicht. Optionales `FAIL` oder `UNKNOWN` reduziert ausschließlich den ProductScore um die im Scoring-Modell konfigurierte Punktzahl und bleibt in den Ergebnissen sichtbar.
Der ProductScore darf unabhängig von der aktuellen Angebotslage berechnet und als Diagnostik gespeichert werden, sofern das Profil und die vorhandenen Präferenzdaten dies zulassen. Ein technisch sehr gutes Produkt verliert seinen ProductScore nicht nur deshalb, weil das betrachtete Offer aktuell zu teuer oder nicht lieferbar ist.
### Offer Eligibility
`offer_eligibility` beschreibt ausschließlich die Eignung des konkret ausgewerteten Offers. Systemseitige Offer-Gates sind:
- Markt entspricht dem Profil,
- Zustand ist in `allowed_conditions`,
- Währung entspricht der Profilwährung,
- aktuelle Preisbeobachtung ist vorhanden und frisch,
- Preis-Confidence erfüllt die Mindestschwelle,
- obligatorische Versandkosten sind bekannt,
- Gesamtpreis <= Budgetmaximum,
- aktuelle Verfügbarkeit ist `in_stock`.
Ein bekannter Verstoß ergibt `offer_eligibility=INELIGIBLE`. Fehlende/zu alte Preisbeobachtung, geringe Preis-Confidence, unbekannte Versandkosten oder unbekannte Verfügbarkeit ergeben `offer_eligibility=UNKNOWN`. `out_of_stock` ergibt `INELIGIBLE`. Ein Preis oberhalb des Budgets ist ein Angebotsproblem und macht nicht das Produkt selbst ungeeignet.
### Overall Eligibility
`overall_eligibility` wird ausschließlich aus Product und Offer Eligibility abgeleitet:
- wenn mindestens einer der beiden Status `INELIGIBLE` ist → `overall_eligibility=INELIGIBLE`;
- sonst wenn mindestens einer `UNKNOWN` ist → `overall_eligibility=UNKNOWN`;
- sonst → `overall_eligibility=ELIGIBLE`.
Alle Gates werden ausgewertet, damit die Antwort sämtliche Gründe enthält. Ein bekannter harter Fehler hat innerhalb der jeweiligen Ebene Vorrang vor unbekannten Daten. Das API-Feld `eligible` ist eine Convenience-Abbildung von `overall_eligibility`: `ELIGIBLE` → true, `INELIGIBLE` → false, `UNKNOWN` → null.
## 7. Scoring-Modell `generic-v1`
Alle Scores liegen zwischen 0 und 100. Rechne intern mit Decimal, runde nur endgültige Scores und angezeigte Komponenten auf zwei Dezimalstellen, `ROUND_HALF_UP`. Klassifikation anhand des ungerundeten DealScore. Der Snapshot enthält auch ungerundete Dezimalstrings für eine eindeutige Grenzfallprüfung.
Die unveränderliche Modelldefinition enthält mindestens:
```json
{
"version": "generic-v1",
"algorithm_id": "linear-budget-history-v1",
"supported_preference_strategies": ["numeric_linear"],
"minimum_confidence": 0.8,
"maximum_price_age_hours": 48,
"optional_constraint_penalty_points": 5,
"missing_preference_score": 0,
"product_weight": 0.6,
"price_weight": 0.4,
"budget_weight": 0.4,
"history_weight": 0.6,
"history_window_days": 90,
"minimum_history_days": 3,
"history_discount_for_max_score": 0.2,
"classification_upper_bounds": [
{"upper": 60, "inclusive": false, "label": "IGNORE"},
{"upper": 75, "inclusive": false, "label": "WATCH"},
{"upper": 85, "inclusive": false, "label": "GOOD"},
{"upper": 92, "inclusive": true, "label": "VERY_GOOD"},
{"upper": 100, "inclusive": true, "label": "EXCEPTIONAL"}
]
}
```
`clamp(x)=min(100,max(0,x))`. Modellvalidierung prüft positive Zeitfenster, gültige Confidence, nichtnegative Gewichte mit jeweiliger Summe 1, aufsteigende lückenlose Klassifikationsgrenzen, unterstützte Algorithmus-ID und eine nichtleere Liste unterstützter Preference-Strategien. Die Definition wird als kanonisches JSON inklusive SHA-256 gespeichert. Eine Version darf niemals mit geändertem Inhalt wiederverwendet werden. Neue Algorithmen oder Preference-Strategien werden als Code ergänzt und unter einer neuen Scoring-Version bzw. Algorithmus-ID registriert; JSON darf keinen ausführbaren Code enthalten.
### ProductScore
v0.1 implementiert ausschließlich die Preference-Strategie `numeric_linear`. Für eine solche numerische Präferenz mit Istwert `x`, akzeptablem Wert `a` und Idealwert `i`:
```text
higher: component = clamp(50 + 50 × (x − a) / (i − a))
lower: component = clamp(50 + 50 × (a − x) / (a − i))
base_product_score = Σ(weight × component) / Σ(weight)
product_score = clamp(base_product_score − 5 × Anzahl optionaler FAIL/UNKNOWN)
```
Damit erhält ein akzeptabler Wert 50, ein idealer oder besserer Wert maximal 100. Ein fehlender oder unsicherer Präferenzwert erhält 0; sein Gewicht bleibt im Nenner. Fehlende Daten dürfen den Score nicht verbessern. `product_coverage` ist die Summe der Gewichte verwertbarer Präferenzen geteilt durch die Summe aller Gewichte. Confidence wird nicht nochmals als Score-Multiplikator verwendet.
Der ProductScore beschreibt Produkt-Fit und wird unabhängig von `offer_eligibility` berechnet. Auch bei `product_eligibility=INELIGIBLE` oder `UNKNOWN` darf er diagnostisch ausgegeben werden; Mandatory Gates bleiben jedoch autoritativ und können niemals durch einen hohen ProductScore überstimmt werden.
Die Strategy-Auswahl erfolgt explizit über das Preference-Feld `strategy`; es gibt keine dynamische Plugin-Suche. Unbekannte Strategien sind für `generic-v1` ein Validierungsfehler.
### Auswahl des aktuellen Preises und der Referenzhistorie
Der Evaluation-Request bezeichnet genau ein Offer. Kein implizites billigstes Angebot und kein Vermischen mehrerer Händler oder Varianten.
v0.1 verwendet als `reference_history` ausschließlich Preisbeobachtungen dieses konkreten Offers. Diese Bindung ist eine Entscheidung der Scoring-Version, nicht des generischen Preis-Scoring-Vertrags. Die reine Preis-Scoring-Funktion ist daher konzeptionell als Berechnung über `current_price`, `reference_history` und `budget` zu implementieren; sie darf fachlich nicht verlangen, dass die Referenzserie dauerhaft ein Offer-Objekt ist. Spätere Scoring-Versionen dürfen Variant-Level- oder Market-Level-Referenzserien bereitstellen, ohne Product, ProductVariant, Offer oder PriceObservation neu zu modellieren.
Für v0.1 gilt für die offer-bezogene Referenzserie:
1. Betrachte ausschließlich Beobachtungen dieses Offers mit `observed_at <= as_of` und `ingested_at <= snapshot_cutoff`.
2. Aktuell ist die jüngste Zeile nach `(observed_at, ingested_at, id)` absteigend. Auch eine nicht verfügbare oder unsichere jüngste Zeile darf nicht durch eine ältere günstigere ersetzt werden.
3. Frische: `as_of − observed_at <= 48 Stunden`, einschließlich Grenze.
4. Historie endet strikt vor dem UTC-Kalendertag der aktuellen Beobachtung. Dadurch wird der aktuelle Angebotspreis nicht selbst zur historischen Referenz.
5. Für jedes Fenster N ∈ {30,90,365} gilt `[current_day − N Tage, current_day)`. Wähle zunächst pro UTC-Tag die jüngste Zeile, danach filtere Tage mit `in_stock`, bekannter Gesamtsumme und Confidence >= 0.8. So verdrängt eine spätere unbekannte Beobachtung eine frühere bekannte desselben Tages.
6. Berechne `median_30d`, `median_90d`, `lowest_365d` aus diesen Tageswerten. Bei gerader Anzahl ist der Median das arithmetische Mittel der beiden mittleren Werte. Gib die Zahl der vorhandenen Tage pro Fenster mit aus; keine Interpolation fehlender Tage.
Historienaggregate werden berechnet und im Evaluation-Snapshot gespeichert, nicht als vom Client beliebig vorgegebene Wahrheit übernommen. Die v0.1-Historie ist angebotsbezogen und keine Aussage über den Gesamtmarkt. `lowest_365d` darf als Minimum weniger beobachteter Tage angezeigt werden, muss dann ausdrücklich als Minimum der vorhandenen Daten bezeichnet sein. Der Snapshot speichert zusätzlich `price_reference_scope="offer"` und die IDs der verwendeten Beobachtungen, damit spätere Scoring-Versionen andere Scopes eindeutig unterscheiden können.
### PriceScore
Seien `p` aktueller Gesamtpreis, `t` Budgetziel, `m` Budgetmaximum, `r` 90-Tage-Median der von der Scoring-Version bereitgestellten Referenzhistorie:
```text
budget_score = 100 falls p <= t
budget_score = clamp(100 × (m − p) / (m − t)) sonst
discount = (r − p) / r
history_score = clamp(50 + 50 × discount / 0.20)
price_score = 0.40 × budget_score + 0.60 × history_score
```
Historien-Score: beim Median 50, bei 20 % günstiger 100, bei 20 % teurer 0. Nur bei mindestens drei verfügbaren Tagen im 90-Tage-Fenster ist `history_score` vorhanden. Andernfalls gilt `price_score=budget_score`, `price_basis=BUDGET_ONLY`, `history_score=null` und ein expliziter Hinweis `INSUFFICIENT_HISTORY`. Bei ausreichender Historie `price_basis=BUDGET_AND_HISTORY`. Kein Bonus für ein vermeintliches Jahrestief; 30-Tage-Median und Minimum dienen zunächst nur der Erklärung.
Ohne frischen, hinreichend sicheren und vollständig bekannten Gesamtpreis in Profilwährung ist der PriceScore null. Ein bekannter Preis oberhalb des Budgets kann diagnostisch bewertet werden, während `offer_eligibility` wegen des Budget-Gates `INELIGIBLE` bleibt. Bei falscher Währung wird ohne Wechselkursumrechnung kein PriceScore berechnet. Ausverkaufte Angebote dürfen ihren bekannten Preis diagnostisch behalten; `offer_eligibility` bleibt dennoch `INELIGIBLE`.
### DealScore und Klassifikation
Nur bei `overall_eligibility=ELIGIBLE` und vorhandenem PriceScore:
```text
deal_score = 0.60 × product_score + 0.40 × price_score
```
| Bereich des ungerundeten DealScore | Klassifikation |
|---|---|
| < 60 | IGNORE |
| >= 60 und < 75 | WATCH |
| >= 75 und < 85 | GOOD |
| >= 85 und <= 92 | VERY_GOOD |
| > 92 | EXCEPTIONAL |
Bei `overall_eligibility=INELIGIBLE`: DealScore null, Klassifikation `INELIGIBLE`. Bei `overall_eligibility=UNKNOWN`: DealScore null, Klassifikation `INSUFFICIENT_DATA`. ProductScore und vorhandener PriceScore dürfen als Diagnostik ausgegeben werden, ersetzen aber niemals die Gate-Ergebnisse. Die getrennten Product-/Offer-Status müssen in Snapshot und API sichtbar bleiben, damit ein gutes Produkt mit momentan schlechtem Angebot weiterhin beobachtbar bleibt.
### Nachvollziehbarkeit und Wiederholbarkeit
Jede Evaluation speichert Profil und Version, Modellversion und Hash, Engine-Build-ID, `as_of`, `snapshot_cutoff`, `product_eligibility`, `offer_eligibility`, `overall_eligibility`, vollständige tatsächlich verwendete Eingaben und IDs sowie alle Komponenten, Gates, Aggregate, Coverage, Price-Reference-Scope, Reason-Codes und verständliche Texte. Persistierte Evaluationen sind unveränderlich. Ein erneuter Request erzeugt eine neue Evaluation; ein GET berechnet nichts neu.
Historische Abfragen mit `as_of` rekonstruieren nur den Datenstand zum aktuellen Snapshot-Cutoff, einschließlich später eingegangener Rückdatierungen. Sie behaupten nicht, den damaligen Wissensstand wiederherzustellen. Exakte Wiederholung einer alten Bewertung erfolgt intern aus deren gespeichertem Snapshot und zugehöriger Algorithmusversion. Alte Algorithmen bleiben verfügbar; kein Replay-HTTP-Endpunkt für v0.1 erforderlich.
## 8. Source-Adapter
Implementiere einen kleinen `Protocol` und `ManualSource`. Quell-DTOs enthalten Rohdaten, Quellenkennung, externe IDs, Zeitpunkte und Confidence; sie enthalten keine ORM-Objekte und keine Scoring-Logik.
```python
class ProductSource(Protocol):
source_key: str
def get_product(self, external_id: str) -> SourceProduct: ...
def get_offers(self, external_product_id: str) -> list[SourceOffer]: ...
def get_price_history(self, external_offer_id: str) -> list[SourcePriceObservation]: ...
```
`ManualSource` liest einen validierten lokalen Demo-Datensatz, keine externe Website. Ein Import-Service nimmt den Adapter entgegen und legt über dieselben Application Services wie die API Produkte, Varianten, Attribute, Angebote und Beobachtungen an. Explizite Variantenzuordnung aus der Fixture; keine Matching-Engine. Wiederholter Demo-Import erzeugt keine Duplikate: stabile UUIDs für Produkte/Varianten/Attribute, stabile externe Offer-/Beobachtungs-IDs und identischer Inhalt; Widersprüche melden statt überschreiben.
Adapter normalisieren keine Preise oder technischen Eigenschaften im Scoring-Modul. Der Pfad lautet Quelle → validierte DTOs → Normalisierung → Persistenz → Evaluation. Für spätere Quellen genügen zunächst Adapter und Import-Mapping. Pagination, Rate Limits und Retry-Strategien erst zusammen mit der ersten realen Quelle einführen.
## 9. HTTP-API
Alle fachlichen Endpunkte unter `/api/v1`. UUIDs für Entitäten, String-IDs für Profile, Attribute und Scoring-Modelle. Pydantic-Schemas mit `extra=forbid`; Pflichtfelder und Enums im OpenAPI-Schema dokumentieren. JSON-Ausgabe nutzt `snake_case`.
| Methode / Pfad | Vertrag |
|---|---|
| GET `/health/live` | Prozess lebt, kein DB-Zugriff |
| GET `/health/ready` | DB erreichbar und Migration auf Head; sonst 503 |
| POST `/api/v1/attribute-definitions` | unveränderliche Definition anlegen |
| GET `/api/v1/attribute-definitions` | Definitionen listen |
| GET `/api/v1/sources` | registrierte Quellen, einschließlich `manual` |
| GET `/api/v1/scoring-models/{version}` | vollständige Regeldefinition + Hash |
| POST `/api/v1/profiles` | vollständiges Profil inklusive Version anlegen |
| GET `/api/v1/profiles` | konkrete Profilversionen listen |
| GET `/api/v1/profiles/{id}?version=1` | konkrete Version; Query verpflichtend |
| POST `/api/v1/products` | Product anlegen |
| GET `/api/v1/products/{id}` | Product lesen |
| POST `/api/v1/products/{id}/variants` | Variante anlegen |
| GET `/api/v1/variants/{id}` | Variante mit Attributaussagen lesen |
| POST `/api/v1/variants/{id}/attributes` | einzelne unveränderliche Attributaussage hinzufügen |
| POST `/api/v1/variants/{id}/offers` | Offer anlegen |
| GET `/api/v1/offers/{id}` | Offer lesen |
| POST `/api/v1/offers/{id}/prices` | einzelne Beobachtung hinzufügen bzw. identisch wiederholen |
| GET `/api/v1/offers/{id}/prices` | gespeicherte Beobachtungen listen |
| POST `/api/v1/evaluate/{variant_id}` | auswerten und Ergebnis atomar speichern |
| GET `/api/v1/evaluations/{id}` | gespeichertes Ergebnis inklusive Snapshot lesen |
Listen verwenden `limit` (Default 50, maximal 200) und `offset` (>=0); Antwort `{items, limit, offset, total}`. Stabile Sortierung nach Primärschlüssel, bei Preisen `(observed_at, ingested_at, id)` aufsteigend. Keine Update-/Delete-Endpunkte in v0.1.
Evaluation-Request:
```json
{
"profile_id": "mobile-development-notebook",
"profile_version": 1,
"offer_id": "33333333-3333-4333-8333-333333333333",
"as_of": "2026-10-03T12:00:00Z"
}
```
`as_of` ist optional; der Service setzt es einmal auf Serverzeit UTC. Zukunftswerte sind ungültig. Offer muss zur angegebenen Variante gehören (422). Das Profil legt die Scoring-Version fest; kein stiller Override. Fachlich unpassende Kategorie wird im Product-Gate gespeichert; Markt, Zustand, Währung, Preis und Verfügbarkeit werden im Offer-Gate gespeichert. Keiner dieser fachlichen Fälle ist ein HTTP-Fehler.
Evaluation-Response enthält mindestens:
```json
{
"id": "44444444-4444-4444-8444-444444444444",
"profile_id": "mobile-development-notebook",
"profile_version": 1,
"scoring_version": "generic-v1",
"product_eligibility": "ELIGIBLE",
"offer_eligibility": "ELIGIBLE",
"overall_eligibility": "ELIGIBLE",
"eligible": true,
"product_score": 90.21,
"price_score": 98.24,
"deal_score": 93.42,
"classification": "EXCEPTIONAL",
"price_basis": "BUDGET_AND_HISTORY",
"product_coverage": 1.0,
"failed_constraints": [],
"unknown_constraints": [],
"reasons": [
{"code": "PRICE_BELOW_MEDIAN", "message": "Gesamtpreis 18,83 % unter dem Median der beobachteten 90-Tage-Historie."}
]
}
```
Zusätzlich verpflichtend: `variant_id`, `offer_id`, `as_of`, `snapshot_cutoff`, `scoring_hash`, `engine_build`, `product_eligibility`, `offer_eligibility`, `overall_eligibility`, `components`, sämtliche `constraint_results` und Offer-Gates, `price_statistics`, `price_reference_scope`, `input_snapshot` und `warnings`. Die Kurzantwort oben illustriert nur die Kernfelder. `failed_constraints` enthält alle `FAIL`-Ergebnisse; `unknown_constraints` alle `UNKNOWN`-Ergebnisse. Reason-Codes sind stabil, Texte dienen Menschen und dürfen nicht als Programmierschnittstelle ausgewertet werden.
POST liefert 201, identisch wiederholte Preisbeobachtung 200; GET liefert 200. Fehlende IDs: 404. Verletzte Eindeutigkeit/abweichender Wiederholungsinhalt: 409. Ungültige Payload/Referenzkombination: 422. Einheitliches Fehlerformat `{error: {code, message, details}}`, auch für Request-Validierung. Keine SQL-Details an Clients. Eine fachlich negative Evaluation ist eine erfolgreiche 201-Antwort.
## 10. Datenbank und Transaktionen
Alle Zeitstempel `TIMESTAMPTZ`; JSON-Daten `JSONB`; UUID-Primärschlüssel für Entitäten; Decimal-Geldfelder. `created_at`/`ingested_at` werden serverseitig gesetzt. Fremdschlüssel sind obligatorisch. Kein Cascade-Delete historischer Bewertungen oder Quelldaten.
| Tabelle | Wesentliche Spalten und Constraints |
|---|---|
| `attribute_definitions` | `key` PK, Typ, Einheit, Bereich, Beschreibung |
| `sources` | `id` String-PK, `name`, `adapter_type`; Seed `manual` |
| `scoring_models` | `version` PK, `algorithm_id`, `definition` JSONB, `definition_hash`, `created_at` |
| `product_profiles` | zusammengesetzter PK `(id, version)`, `definition` JSONB, `category`, FK `scoring_version`, `definition_hash`, `created_at` |
| `products` | `id`, Kategorie, Marke, Name, Modell, `created_at` |
| `product_variants` | `id`, FK Product, Name, Hersteller-SKU, Kennungen JSONB, `created_at` |
| `variant_attributes` | `id`, FK Variante/Definition/Quelle, Raw/Normalized JSONB, Einheit, Status, Normalizer-Version, Confidence, Source-Referenz, Beobachtungs-/Ingestion-Zeit, Fehler |
| `offers` | `id`, FK Variante/Quelle, externe ID, Händler, URL, Markt, Währung, Zustand; UNIQUE Quelle/externe ID |
| `price_observations` | `id`, FK Offer/Quelle, externe Beobachtungs-ID, Preise, Währung, Verfügbarkeit, Confidence, Raw JSONB, beide Zeiten; UNIQUE Offer/externe ID |
| `evaluations` | `id`, FK Variante/Offer/Profilversion/Scoring-Version, Zeitpunkte, Hash, Build-ID, `product_eligibility`, `offer_eligibility`, `overall_eligibility`, drei nullable Scores, Klassifikation, `price_reference_scope`, vollständige Eingabe-/Ergebnis-Snapshots JSONB |
| `evaluation_components` | `id`, FK Evaluation, `kind`, `component_key`, nullable Rohscore/Gewicht/Beitrag, `details` JSONB; UNIQUE Evaluation/kind/key |
`evaluation_components` enthält Präferenzen inklusive Strategy, optionale Abzüge, Budget-/History-Komponenten, Product-Gates und Offer-Gates. JSON-Snapshots sind die vollständige Replay-Grundlage; relationale Komponenten dienen Abfragen und dürfen bei Erstellung nicht davon abweichen.
DB-Checks mindestens für nichtnegative Preise, positiven Artikelpreis, Confidence 0–1, Scores 0–100, Profilversion >0 und gültige feste Enums. Typ-/Einheiten-/Profilvalidierung bleibt zusätzlich in der Application-Schicht. Redundante Indexspalten eines Profils müssen aus dessen validierter Definition erzeugt werden.
Indizes: Attribute `(variant_id, attribute_key, observed_at DESC, ingested_at DESC, id DESC)`, Preise `(offer_id, observed_at DESC, ingested_at DESC, id DESC)`, Offers `variant_id`, Evaluationen `(profile_id, profile_version, created_at DESC)` und `variant_id`.
Eine Evaluation liest und schreibt innerhalb einer PostgreSQL-Transaktion mit konsistentem Snapshot (`REPEATABLE READ`). Zu Beginn `snapshot_cutoff` festlegen; komplette Inputs, Ergebnis und Komponenten atomar committen. Bei Fehlern kein Teilergebnis. Insert-Konflikte über UNIQUE-Constraints behandeln, nicht allein per vorherigem SELECT.
Alembic ist die einzige produktive Schemaanlage; kein `metadata.create_all()` beim API-Start. Initialmigration enthält Tabellen, Constraints und Indizes. Bootstrap von Quellen und Regeldefinitionen separat, idempotent und inhaltlich geprüft. Ein Konflikt gleicher Version mit anderem Hash stoppt den Bootstrap sichtbar.
## 11. Repository und Lieferumfang
```text
buying-agent/
├── README.md
├── Dockerfile
├── docker-compose.yml
├── .dockerignore
├── .env.example
├── .gitignore
├── pyproject.toml
├── uv.lock
├── alembic.ini
├── migrations/
│ ├── env.py
│ └── versions/
├── app/
│ ├── main.py
│ ├── settings.py
│ ├── api/
│ │ ├── schemas.py
│ │ ├── profiles.py
│ │ ├── products.py
│ │ ├── offers.py
│ │ ├── evaluations.py
│ │ └── health.py
│ ├── domain/
│ │ ├── models.py
│ │ ├── attributes.py
│ │ ├── constraints.py
│ │ └── enums.py
│ ├── scoring/
│ │ ├── product.py
│ │ ├── price.py
│ │ ├── deal.py
│ │ └── engine.py
│ ├── normalization/
│ │ ├── base.py
│ │ └── manual.py
│ ├── sources/
│ │ ├── base.py
│ │ └── manual.py
│ ├── persistence/
│ │ ├── db.py
│ │ ├── models.py
│ │ └── repositories.py
│ └── services/
│ ├── ingestion_service.py
│ └── evaluation_service.py
├── rules/generic-v1.json
├── profiles/mobile-development-notebook.json
├── fixtures/manual-demo.json
├── scripts/
│ ├── bootstrap.py
│ └── demo.py
└── tests/
├── unit/
├── integration/
└── e2e/
```
`__init__.py` nach Bedarf ergänzen. Kleine Dateien dürfen sinnvoll zusammengelegt werden; Architekturgrenzen sind verbindlich. Kein generisches Repository-Framework und kein Plugin-Discovery-System erforderlich. Das Lockfile-Beispiel verwendet uv; uv als Build-/Entwicklungswerkzeug mit dokumentierter Version verwenden.
README enthält Setup, Architektur, Datenmodell, Formeln, Fehlersemantik, alle Start-/Testkommandos, vollständige Beispielrequests sowie Grenzen der synthetischen Preisbewertung. Diese Spezifikation ebenfalls im Repository unter `docs/buying-agent-v0.1-spec-v1.1.md` ablegen.
## 12. Docker und lokale Ausführung
Compose enthält nur zwei dauerhaft laufende Services: `api` und `postgres`. PostgreSQL 17 mit benanntem Datenvolume und Healthcheck. API basiert auf Python 3.13, läuft als Nicht-Root und bindet den veröffentlichten Port an `127.0.0.1:8000`. Die Datenbank braucht keinen veröffentlichten Host-Port. Secrets und lokale DB-Zugänge über `.env`, nur Platzhalter in `.env.example`.
Für das lokale Einzelinstanz-Skeleton darf das API-Entrypoint nach erfolgreichem DB-Healthcheck sequenziell `alembic upgrade head`, Bootstrap und Uvicorn ausführen. Fehlgeschlagene Migrationen verhindern den API-Start. Diese Startmigration ist keine Strategie für spätere Mehrinstanz-Deployments.
Verbindlicher Startpfad:
```sh
cp .env.example .env
docker compose up --build -d
docker compose exec api python -m scripts.demo
```
Für PowerShell zusätzlich `Copy-Item .env.example .env` dokumentieren. `scripts.demo` legt Definitionen, Profil und synthetische Kandidaten über die API an, wiederholt sich ohne Duplikate und ruft Evaluationen mit explizitem `as_of` auf. Die Demo umfasst mindestens die Fälle A, B, C und D aus Abschnitt 13. Statische Fixtures nutzen den unten definierten historischen Zeitpunkt, damit sie nicht mit der Kalenderzeit altern. Der Adapterpfad wird separat im Integrationstest geprüft; die Demo muss den HTTP-Pfad abdecken.
Testabhängigkeiten in einer Docker-Build-Stage `test` bereitstellen. README dokumentiert einen ausführbaren Compose-Test-Override oder gleichwertigen Befehl mit dieser Stage und separater Testdatenbank. Tests dürfen niemals das normale Datenvolume leeren. Kein dauerhafter dritter Service nötig.
## 13. Deterministische Beispiele und Tests
### Referenzfall A
`as_of=2026-10-03T12:00:00Z`, aktuelle Beobachtung um 10:00 UTC; alle technischen Attribute vorher beobachtet. Kategorie Notebook, Markt DE, Zustand neu, EUR, lieferbar, Confidence jeweils 1.0, Versand 0.00.
| Attribut | Wert | Präferenzscore |
|---|---:|---:|
| ram_gb | 32 | 100 |
| storage_gb | 1024 | nur Mindestconstraint |
| weight_kg | 1.2 | 83.333333… |
| battery_score | 90 | 87.5 |
| display_score | 90 | 87.5 |
| cpu_score | 90 | 87.5 |
Gesamtpreise der Historie: 1599.00 am 2026-09-01, 1599.00 am 2026-09-15, 1599.00 am 2026-10-01; jeweils 10:00 UTC. Aktuell 1298.00. Alle Zeilen betreffen dasselbe Offer und sind lieferbar. Fixtures werden vor Evaluation vollständig importiert.
Erwartungen:
```text
product_eligibility = ELIGIBLE
offer_eligibility = ELIGIBLE
overall_eligibility = ELIGIBLE; eligible = true
product_score = 90.21
median_30d = 1599.00 (2 Tage)
median_90d = 1599.00 (3 Tage)
lowest_365d = 1599.00 (3 Tage)
budget_score = 100
history_score = 97.06
price_score = 98.24
deal_score = 93.42
classification = EXCEPTIONAL
product_coverage = 1.0
```
Die Engine rechnet mit den ungerundeten Zwischenwerten. Die Response aus Abschnitt 9 bezieht sich auf diesen Fall. Das beobachtete Minimum von 1599.00 ist ausdrücklich kein belegtes Jahrestief des Marktes.
### Weitere Demokandidaten
- B: identisch zu A, aber 8 GB RAM → `product_eligibility=INELIGIBLE`, `offer_eligibility=ELIGIBLE`, `overall_eligibility=INELIGIBLE`, Mandatory RAM `FAIL`, ProductScore weiterhin diagnostisch vorhanden, kein DealScore.
- C: identisch zu A, aber keine verwertbare Gewichtsangabe → `product_eligibility=UNKNOWN`, `offer_eligibility=ELIGIBLE`, `overall_eligibility=UNKNOWN`, Mandatory Gewicht `UNKNOWN`, ProductScore diagnostisch vorhanden, Klassifikation `INSUFFICIENT_DATA`, kein DealScore.
- D: technisch identisch zu A, aber aktueller Gesamtpreis 2049.00 bei Budgetmaximum 2000.00 → `product_eligibility=ELIGIBLE`, `offer_eligibility=INELIGIBLE`, `overall_eligibility=INELIGIBLE`; ProductScore und diagnostischer PriceScore bleiben vorhanden, DealScore null. Dieser Fall beweist die Trennung von Produkt-Fit und Angebots-Fit.
### Unit-Tests
- Jeden Constraint-Operator mit PASS, FAIL, UNKNOWN; Gleichheitsgrenzen, strikte Typen, leere Listen und Confidence exakt 0.8 bzw. knapp darunter.
- Preference-Strategie `numeric_linear`: akzeptabel 50, ideal 100, Clamping, lower/higher, nicht auf 1 summierte Gewichte, fehlende Werte und optionale Abzüge; unbekannte `strategy` wird für `generic-v1` abgewiesen.
- Budget bei Ziel/Maximum/darüber; History-Score bei −20 %, 0 % und +20 % Rabatt; BUDGET_ONLY bei 0–2 Historientagen.
- Tagesauswahl vor Qualitätsfilter, Fenstergrenzen, Ausschluss des aktuellen Tages, Median gerader/ungerader Anzahl, identische Zeitstempel und stabile Tie-Breaks.
- Keine Vermischung von Offers, Varianten, Währungen oder Zuständen; aktuelle Zeile unbekannt/ausverkauft ohne Rückfall auf ältere Zeilen; ProductScore bleibt von Offer-Gates unabhängig.
- Frische exakt 48 Stunden/knapp darüber; unbekannter Versand; fehlender Preis; Kombination Mandatory FAIL und UNKNOWN.
- Klassifikationsgrenzen 60, 75, 85, 92 und unmittelbar darüber; Rundung darf keine Klasse verschieben.
- Referenzfall A einschließlich exakter gerundeter Resultate; andere Kategorie mit eigenen Attributen ohne Engine-Änderung; Fall D zur Trennung von Product/Offer Eligibility.
- Preis-Scoring-Funktion wird mit explizitem `current_price`, `reference_history` und `budget` getestet; Domain-Test darf dafür kein Offer-ORM-Objekt benötigen. `price_reference_scope=offer` ist für v0.1 verpflichtend.
- Replay aus gespeichertem Input trotz später hinzugefügter Attribute/Preise liefert identische fachliche Ergebnisse; generierte IDs und Speicherzeit sind kein Bestandteil des Vergleichs.
### Integration und End-to-End
- Gegen echtes PostgreSQL 17, nicht SQLite als Ersatz für JSONB, Numeric, Constraints oder Isolation.
- Migration auf leerer Test-DB; erneutes Upgrade ohne Änderung; initialer Downgrade/Upgrade nur in wegwerfbarer Test-DB.
- Bootstrap identisch wiederholbar, veränderte Regel mit gleicher Version wird abgewiesen.
- CRUD-Anlage-/Lesepfade, validierte Profilreferenzen, 404/409/422, Pagination, exakte Decimal- und UTC-Roundtrips.
- Doppelte Preisimporte auch bei konkurrierenden Requests: eine Beobachtung, kein stiller Content-Overwrite.
- Gescheiterte Evaluation-Persistenz hinterlässt weder Evaluation noch Komponenten.
- HTTP-End-to-End: Profil → Produkt → Variante → Attribute → Offer → Historie → Evaluation → GET; danach API-Neustart und erneutes GET desselben unveränderten Ergebnisses.
- ManualSource-Import nutzt dieselben Services und erzeugt bei Wiederholung keine Duplikate.
- Einfügen neuer Preisbeobachtungen ändert alte Evaluationen nicht; neue Evaluation verwendet neue Inputs.
## 14. Implementierungsreihenfolge
1. Projekt, Settings, Docker, PostgreSQL und initiale Migration aufsetzen.
2. Domain-Modelle, Definitions-/Profilvalidierung einschließlich `strategy`, getrennte Product-/Offer-Gates und reine Constraint-/Scoring-Funktionen samt Referenztests implementieren.
3. Preis-Scoring als reine Berechnung über `current_price`, `reference_history` und `budget` implementieren; v0.1-Assembler liefert dafür ausschließlich offer-bezogene Historie und markiert `price_reference_scope=offer`.
4. Repositories und transaktionalen Evaluation-Service integrieren.
5. API, ManualSource und Bootstrap anschließen; ersten vollständigen Durchstich ausführen.
6. Demo, Integrationstests, Fehlerfälle und nachvollziehbare Responses vervollständigen.
7. README und sauberen Start auf leerem Volume prüfen; tatsächliche Testergebnisse dokumentieren.
Routineentscheidungen innerhalb dieser Spezifikation selbst treffen. Falls eine Vorgabe technisch widersprüchlich ist, den konkreten Konflikt dokumentieren und minimal auflösen; keine stillen fachlichen Formeländerungen oder Scope-Erweiterungen. Insbesondere keine Infrastruktur für spätere Preference-Strategien, Variant-/Market-Level-Preisreihen, n8n, LLMs oder Discovery vorwegnehmen.
## 15. Definition of Done
- [ ] Ein frischer Checkout startet mit den dokumentierten Docker-Kommandos; Migration und Readiness funktionieren.
- [ ] PostgreSQL persistiert Daten über API-/Container-Neustarts hinweg.
- [ ] Alle vier Entitäten Product, ProductVariant, Offer, PriceObservation sind getrennt und korrekt verknüpft.
- [ ] Attribute sind generisch und bewahren Raw inhaltlich unverändert, Normalized, Quelle, Confidence und Normalizer-Version.
- [ ] Beispielprofil und `generic-v1` liegen versioniert als Dateien vor und werden validiert gespeichert; jede Preference besitzt `strategy`, v0.1 akzeptiert ausschließlich `numeric_linear`.
- [ ] Product-, Offer- und Overall-Eligibility werden getrennt berechnet, persistiert und über die API ausgegeben.
- [ ] Ein technisch passendes Produkt behält ProductScore/Product Eligibility unabhängig davon, ob das konkrete Offer aktuell zu teuer, ausverkauft oder anderweitig ungeeignet ist.
- [ ] Das Preis-Scoring ist fachlich über `current_price`, `reference_history` und `budget` entkoppelt; v0.1 verwendet ausschließlich `price_reference_scope=offer` und implementiert keine Variant-/Market-Level-Historie.
- [ ] Demo liefert für A, B, C und D die festgelegten Ergebnisse über die API.
- [ ] Scores, harte Gates, fehlende Daten, Historienbasis, Reference Scope und Begründungen sind sichtbar und reproduzierbar.
- [ ] Evaluationen enthalten vollständige unveränderliche Snapshots; spätere Daten und Regelversionen verändern sie nicht.
- [ ] Unit-, PostgreSQL-Integrations- und HTTP-End-to-End-Tests sind grün; Kommandos und Resultate sind dokumentiert.
- [ ] Neue Kategorie und neue Attribute funktionieren ohne Schema- oder Engine-Änderung, solange sie von den in der gewählten Scoring-Version unterstützten Strategies/Operatoren abgedeckt werden.
- [ ] Keine reale externe Quelle, kein n8n, keine LLM-Normalisierung, keine Web-UI, keine Auto-Discovery, keine Variant-/Market-Level-Preisaggregation und keine vorsorgliche Plugin-Infrastruktur wurden eingebaut.
- [ ] README ermöglicht einem anderen Entwickler Setup, Demo, Tests und Verständnis der Grenzen ohne Chat-Kontext.
Abschlussbericht des Coding-Agenten: implementierter Umfang, relevante Entscheidungen, ausgeführte Prüfungen mit Ergebnissen und gegebenenfalls konkrete verbleibende Abweichungen. Nicht ausgeführte Tests ausdrücklich als solche benennen.
## 16. Future Extension Contract
v0.1 ist ein Walking Skeleton des Zielsystems. Die folgenden Fähigkeiten sind ausdrücklich **keine v0.1-Deliverables**, die Architektur darf ihre spätere Ergänzung jedoch nicht durch produktspezifische Schemata, fest verdrahtete Scoring-Annahmen oder n8n-interne Fachlogik unnötig blockieren:
- mehrere externe Produkt- und Preisquellen,
- source-spezifische Search-/Discovery-Adapter,
- Variant-Level- und Market-Level-Preisreferenzserien,
- weitere Produktkategorien ohne produktspezifische DB-Spalten,
- weitere explizit versionierte Preference-Strategien wie `boolean_preferred`, `categorical_match`, `set_contains` oder `categorical_rank`,
- LLM-gestützte Normalisierung mit Raw-Wert, normalisiertem Wert, Confidence und Normalizer-Version,
- n8n für Scheduling, Orchestrierung und Benachrichtigungen,
- automatische Kandidatensuche und Kandidatenaufnahme,
- mehrere parallele Buying-Profile und Watchlists.
Dieser Vertrag verlangt **keine vorsorgliche Infrastruktur** für diese Funktionen. Insbesondere keine Plugin-Registry, keinen Event-Bus, keine Microservices, keine zusätzlichen Preisaggregationstabellen, keine LLM-Abstraktionsschicht und keine n8n-Workflows bauen, solange sie nicht für den aktuellen Slice erforderlich sind.
Die stabilen Erweiterungspunkte von v0.1 sind:
1. Source-Adapter liefern validierte DTOs in den bestehenden Importpfad.
2. Attribute und Constraints bleiben generisch und datengetrieben.
3. Preference-Strategien werden explizit benannt und durch versionierte Scoring-Modelle unterstützt.
4. PriceScore konsumiert eine von der Scoring-Version bereitgestellte Referenzserie statt dauerhaft an Offer-Identität gekoppelt zu sein.
5. Product-, Offer- und Overall-Eligibility bleiben getrennt, damit Monitoring später gute Produkte unabhängig von momentan schlechten Angeboten verfolgen kann.
6. n8n bleibt externer Orchestrator; die Buying Engine bleibt ohne n8n vollständig nutzbar und testbar.

38
migrations/env.py Normal file
View File

@ -0,0 +1,38 @@
from logging.config import fileConfig
from alembic import context
from sqlalchemy import engine_from_config, pool
from app.settings import get_settings
config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
config.set_main_option("sqlalchemy.url", get_settings().database_url)
target_metadata = None
def run_migrations_offline() -> None:
url = config.get_main_option("sqlalchemy.url")
context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

25
migrations/script.py.mako Normal file
View File

@ -0,0 +1,25 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
revision: str = ${repr(up_revision)}
down_revision: str | None = ${repr(down_revision)}
branch_labels: str | Sequence[str] | None = ${repr(branch_labels)}
depends_on: str | Sequence[str] | None = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,21 @@
"""Deploy skeleton — domain schema follows in a later revision.
Revision ID: 0001_deploy_skeleton
Revises:
Create Date: 2026-10-03
"""
from collections.abc import Sequence
revision: str = "0001_deploy_skeleton"
down_revision: str | None = None
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
pass
def downgrade() -> None:
pass

32
pyproject.toml Normal file
View File

@ -0,0 +1,32 @@
[project]
name = "buying-agent"
version = "0.1.0"
description = "Generic buying and deal evaluation agent"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"alembic>=1.16",
"fastapi>=0.118",
"psycopg[binary]>=3.2",
"pydantic>=2.11",
"pydantic-settings>=2.11",
"sqlalchemy>=2.0",
"uvicorn[standard]>=0.37",
]
[project.optional-dependencies]
test = [
"httpx>=0.28",
"pytest>=8.4",
]
[build-system]
requires = ["setuptools>=75"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
include = ["app*"]
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra"

7
scripts/startup.sh Normal file
View File

@ -0,0 +1,7 @@
#!/bin/sh
set -e
echo "=== BuyingAgent API startup ==="
alembic upgrade head
echo "=== starting uvicorn ==="
exec uvicorn app.main:app --host 0.0.0.0 --port 8000

View File

@ -0,0 +1,11 @@
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_health_live_does_not_touch_the_database() -> None:
response = client.get("/health/live")
assert response.status_code == 200
assert response.json() == {"status": "ok"}

875
uv.lock Normal file
View File

@ -0,0 +1,875 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "alembic"
version = "1.20.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mako" },
{ name = "sqlalchemy" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ed/aa/02910bdb8e2f1444f6654d5b296cd827d126f82209050ee7b1000f92ac4b/alembic-1.20.0.tar.gz", hash = "sha256:db505480647bc60386c5369402f4a57a506b7539c9e9ef5e270d45cbbe4939bf", size = 2093272, upload-time = "2026-09-11T19:09:11.126Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/27/78a89b55b0904d222183164e079b4ca56208e94eff1d35ad1f1ad5be9b06/alembic-1.20.0-py3-none-any.whl", hash = "sha256:77eb101048d95f982c0353e9233404889dcd7a6fc244c107836c0e2fc9cf7d9d", size = 268719, upload-time = "2026-09-11T19:09:12.88Z" },
]
[[package]]
name = "annotated-doc"
version = "0.0.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" },
]
[[package]]
name = "annotated-types"
version = "0.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" },
]
[[package]]
name = "anyio"
version = "4.15.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "typing-extensions", marker = "python_full_version < '3.15'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" },
]
[[package]]
name = "buying-agent"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "alembic" },
{ name = "fastapi" },
{ name = "psycopg", extra = ["binary"] },
{ name = "pydantic" },
{ name = "pydantic-settings" },
{ name = "sqlalchemy" },
{ name = "uvicorn", extra = ["standard"] },
]
[package.optional-dependencies]
test = [
{ name = "httpx" },
{ name = "pytest" },
]
[package.metadata]
requires-dist = [
{ name = "alembic", specifier = ">=1.16" },
{ name = "fastapi", specifier = ">=0.118" },
{ name = "httpx", marker = "extra == 'test'", specifier = ">=0.28" },
{ name = "psycopg", extras = ["binary"], specifier = ">=3.2" },
{ name = "pydantic", specifier = ">=2.11" },
{ name = "pydantic-settings", specifier = ">=2.11" },
{ name = "pytest", marker = "extra == 'test'", specifier = ">=8.4" },
{ name = "sqlalchemy", specifier = ">=2.0" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.37" },
]
provides-extras = ["test"]
[[package]]
name = "certifi"
version = "2026.7.22"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
]
[[package]]
name = "click"
version = "8.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" },
]
[[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 = "fastapi"
version = "0.142.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-doc" },
{ name = "opentelemetry-api" },
{ name = "pydantic" },
{ name = "starlette" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/56/4f/f7c30a73127e0a8bbffe788369b8359e530b01ae06e2757936fa35bc5e6d/fastapi-0.142.2.tar.gz", hash = "sha256:06366626f2e70576367714d9ab2fe8472e6c8456dba69b399f9f797ab5e92570", size = 466721, upload-time = "2026-09-30T09:51:11.277Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/b6/78aaf9141fb46742928c113f3cf6ef2259d538cb02b604b7656c1dc9883c/fastapi-0.142.2-py3-none-any.whl", hash = "sha256:bd5f4d81f1e93a88bcd77caf4dfe3c2dbffc3805407a0007e9a114c18b3a670b", size = 144409, upload-time = "2026-09-30T09:51:09.8Z" },
]
[[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.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" },
{ url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" },
{ url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" },
{ url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" },
{ url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" },
{ url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" },
{ url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" },
{ url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" },
{ url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" },
{ url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" },
{ url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" },
{ url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" },
{ url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" },
{ url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" },
{ url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" },
{ url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" },
{ url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" },
{ url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" },
{ url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" },
{ url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" },
{ url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" },
]
[[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.20"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" },
]
[[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 = "mako"
version = "1.4.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5a/09/e07c4b5579a79f4b16f8d4f29f6c54514ac787c4ad506b8c4f28a0e6b0bf/mako-1.4.3.tar.gz", hash = "sha256:cd6537fe88d5fec315c55c2f8529bc4ce7a9a352ad7db3eeaa6a66e2dd4ec37a", size = 412799, upload-time = "2026-09-22T20:54:31.509Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/a0/053d6af3e8f871e0073b4a36732d9e65be77a72e5434c31b94f6af78a6bb/mako-1.4.3-py3-none-any.whl", hash = "sha256:723296007c870bfd6b3f0c3230dba7198096e5269297ebf5e4eff9e7ffa39d4f", size = 80164, upload-time = "2026-09-22T20:54:33.128Z" },
]
[[package]]
name = "markupsafe"
version = "3.0.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/38/9b/e422a865e1d5d57d0e509b4e0bf1c1a70a7f6382c29a5aa428df994c8bc8/markupsafe-3.0.4.tar.gz", hash = "sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6", size = 153777, upload-time = "2026-10-02T23:07:22.29Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/18/4bc5ba32499e87bb2b0ef5b3a9bb9c00a131fa961ddf0be548cb550f548b/markupsafe-3.0.4-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:de8b364c423ef0a4bad9069657d617f9a5d2b2062457a89b1fa16ee199c399c1", size = 12786, upload-time = "2026-10-02T23:05:08.709Z" },
{ url = "https://files.pythonhosted.org/packages/4e/6f/17f0c099bf25f3e31e63cc19244d9f6af861a9a4ab778c203997903cfdd0/markupsafe-3.0.4-cp313-cp313-android_24_x86_64.whl", hash = "sha256:34bdde374c5932765d7dc685c4a1d191a3207852d67e8e0a9eb6ea85156181f1", size = 12510, upload-time = "2026-10-02T23:05:09.93Z" },
{ url = "https://files.pythonhosted.org/packages/11/af/1a141081b905036ee904ec4bd945e1f70b4e1b32d33c4e59e8cf1d58b247/markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:6bd9e1788e15bfcf6a9082de42e30387e7b85d211ab21e57a939bb8cfaaf8d96", size = 11583, upload-time = "2026-10-02T23:05:10.884Z" },
{ url = "https://files.pythonhosted.org/packages/e7/0a/a89385ae590232622a03e091805cff12f24fabe6c11e0e8bae096cece81c/markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5066b244f576f91afc8ee3ba029a89f99d39c79b1853fe9d39bea9f0afbec148", size = 11801, upload-time = "2026-10-02T23:05:11.913Z" },
{ url = "https://files.pythonhosted.org/packages/ed/85/ea548dc013962eb73653124bc595635fbf9e0fa41d1f181a967ccb784dfb/markupsafe-3.0.4-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7a83aa6e4805df46fed18e989d3d16f86ef60cb50bbc8d9ce3a6be89165fbf6e", size = 11443, upload-time = "2026-10-02T23:05:12.887Z" },
{ url = "https://files.pythonhosted.org/packages/cc/72/15f2e5ec9cf2eb00d5cdfe968d94e4156a7bd7303832c3f3b2c403a36839/markupsafe-3.0.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2d1b7d9308288661f56672b1b157d75fc536714d3638487bbea17b6318a78248", size = 11665, upload-time = "2026-10-02T23:05:13.829Z" },
{ url = "https://files.pythonhosted.org/packages/ca/e0/4030bea613677e333c8a2c901fd405055f657f9d06acba5b7357984b6ef7/markupsafe-3.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73e77980c7207854f00fc4e71fb1626868d5740ab4012623d55c7a99ad122a72", size = 12061, upload-time = "2026-10-02T23:05:14.807Z" },
{ url = "https://files.pythonhosted.org/packages/f3/a5/28b76a7449eb702966b88bef599e2360b411fbb3afeee8fe560939be06ec/markupsafe-3.0.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7018d4af1cd272e847aa5917983ab5e83e4f6579f9dbfecd4a79c0ca80b144c2", size = 24395, upload-time = "2026-10-02T23:05:15.909Z" },
{ url = "https://files.pythonhosted.org/packages/07/6c/21232811afc3a063b5e934b1ae2efda52f46154ec382f585149c020e61fe/markupsafe-3.0.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c90d5b3d4e944e065a301d741b3c1d784f6bd1f503aa68b4967e32b2ba313d85", size = 27183, upload-time = "2026-10-02T23:05:16.976Z" },
{ url = "https://files.pythonhosted.org/packages/14/38/6ccdfa5b59049cb36fb80cbc80aee9cf1fc9bb77d1335ad435f2070b08cf/markupsafe-3.0.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18a801868a884f216e784d7d14db2a4077143ce7610440aee2ce8f734e7cfcde", size = 24772, upload-time = "2026-10-02T23:05:18.209Z" },
{ url = "https://files.pythonhosted.org/packages/63/e0/cec6865dfe88cb48fedd4b20aed6af5158e41092adcbf3e028bcc6ec2108/markupsafe-3.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434139499bb20b502ed3baa1f169e618f924a97e7a777fea1a49446d80106cf6", size = 23017, upload-time = "2026-10-02T23:05:19.286Z" },
{ url = "https://files.pythonhosted.org/packages/ee/76/6ed4940bb7648a9aac457c14f870cfdd5105f139a0fb1f29cd61fafa47d1/markupsafe-3.0.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9e227f3dbe6bde7491cf0a9965d00b88c6b1a4a95d11480ddf88bb96d397c19f", size = 22036, upload-time = "2026-10-02T23:05:20.352Z" },
{ url = "https://files.pythonhosted.org/packages/a1/4f/ed476226d4fe46a09090a36025bf319296810028df55eb12f1253b540f3a/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b8cd1f918b26fd7b1832ece557cc18f2d8747309ff8b3f0ef9d4250c5ad67a39", size = 23786, upload-time = "2026-10-02T23:05:21.576Z" },
{ url = "https://files.pythonhosted.org/packages/9a/35/66ff30450e35ef5fba9ebc930c9411747e537fd9447b65e44f5007e2b84d/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a5fcffb37e602b0b3c1638a97746b9b96125caa9bcf6fa41d337a9261de231ee", size = 22672, upload-time = "2026-10-02T23:05:22.922Z" },
{ url = "https://files.pythonhosted.org/packages/32/0b/72f45ce4b4efcbca4b80cf1b06703eff0be8d37e82abb78f66c85a7ead1e/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5989cb26b2e1efc6a42216a9f6b5ee495ce5ace2e5b352a9af489976b32d1ee2", size = 24107, upload-time = "2026-10-02T23:05:24.175Z" },
{ url = "https://files.pythonhosted.org/packages/d2/03/71776e5fdcba04614b384cc102e8a4198208579d896fd1394cb7cb9aa900/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:add96447a86d205ab616665d53b2950ee81083757f56e6ea833c8b2917646b46", size = 21615, upload-time = "2026-10-02T23:05:25.215Z" },
{ url = "https://files.pythonhosted.org/packages/ab/5f/801ce02a02e7aee0f784b1ec7843026178f6adeb9c93ac67eb1992a9a84d/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2628d3a8cb648ecebb3c5d6b0a1052d400e4d8b7ac0fb786be8d285b50040d17", size = 23076, upload-time = "2026-10-02T23:05:26.423Z" },
{ url = "https://files.pythonhosted.org/packages/4a/85/c43776625428f3bb4a61e8633940400e3efe6409e3c6f5bff26de5e45618/markupsafe-3.0.4-cp313-cp313-win32.whl", hash = "sha256:672d207103e6b16ca098611b0f9efad6bc00afd47c03d6ef62186495ca677dc0", size = 14118, upload-time = "2026-10-02T23:05:27.716Z" },
{ url = "https://files.pythonhosted.org/packages/6f/36/163da64de88a13db79214ef75fa041be7fa13bdb42261cf5b7484de14bfb/markupsafe-3.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:1f1f9477e174582b0a1b583d60b66e1f2cf5d3fe12cee985e4aedf44766600e5", size = 14313, upload-time = "2026-10-02T23:05:28.749Z" },
{ url = "https://files.pythonhosted.org/packages/9f/a8/9b662783ffaa1149221432a923cee562f78b9cbbb8baa3df9b3753e63e1e/markupsafe-3.0.4-cp313-cp313-win_arm64.whl", hash = "sha256:06de8ef6331f6e822c28d577dc8bf43fe398800477c49498f38fc38b67ff33fc", size = 14189, upload-time = "2026-10-02T23:05:29.917Z" },
{ url = "https://files.pythonhosted.org/packages/5c/c3/a944f3b0df22bd129e96915b9f4e98d2eeca6516687d7618304a966c3c74/markupsafe-3.0.4-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:4ed644d75aa94a2baf7ec3a96eaa160ea58c742eb9d27c6506053c5c40fc84ed", size = 12787, upload-time = "2026-10-02T23:05:30.971Z" },
{ url = "https://files.pythonhosted.org/packages/d4/d6/a44863f69d88b6c7e27889108f70d47aed259edf89d5df3c5fca1eac87d6/markupsafe-3.0.4-cp314-cp314-android_24_x86_64.whl", hash = "sha256:6d2a9efe686f9de00d0d1ea32a4a5a86d558a2277501bd78d964214eab625e59", size = 12525, upload-time = "2026-10-02T23:05:32.263Z" },
{ url = "https://files.pythonhosted.org/packages/17/8f/168ba80e532dd6a93f96f8f706f1ad41d7990b6e1aeedc1cc0d211a33497/markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8781a792a070cf2bd1b86d3aa943894115faaba6e88122a7bf32d62072742453", size = 11589, upload-time = "2026-10-02T23:05:33.251Z" },
{ url = "https://files.pythonhosted.org/packages/32/b3/aa2c95a574d3af39403a469b295886eb9b6d448da568cbebb5a2cbfdc2e5/markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:971a3bbb75d97ae4e2e8f7d4834236f86f85f0c85e04ab2e191db1123b04f80b", size = 11801, upload-time = "2026-10-02T23:05:34.315Z" },
{ url = "https://files.pythonhosted.org/packages/60/d0/34b810107d83840e768bf485de795893ebbae35b26ab061b487adfa0a692/markupsafe-3.0.4-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:8909c2f1c6dd65e054ac4b573a91c8384d1492281e55d82d159d653f7a13adf6", size = 11447, upload-time = "2026-10-02T23:05:35.302Z" },
{ url = "https://files.pythonhosted.org/packages/6c/ab/2f8488f0f817a39fca068d2b17daf446bf5cdb3eae28c3720af534d873b4/markupsafe-3.0.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cf3468d5ec187ffffcaca8e61929a37448f215dafc1386a12c750a72fe53634", size = 11722, upload-time = "2026-10-02T23:05:36.363Z" },
{ url = "https://files.pythonhosted.org/packages/ad/40/e2d117b048d47282ade906fbfd92814cbee5647afc13fda88a3406039372/markupsafe-3.0.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:52704c5d36eb6dda8866493decd61111fff86244c9b1ad225ca01b9e91e5970f", size = 12061, upload-time = "2026-10-02T23:05:37.397Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a8/73a81135e85ba66217f5af7facb03bbb386807e1a729ab64532e4c802652/markupsafe-3.0.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1caa2fa5a6184fb233153b35f654e6687bd555476f6170f29d8ee9be1a8b0af9", size = 24434, upload-time = "2026-10-02T23:05:38.407Z" },
{ url = "https://files.pythonhosted.org/packages/ac/ca/fa9216dd01efee2dfdacafe7df32b4d0170fbac694b0c258a193d6e53999/markupsafe-3.0.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:387d8cd30e69b3f0a72877b9ae717033396404e19095b17fe89753a981fda44f", size = 27183, upload-time = "2026-10-02T23:05:39.581Z" },
{ url = "https://files.pythonhosted.org/packages/fa/4e/a469509e538d37af51103b17b073126973f2b1cbf197ff32c7ddf025cfe5/markupsafe-3.0.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:051417f74bcaaefa316276e0ff723f541616ca51043d070da00249d9bddd3e3c", size = 24813, upload-time = "2026-10-02T23:05:40.671Z" },
{ url = "https://files.pythonhosted.org/packages/8f/db/d7282caf7ab03af44d5d6fdbaa019b35c7d7f1c90588b839c07cba640d6a/markupsafe-3.0.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8e9f292fcda89b324f2f5c91d13f1424a153e40fc2756f38ee23b15835ff300", size = 23049, upload-time = "2026-10-02T23:05:41.864Z" },
{ url = "https://files.pythonhosted.org/packages/30/f3/b6a425206e6964efda6acee544d0eb01d1501784d0b8e2dcc74986f33b17/markupsafe-3.0.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:df1ae86ff54725a01fa1a0510b914ca53a161b7050be74f6204e24aded5971d0", size = 22088, upload-time = "2026-10-02T23:05:43.014Z" },
{ url = "https://files.pythonhosted.org/packages/ea/8a/84d3582fc1f0d5bd466cdf2eebf175e172158a6e70701aacec1de1b35430/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8965520ac587c94a4ac48b729be3d8b8de00af39699b17585dfb599babe77977", size = 23846, upload-time = "2026-10-02T23:05:44.098Z" },
{ url = "https://files.pythonhosted.org/packages/1c/65/db101cce51b7ba4864ac491a9859d297dd1adf0e55b103fee9db9c47c527/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:340cbb1957ba99929cbf19a75626d36ba1ae21d1730b287d1cf7f824a20c4fc7", size = 22674, upload-time = "2026-10-02T23:05:45.23Z" },
{ url = "https://files.pythonhosted.org/packages/e0/49/ddee9813d71db0c7a5c9d97c832125e6758a0c844777f1cf076569bb0e22/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3a93d9616ddecfb393727a0041a562cf0b15a244e20f2bd25efc7949be4c4f17", size = 24104, upload-time = "2026-10-02T23:05:46.398Z" },
{ url = "https://files.pythonhosted.org/packages/aa/0e/7d8518d726726870a2399d69fd30d0fa36c5e57a2132c336b58d7c491073/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d2e56fd3b00222722abfb3f5f0759ddbae4b90811b5ad4343c64030ad1bde70c", size = 21637, upload-time = "2026-10-02T23:05:47.48Z" },
{ url = "https://files.pythonhosted.org/packages/b4/b0/b505e8a361ba557dbf3b3aa7331ea39b00d2022a26e925ff8463b9714bb3/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0d9c47709875fdb321452056622e930c52afbc07a7d780762fbb8b4d91ce6fa4", size = 23081, upload-time = "2026-10-02T23:05:48.611Z" },
{ url = "https://files.pythonhosted.org/packages/1c/ea/9cc3cea873f980c75cbdb6f4277ce30ee955de38be0b3d02f14c108e0698/markupsafe-3.0.4-cp314-cp314-win32.whl", hash = "sha256:38fc55594dab834470b6733dead2ee9e3f657fb0608c769dcafa0ba5ab52f45c", size = 14272, upload-time = "2026-10-02T23:05:49.707Z" },
{ url = "https://files.pythonhosted.org/packages/80/f0/5792ff768a410f93ee3f84fc19345295ffc352d2c936b424cb37e514714c/markupsafe-3.0.4-cp314-cp314-win_amd64.whl", hash = "sha256:c1bc67752d5f21013cfe430df4062441714eab79f65a6a05e01505957e9c35fe", size = 14501, upload-time = "2026-10-02T23:05:50.788Z" },
{ url = "https://files.pythonhosted.org/packages/5f/cf/3d074a8edffcc6899355232ff2543ae8d929733239596423b7db79698bc9/markupsafe-3.0.4-cp314-cp314-win_arm64.whl", hash = "sha256:7e1636da3d8dfc220b6dd10264db5f2b165e4888c4518594898fbe381049af8a", size = 14364, upload-time = "2026-10-02T23:05:51.857Z" },
{ url = "https://files.pythonhosted.org/packages/d9/31/87ce42159aae2163cf3bbbd0c44bc87780510eecab1ea3859099aed95dcb/markupsafe-3.0.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:805c8b84534fa10891890f0e4be39f3a99e94615d93e8836bf9fa1fdca2feeb2", size = 11759, upload-time = "2026-10-02T23:05:52.951Z" },
{ url = "https://files.pythonhosted.org/packages/5f/53/b047207eeb7752e960aca3eb1df5fb7eefa7dd4c62ac49bb156456c8a702/markupsafe-3.0.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fa95848c929b6a75f6848d3c9793e59db365ee436776e57db835cdbfa79ba977", size = 12096, upload-time = "2026-10-02T23:05:54.066Z" },
{ url = "https://files.pythonhosted.org/packages/ee/51/4326c88a13c7b755657d44b4bb986f8c3d9843ecba7e22d98661d87f9a57/markupsafe-3.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e916035e3e9930cbdfdd10abf48861340221857f45509565898e012263f7b289", size = 25699, upload-time = "2026-10-02T23:05:55.15Z" },
{ url = "https://files.pythonhosted.org/packages/f2/bb/990581b7474bfcf2cf34bed6ba5ea23bd87adb9d671213d68e88620e7a6b/markupsafe-3.0.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4d12837e0203bbace818ff4a7461afdcd78bcd782351cea148139180d7bcffe", size = 27804, upload-time = "2026-10-02T23:05:56.29Z" },
{ url = "https://files.pythonhosted.org/packages/6b/89/89491878c28e8291f5aa2fffe2c2d57230d10ae366d55dd810b840513d78/markupsafe-3.0.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5086f9975abb1ab531ee6afca1761e4b59a19b446f3f6522ed776963228cfe5a", size = 25912, upload-time = "2026-10-02T23:05:57.416Z" },
{ url = "https://files.pythonhosted.org/packages/30/77/680998b54efdea06fc114565cd739b6d059f826a0279219b218dfa750d29/markupsafe-3.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4a635a0487774f841cb1fb62e907e7195cc95bc761e053184b8acc3ceb20733", size = 23623, upload-time = "2026-10-02T23:05:58.557Z" },
{ url = "https://files.pythonhosted.org/packages/ae/75/2709f5ac5de9467b40b10e2bb8f89cc63dfb74582e09aa734b1124a217de/markupsafe-3.0.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb96e6e088d6cf71c1ea977510948320234824cf226e32f6f6e044f7a9c82b34", size = 23301, upload-time = "2026-10-02T23:05:59.94Z" },
{ url = "https://files.pythonhosted.org/packages/a0/c8/39eadc6c5b14c9c7679bfb98f4d4c6a97863b5beb91839aca4d2d6e16e55/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8b5d563170ff8ba3181caa967c99a3c804d1dedb702c7cb93a6a7c32247da978", size = 24950, upload-time = "2026-10-02T23:06:01.289Z" },
{ url = "https://files.pythonhosted.org/packages/1a/5e/01037f8a43e8ccb0bffb4fbdc5212db05bf080fdd7286cd392332d58128a/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:396ec4e65cc889f69786b3b89478b471cee5a3bcf468b9d9bb03e1a30fb291fc", size = 23330, upload-time = "2026-10-02T23:06:02.441Z" },
{ url = "https://files.pythonhosted.org/packages/d4/f4/23e83ce0596bb0cbe670502d31df8f757bbd01a392aa486fa3b40d1ed399/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:15ba9e28640feef770374b116a6f019c21f52404aeabe516aa7f800587b98cfc", size = 25239, upload-time = "2026-10-02T23:06:03.579Z" },
{ url = "https://files.pythonhosted.org/packages/88/5b/3708897368073cc683d524750474f41a77d2986152c380dcc55b20fdf340/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d920abdfa61279ba1a2ef9484aab07bf03331f8c08a10120fa332353d06e6932", size = 22767, upload-time = "2026-10-02T23:06:04.699Z" },
{ url = "https://files.pythonhosted.org/packages/c6/61/ebda1307864b409e6b3115757a3d4a09cca46cfb6cc65191b5de226b424b/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a9f54054101545a9a9cccefddf54316aa6e4491611fcbef9e91b3b6bebec04f6", size = 23668, upload-time = "2026-10-02T23:06:05.9Z" },
{ url = "https://files.pythonhosted.org/packages/09/15/98075cceac3b5ba0dbb8e4762a847be967d2befc349a2cf2d0ac77f62c9d/markupsafe-3.0.4-cp314-cp314t-win32.whl", hash = "sha256:12a606a492de952afcb43b59a14aaaaad120e708d3663dd0fdf2d738d427a691", size = 14301, upload-time = "2026-10-02T23:06:07.109Z" },
{ url = "https://files.pythonhosted.org/packages/0b/a3/768b560fcc4156685cb563d922b217810cfa7bc135773367f62f1f9d2078/markupsafe-3.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a18f38cafc329bac5e3c2b96c765b4c96d3d103421ed22ab7988c1e3fce27464", size = 14526, upload-time = "2026-10-02T23:06:08.276Z" },
{ url = "https://files.pythonhosted.org/packages/93/63/da554b4c97a6b0ea3229ca7fe8cbfb620be81613d517f482e85958550537/markupsafe-3.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:eba154571c16e032112afac0dc2dfe9e63c2ceb7aedd07bb7eecf2ce26d4dd4c", size = 14421, upload-time = "2026-10-02T23:06:09.402Z" },
{ url = "https://files.pythonhosted.org/packages/a9/30/54d11c8ca027114898cab97421fb39e4ffd9ddf47cdbc44df2ec76722da9/markupsafe-3.0.4-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:737c9c3981998eba27f11786f84fddcbabc74068b72a4a1f454ea02094b57b65", size = 12791, upload-time = "2026-10-02T23:06:10.485Z" },
{ url = "https://files.pythonhosted.org/packages/10/6d/97c913e253a14bd3cd0e15a5c56d13203b823fa7ee32498342896a072dc4/markupsafe-3.0.4-cp315-cp315-android_24_x86_64.whl", hash = "sha256:489505b03f692c3f376394e49194fa7a7f9e8558d6e293a7056a0032b0c38163", size = 12527, upload-time = "2026-10-02T23:06:11.834Z" },
{ url = "https://files.pythonhosted.org/packages/26/f9/b86d032042a4d597d9e1997f0e5f63a3eedaf11258e0a05760b0a0a826ea/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:077293e425f28ec737dbcad442a71752e28f8ae27cde3d68acd1fb212091cd92", size = 11602, upload-time = "2026-10-02T23:06:13.122Z" },
{ url = "https://files.pythonhosted.org/packages/f2/dc/73c14c1eedf0ac5fa3292ba43435e6c49d2c2050f33cebde541f8f4807f1/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9348cbb300d224fe3b89793262cb093504d4ae927004468463f745188a193e4a", size = 11817, upload-time = "2026-10-02T23:06:14.227Z" },
{ url = "https://files.pythonhosted.org/packages/8f/69/2c2fcaa5fcee22d72c7819c0d536fd181c74a688e6143845419579cd2863/markupsafe-3.0.4-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:b807e598953730f82e4eae3bd30f6a122cf6b31c398c6b504c0e04c13c170429", size = 11446, upload-time = "2026-10-02T23:06:15.574Z" },
{ url = "https://files.pythonhosted.org/packages/88/54/9e5ec76c62e6e2834d5a93623018c943e8b3bb41d663e3fd4c03303b9b85/markupsafe-3.0.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:799c39bdf5e2f1292fedd3009f7b3c9e760f10b2420cb9638d56920840ff6db8", size = 11719, upload-time = "2026-10-02T23:06:16.701Z" },
{ url = "https://files.pythonhosted.org/packages/96/24/3ec292b44064c16229e064d770b2625bd8ea941aa61f44905a9fa44942c0/markupsafe-3.0.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ae9dcb8fbe244cb82f8a6458b455b927a03685e383d9bacf1ea5ce180b96dc97", size = 12075, upload-time = "2026-10-02T23:06:17.855Z" },
{ url = "https://files.pythonhosted.org/packages/aa/85/b64fdb1f304848518742136983c24e96d967bfb59a0ea160e92736901ab0/markupsafe-3.0.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bced6e2a6dba6a28f7dd3c6ce14df1b2dd495923f16ea484cad03decd463b2b", size = 24656, upload-time = "2026-10-02T23:06:18.963Z" },
{ url = "https://files.pythonhosted.org/packages/9c/18/23997d4c65b355da6390d61cd56e0ab3befd6ba8dda25cb40c602bd0fa6b/markupsafe-3.0.4-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3882fb412298575bae3b9c46868251f15cc69307359f87bb1b382e53d6e5a2c9", size = 27737, upload-time = "2026-10-02T23:06:20.117Z" },
{ url = "https://files.pythonhosted.org/packages/d4/36/35998dead3c6af88c38265a56e58100211f036234ab88eb2283fd4cbce44/markupsafe-3.0.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04e7902ba80ee4bac1d50a549606527a1dcf0476cd81403db41099d3b60ec653", size = 25003, upload-time = "2026-10-02T23:06:21.284Z" },
{ url = "https://files.pythonhosted.org/packages/82/96/ef49135ce260db4ca4a12b119ed468449cd248db6b1468e2112b546d7a2e/markupsafe-3.0.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369", size = 23111, upload-time = "2026-10-02T23:06:22.524Z" },
{ url = "https://files.pythonhosted.org/packages/50/7d/83126e338bd88c17a220668235368ad719fd4638e426739858cbb8508f77/markupsafe-3.0.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f68edfc67aabac33708941f26f22a7b8e9f81429bc0cf249fcf7d66b23af8d19", size = 22403, upload-time = "2026-10-02T23:06:23.785Z" },
{ url = "https://files.pythonhosted.org/packages/83/dd/daf7e420de23c8206c365204e7b85e1251d8e19d34196a56336f316e5ed2/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:e5c802729725bd07e2bc3ab7b76dc7e0bbfc53129d8f1eb1c002c24cf774717e", size = 24083, upload-time = "2026-10-02T23:06:25.037Z" },
{ url = "https://files.pythonhosted.org/packages/19/3c/11eecdc06bc44ad5570350085b572ebf049e8f9a38d1ece6d76640b739cd/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:55ffd6ce583d97dc71dc92e930324c8c0d25aea7e3ade6ae54ef77cedb096811", size = 22931, upload-time = "2026-10-02T23:06:26.328Z" },
{ url = "https://files.pythonhosted.org/packages/0d/9e/ac0fd77f2a726e56ecc3ca0235d095feace1358d1b822406c2a2ef26a4dc/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:2cb3dd71fc6be918ad4264346a8ed69485f9b7ed7bf35495d8e22807cd6b8bea", size = 24299, upload-time = "2026-10-02T23:06:27.742Z" },
{ url = "https://files.pythonhosted.org/packages/d7/09/c6bd842ad58ff5b3bc76eeed7e9a42a6f11adc5d090ec697b72c9672731e/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:94f5407f7bc64fa6463906b896f9904beeeb7dd8dc116ee8e9056c8714ff9916", size = 21948, upload-time = "2026-10-02T23:06:29.274Z" },
{ url = "https://files.pythonhosted.org/packages/a3/46/82f586711fed61e86faa1ee1bc317d68cd45a10c8bdbe3f7d1fdf9026ad8/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2dad610540cb2e6272855c178f08ae9a1c7ac258a7fb71660553a5f104b42741", size = 23150, upload-time = "2026-10-02T23:06:30.583Z" },
{ url = "https://files.pythonhosted.org/packages/19/2d/2dfdce99318abbfa26925195fbc17db188c46a1ec6457be121b6f9cfeb42/markupsafe-3.0.4-cp315-cp315-win32.whl", hash = "sha256:03470d1a8268e692ecf79ecd565593e59d44219377a7ead61f1f1b94c1f7ff6b", size = 14269, upload-time = "2026-10-02T23:06:31.949Z" },
{ url = "https://files.pythonhosted.org/packages/5b/ec/6000fd82e8791e58fcd0456ec20f098957e2b03d5ed02eb73241a577c0ba/markupsafe-3.0.4-cp315-cp315-win_amd64.whl", hash = "sha256:d882a373d8093c2941e01291b7ced96e9cbe4781da9a7751ca7e6c70385e5214", size = 14496, upload-time = "2026-10-02T23:06:33.258Z" },
{ url = "https://files.pythonhosted.org/packages/bc/66/e73bd5016421d5d6e2fb6de7dd609f9de020942ac8c626526bd8c6eeaf82/markupsafe-3.0.4-cp315-cp315-win_arm64.whl", hash = "sha256:353bd63081912ab8cfa6a0c7d185934cdf8426f04c618bba6bc4b394f2069b67", size = 14361, upload-time = "2026-10-02T23:06:34.539Z" },
{ url = "https://files.pythonhosted.org/packages/90/df/cb8c3dc98d313a951df2f8968f44e4cb5643df6d3cab749a530ce2f7d972/markupsafe-3.0.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c61750fadcd119d0825bcb7d7d675dd264dcc89cc05292aab5be68ebdbb374ad", size = 11760, upload-time = "2026-10-02T23:06:35.807Z" },
{ url = "https://files.pythonhosted.org/packages/d6/bb/4af9b3ca0753d654ac75f9531d5bd741bb77ca6e696f36807c475ffc099a/markupsafe-3.0.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1c0df495a977d10460a94941799c72d5b5ab03d3858d949b55b5a66c8f371c99", size = 12108, upload-time = "2026-10-02T23:06:37.089Z" },
{ url = "https://files.pythonhosted.org/packages/3f/d4/b56429313aee5fd59b079c3df5615299959e25e7113eb6d8caadbdd7d38a/markupsafe-3.0.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02fa4acbc6a3fc5c693c34d4dd8c1130b7fe99cc915181b0ddd6f72aeb296002", size = 25762, upload-time = "2026-10-02T23:06:38.419Z" },
{ url = "https://files.pythonhosted.org/packages/65/f5/34c181e891aa4f7d59c918584672e0c5eb7fffe76c1387d1246008bf4081/markupsafe-3.0.4-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:05295589e619b9bed252a86b532b8e27350abc372d18ba89b59375325e91ec1e", size = 28400, upload-time = "2026-10-02T23:06:39.819Z" },
{ url = "https://files.pythonhosted.org/packages/ce/b5/ad14694fd0ac9a5ce30bc6498f2999378f418583dd1679cca5a1b512957e/markupsafe-3.0.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:be6cb0c799abb0e2ba3e618e6d28ddddf7e485f6c2ce938dfa237daf3905072c", size = 25946, upload-time = "2026-10-02T23:06:41.381Z" },
{ url = "https://files.pythonhosted.org/packages/d6/a8/26b606445387d0ceb1eb1f21840094b84e4e3c3c3983d80d10b89823b490/markupsafe-3.0.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26e9867520db70d37f7fb421a7f0d8adb40171011fb84ce869afa1a83370dfa8", size = 23634, upload-time = "2026-10-02T23:06:42.748Z" },
{ url = "https://files.pythonhosted.org/packages/39/a2/b8814de672f1f0094d498bf646f2fec9d6356b503d28ef500b71c5095377/markupsafe-3.0.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f03460ff076f70ab595bb45a0205ccea1971443575b6920c52e755dec2b3fbfe", size = 23488, upload-time = "2026-10-02T23:06:44.176Z" },
{ url = "https://files.pythonhosted.org/packages/db/c7/287223376fb73335a3cc5d6eb22c6ab01358cf33945a9c39c06b9dac3f4b/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:436e3ffc6310d3c41878c601db29098102fe5d8a467c49da4a4125254e0980f2", size = 25037, upload-time = "2026-10-02T23:06:45.646Z" },
{ url = "https://files.pythonhosted.org/packages/f9/29/4df8355e313426d19e62ba33e0253c009ca12a0894ee77d67fa67255361c/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:4e2c4809c14559aa7ef426f27fb35afbb38104c349a903bf8f3600456764bb38", size = 23543, upload-time = "2026-10-02T23:06:47.264Z" },
{ url = "https://files.pythonhosted.org/packages/71/e5/8377731e8495668dcc768f645e717df18318c841edaf023a99395f6da9b4/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:da2af0d7aebfc2074080d72efa6ab8317c62481ef1f896f65d9999c1c01f4494", size = 25282, upload-time = "2026-10-02T23:06:48.795Z" },
{ url = "https://files.pythonhosted.org/packages/ed/5f/373456e37ceb1478d657d6fe769cbe0a39f0a8dfc1548eeb19c471eefdd9/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:aa2c838cc024642cc04c6854232f32b43e5e22833dd11119c1766c7873b8370d", size = 22993, upload-time = "2026-10-02T23:06:50.31Z" },
{ url = "https://files.pythonhosted.org/packages/d7/93/2cbd5628435afb6f541bbaced4bce0c2edac4b09a142e6e928b8b0da9858/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b91cc9d336957239ff200f30097e6fea2dc6d6fb3c81e853eaa09eac904fd894", size = 23712, upload-time = "2026-10-02T23:06:51.759Z" },
{ url = "https://files.pythonhosted.org/packages/81/99/157e10966b033b363aeda5263e82596ee232a0b1d082fdbf90aa417ff083/markupsafe-3.0.4-cp315-cp315t-win32.whl", hash = "sha256:e49fb0d1ce92cfa0cb198cc5b1b11cdf9d0638658e2a2db2687e39db7c87fc78", size = 14300, upload-time = "2026-10-02T23:06:53.241Z" },
{ url = "https://files.pythonhosted.org/packages/33/05/55884815414c9706a23deca150b72c25a62109e65b0b6ce232077802c719/markupsafe-3.0.4-cp315-cp315t-win_amd64.whl", hash = "sha256:4f6e0852a0283b1b1fd776eeb7b766a5f440b3e2bd31ab51af3b400585f3965c", size = 14522, upload-time = "2026-10-02T23:06:54.729Z" },
{ url = "https://files.pythonhosted.org/packages/92/f9/ecbde7149e95b8a0f18e16d5d747f7dc06049d5da2e4f77f6f5e4a1f46a8/markupsafe-3.0.4-cp315-cp315t-win_arm64.whl", hash = "sha256:39dbacefc411633db5b4378b066a9aca70a3d7e2922c9e578d825f844026eeba", size = 14417, upload-time = "2026-10-02T23:06:56.246Z" },
]
[[package]]
name = "opentelemetry-api"
version = "1.45.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1f/dc/e12c1fe1ed8a7b7149777127b1a0e12ce5bd5a81d97408bedc2128c260f5/opentelemetry_api-1.45.0.tar.gz", hash = "sha256:711ede81773c8025c2c03dac0450bc89f3d30aea6eabcc815c570d4e35a963f7", size = 72115, upload-time = "2026-09-25T12:32:28.399Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/44/b9/040d1a1c7836922828e6480cd2366bb8fe0ebf75b413d2bb51a9b0e7f78f/opentelemetry_api-1.45.0-py3-none-any.whl", hash = "sha256:80e068aba7cd56c8b58512d6a36f8d25cb1dfaa0c0a4cc1c938ccf9f362d9cb3", size = 60020, upload-time = "2026-09-25T12:32:03.192Z" },
]
[[package]]
name = "packaging"
version = "26.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
]
[[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 = "psycopg"
version = "3.3.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/76/26/3ea4ca5eaea1c0debcdf7ee7c1613fbe721dc27a03c461c0817ffd8a0601/psycopg-3.3.6.tar.gz", hash = "sha256:c081f2250df751a943036e42db6df4571c66cd0aabe8291a7a506512b12007d2", size = 168171, upload-time = "2026-09-18T13:22:55.152Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/de/748bd7609c71cae5d737f0ba9192f19329f70180ecda8fff3cac02c5abe3/psycopg-3.3.6-py3-none-any.whl", hash = "sha256:a1db9f7148b06a28606767efaca51fa6f9398c5c0a3810519be69d7000bdb631", size = 215490, upload-time = "2026-09-18T13:15:29.374Z" },
]
[package.optional-dependencies]
binary = [
{ name = "psycopg-binary", marker = "implementation_name != 'pypy'" },
]
[[package]]
name = "psycopg-binary"
version = "3.3.6"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/c3/c072584b69ad44a747b448cfc9766fecb8aae56e372a017e2ef668790057/psycopg_binary-3.3.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ad8f35e67cc16d1fad1fa8c88972dc9b3a3141ea67897399904edab96a301b6", size = 4712284, upload-time = "2026-09-18T13:19:13.451Z" },
{ url = "https://files.pythonhosted.org/packages/0a/b9/4283b785339e8e2318d03048994b093d650ea6289fabaa806b765dc0d449/psycopg_binary-3.3.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:373704aea331d3f3e3402c125a1543f5875e2986ebb54f97d1647942161f803f", size = 4772031, upload-time = "2026-09-18T13:19:18.524Z" },
{ url = "https://files.pythonhosted.org/packages/6f/72/7a1321d359246769fff1affffbd0132785a28f7f63c18524c15a502398f4/psycopg_binary-3.3.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b82491019b884d62318b5f30706c3d7e6d4e5a6cb7eabcb3edc0c1b0fdaceae9", size = 5556392, upload-time = "2026-09-18T13:19:24.418Z" },
{ url = "https://files.pythonhosted.org/packages/de/b0/c6f8a0585a5dacbea74e130bcfc66629390e8f5bbc79d2a8e806e8952150/psycopg_binary-3.3.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cec5ea900390897d0b46130f60bc2883bf19c314f9044235217c8be88b0ef269", size = 5237855, upload-time = "2026-09-18T13:19:31.257Z" },
{ url = "https://files.pythonhosted.org/packages/e2/fc/c3a7a8bbef7e945ec584ac61d460a612363ea398511cd0e220242b1d69f1/psycopg_binary-3.3.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98c02090d88f2ebc0ec1e8da538f77d225ce0fffecf372aa39262e62a1b054ef", size = 6833856, upload-time = "2026-09-18T13:19:43.622Z" },
{ url = "https://files.pythonhosted.org/packages/a9/f2/8e80b921db728ebb68fc105bd7c4277f908210ad755bd6481d5ea7add740/psycopg_binary-3.3.6-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ee2c4728c691245e24501fcd7a97b5b381236b9985bc445bba88cdce7d1b5784", size = 5070730, upload-time = "2026-09-18T13:19:49.968Z" },
{ url = "https://files.pythonhosted.org/packages/54/6a/5b313e0c5348244f0e973aff3258bf86766656256d5ece8d541a53e35b4a/psycopg_binary-3.3.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f19cc87343eaa55255e76b31259a570072ac95d6ae82c92dd34b97691f5e49dc", size = 4598089, upload-time = "2026-09-18T13:19:56.426Z" },
{ url = "https://files.pythonhosted.org/packages/32/e9/db7f76ec24bf6699e92bf604e5c4bae10664a681a8999ef42aa0faf0f2c6/psycopg_binary-3.3.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdccb3a0e184b03e9baa673b15a809cf36c339c85dbda0ebc25a698846dfbee8", size = 4278481, upload-time = "2026-09-18T13:20:04.681Z" },
{ url = "https://files.pythonhosted.org/packages/61/83/72c67013656f4d6b547caabffb193e91d57e63f90eefdcc6d045c400e97d/psycopg_binary-3.3.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9892188bb15e5803beb51afe8a25add6b56be391a53058e8bca03b74e1e6bf22", size = 4009229, upload-time = "2026-09-18T13:20:11.905Z" },
{ url = "https://files.pythonhosted.org/packages/82/35/5e4500df2c999eb0faed8b184e6958b834172128274f06167a5deef4c19c/psycopg_binary-3.3.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3af90f92769d8cc10f94515ee7a0aef36ea85ca733a0ce22858f6e0953f41138", size = 4321467, upload-time = "2026-09-18T13:20:17.949Z" },
{ url = "https://files.pythonhosted.org/packages/55/7f/e350e1cf498ba2565c3f87b12f429d2012eb86b76c2b3845a19ee5fbb4d6/psycopg_binary-3.3.6-cp313-cp313-win_amd64.whl", hash = "sha256:0ebfad5d131de9f892ae9e70cc7616207768b6714b66a52d4612b8ceaf78b372", size = 3658179, upload-time = "2026-09-18T13:20:22.691Z" },
{ url = "https://files.pythonhosted.org/packages/6d/b9/60711317c284a442511644ea7185b56ebe627606d6741e732cd16108c47b/psycopg_binary-3.3.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b3f75dee0f9afafabe4edc52c4842f1e1878ed2069bd05b22d6fe961e97e4dba", size = 4720512, upload-time = "2026-09-18T13:20:29.278Z" },
{ url = "https://files.pythonhosted.org/packages/63/da/28befc84454cbc6374550de7746f591f8fe1b6165c1fce249652cc8291c4/psycopg_binary-3.3.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5927b7ba63153cd8e9862987290a2b783a5c590daf2a4ef981700cc3569166d4", size = 4782318, upload-time = "2026-09-18T13:20:35.401Z" },
{ url = "https://files.pythonhosted.org/packages/a4/8a/0d21c2c833cdc0d4244c77e858e0ed37fa2abec2623be4fd686f617109ce/psycopg_binary-3.3.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0bf08b749cc144f33b44a91b78e3f71c60eb07963746a0df5a100b36ce3d7475", size = 5567460, upload-time = "2026-09-18T13:20:41.902Z" },
{ url = "https://files.pythonhosted.org/packages/49/6d/7692d0d4e656b6cc9868d8acc2e3b42f17a0db4a625400a6d093cb0533a1/psycopg_binary-3.3.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:31cd942c23f613276b81a6e6598cefa12960058b0f46e1e874b540c793f6aca5", size = 5246902, upload-time = "2026-09-18T13:20:47.661Z" },
{ url = "https://files.pythonhosted.org/packages/d4/c1/b8a1f18fb1b7558a17f57f7cb3fc8bc93189feea2958925950b3acb15743/psycopg_binary-3.3.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4690cf67738f0e0e49a32aeec99bf0e4595cc2b4f1af984a4345394b1dcff91a", size = 6847192, upload-time = "2026-09-18T13:20:56.874Z" },
{ url = "https://files.pythonhosted.org/packages/a5/76/404f33519167c65cca88ec4998776f1dbebccc301ee977f0e62c47fb0826/psycopg_binary-3.3.6-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ad1c785e784cfd87e8436c6b7702f2d321fc39601bbaf29bc63a41a867091638", size = 5079573, upload-time = "2026-09-18T13:21:04.155Z" },
{ url = "https://files.pythonhosted.org/packages/f0/d9/79e8fbc8f37262a415f3550f0bcc5f98037442bf3d12ef6cbae2056655ae/psycopg_binary-3.3.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:79a2a1c3449f6c3409427078ed1cec10de79f3023cb5f2504f0597d350ad46c7", size = 4613633, upload-time = "2026-09-18T13:21:10.664Z" },
{ url = "https://files.pythonhosted.org/packages/d4/47/96225db74be7d2ce04b3a58678b53cda610225055edf5faa775c9f501d8b/psycopg_binary-3.3.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:86147cb5d140341c3363fb5bacce31f8d5543902a46699d3c536b101bbceaf9e", size = 4293375, upload-time = "2026-09-18T13:21:16.027Z" },
{ url = "https://files.pythonhosted.org/packages/2a/d2/18e9c779a5efd565250329adaf529ecc2b8b2ed5be5cb0f6ccee208cbfd9/psycopg_binary-3.3.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7308c93cf0b19bbaf8e6ff0a6ad50d3c442385739245fe15a8d593bf841734a6", size = 4019883, upload-time = "2026-09-18T13:21:21.587Z" },
{ url = "https://files.pythonhosted.org/packages/ef/28/0cc654afc6c2cda982767f5679d3646b30b1ec86545bdaa9402202d6776c/psycopg_binary-3.3.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:05a83ac9fd52b9bca7cb5ab04b3691163170bd16f53defa27216ea3aa07ee781", size = 4332607, upload-time = "2026-09-18T13:21:27.63Z" },
{ url = "https://files.pythonhosted.org/packages/f1/3e/0a753a74fbd7aef120f286c016e09d3cc3f1daf7688f4a145d27281260b2/psycopg_binary-3.3.6-cp314-cp314-win_amd64.whl", hash = "sha256:1fbd30e537dab22cafdf080608f10148fe2a5f3a61294ddb5113caac8a623840", size = 3755671, upload-time = "2026-09-18T13:21:33.855Z" },
{ url = "https://files.pythonhosted.org/packages/0e/b1/a372b9c02aea50148e71c9853e19efca8fa5ae2010a8e27243b9b8f790c0/psycopg_binary-3.3.6-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:bf8c8481d026b85dd70c5fa7dde85b2333aed0b32a2602bcd38a900cbd78a49c", size = 4719571, upload-time = "2026-09-18T13:21:41.437Z" },
{ url = "https://files.pythonhosted.org/packages/65/7c/811e3828c6b82e2f10c6c9cdd963cfc66f3e024026e5a69ac18530bad984/psycopg_binary-3.3.6-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:b599defe9190b17e9907c8b4d114c181e702c87efcd1b8a0ad40971cdcc4634a", size = 4781230, upload-time = "2026-09-18T13:21:49.516Z" },
{ url = "https://files.pythonhosted.org/packages/3e/15/9a784eed813ea9e97c294af3ead63d02b7b203502c66380336c50065e441/psycopg_binary-3.3.6-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b8ece331509f7a975b90501f41e83ad905e4141753fedf3f2711b2bc70a8efbc", size = 5566111, upload-time = "2026-09-18T13:21:58.089Z" },
{ url = "https://files.pythonhosted.org/packages/68/16/47194e002007c27337b11e49bf459c4b19727463f9aff2e1a90917bcc806/psycopg_binary-3.3.6-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c61617eaae0112ca154da87ffb99b73af2c74067acac28dfb9a4455b019dff2e", size = 5249963, upload-time = "2026-09-18T13:22:06.695Z" },
{ url = "https://files.pythonhosted.org/packages/53/84/5dcf9f310b11f0675cd860c6b2c70f58ce61798a3ee3f6f962b53fa358ca/psycopg_binary-3.3.6-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6d19cb4999d03231e8730a5f66c8f5068bc3b532677eb39dab0f600bff3e312", size = 6847925, upload-time = "2026-09-18T13:22:13.088Z" },
{ url = "https://files.pythonhosted.org/packages/f3/06/1957a06dc22963c418c27b284929579de84f29c37ad1abe6dc6ee9e8cf25/psycopg_binary-3.3.6-cp315-cp315-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8cbb54454dbf1bbf2ff08dd7693e8d94ac94b1a20f70f4b3b813d52ecb5cbc1", size = 5087720, upload-time = "2026-09-18T13:22:17.959Z" },
{ url = "https://files.pythonhosted.org/packages/21/43/ac07d042bae99b57bf123bb473632f29af544008094da0ffd285ab8011e2/psycopg_binary-3.3.6-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:dc75da5a20951049f7b773145f998f69d181adad9c58a0ff36e0cf1d73c10e10", size = 4613412, upload-time = "2026-09-18T13:22:26.719Z" },
{ url = "https://files.pythonhosted.org/packages/aa/b1/019156fbeafcefb4cccc9d109de4699493bceb8313c7545c8349e089dfbc/psycopg_binary-3.3.6-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:955e3dd94da361e052d2e49acf591017158dc8f8ed2c8a42c2e3943403c39dc2", size = 4292618, upload-time = "2026-09-18T13:22:33.042Z" },
{ url = "https://files.pythonhosted.org/packages/5d/0f/62113dc6b1df65983a1f2fc816c04b1edfa22f2ae9d4abee74ed267f4a96/psycopg_binary-3.3.6-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:c7753871eb57e6a5f4646f6168590c6653073dea5e9e720b201c8875332df4c8", size = 4027121, upload-time = "2026-09-18T13:22:38.334Z" },
{ url = "https://files.pythonhosted.org/packages/5d/d5/cf0cbd1ea5a7d8167fe2c6953efde19101f7b193bd61a23e6d622ad6854c/psycopg_binary-3.3.6-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:303732e798fe6729f8e12021b9c96107df8e95ecec4dd487c67b98ec2a59435e", size = 4336388, upload-time = "2026-09-18T13:22:45.576Z" },
{ url = "https://files.pythonhosted.org/packages/98/33/e2a5b36edf8aa422f6fa4b894756eb33dc93b36df5f65121280bb8b929c4/psycopg_binary-3.3.6-cp315-cp315-win_amd64.whl", hash = "sha256:2f122603f36050937982abf9668d8bc4769a79f7c93a65013b1c49f1cab7b56b", size = 3756154, upload-time = "2026-09-18T13:22:51.283Z" },
]
[[package]]
name = "pydantic"
version = "2.13.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/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" },
]
[[package]]
name = "pydantic-core"
version = "2.46.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" },
{ url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" },
{ url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" },
{ url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" },
{ url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" },
{ url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" },
{ url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" },
{ url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" },
{ url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" },
{ url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" },
{ url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" },
{ url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" },
{ url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" },
{ url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" },
{ url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" },
{ url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" },
{ url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" },
{ url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" },
{ url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" },
{ url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" },
{ url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" },
{ url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" },
{ url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" },
{ url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" },
{ url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" },
{ url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" },
{ url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" },
{ url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" },
{ url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" },
{ url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" },
{ url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" },
{ url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" },
{ url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" },
{ url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" },
{ url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" },
{ url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" },
{ url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" },
{ url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" },
{ url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" },
{ url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" },
{ url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" },
{ url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" },
{ url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" },
{ url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" },
{ url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" },
]
[[package]]
name = "pydantic-settings"
version = "2.15.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dotenv" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" },
]
[[package]]
name = "pygments"
version = "2.21.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
]
[[package]]
name = "pytest"
version = "9.1.1"
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/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
]
[[package]]
name = "python-dotenv"
version = "1.2.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/74/26/2fbeedb218a787a5eea551c7532cac4e009f83d689dd2faa0d0353473f86/python_dotenv-1.2.4.tar.gz", hash = "sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0", size = 60824, upload-time = "2026-10-01T05:36:10Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc", size = 23266, upload-time = "2026-10-01T05:36:08.633Z" },
]
[[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 = "sqlalchemy"
version = "2.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/02/4b/81d972a46c9f1d978af1795e2abc988855c711453552cb45d75f6e4abbf5/sqlalchemy-2.1.3.tar.gz", hash = "sha256:ade5281df06038c6394f532590d592d3381ee5d11b9e0006a2570aef41145118", size = 10525778, upload-time = "2026-10-02T22:57:00.308Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1a/71/68c29931fb2c5178241f7d0c63dbdd800ad65685eac70d7f2117145b5e3c/sqlalchemy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:260bdd8baa1dc631d76d14b26ad009ebbb665b4469666afef8dcaa1f964016cb", size = 2457479, upload-time = "2026-10-03T02:39:19.741Z" },
{ url = "https://files.pythonhosted.org/packages/94/21/5dbd91816b2cdb367bca3195f300ffeca916762e088d6924fc33a18f50ad/sqlalchemy-2.1.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fc01a8047d6a4f4e997441994f07af79fdd470fc80669751e32f91153d6b491", size = 4582199, upload-time = "2026-10-02T23:37:44.508Z" },
{ url = "https://files.pythonhosted.org/packages/32/d1/79962ce06d6cd34516def113f5170184fa9401ebddc59fd6cb18f6e919f3/sqlalchemy-2.1.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b1b3ff10eba0f9290142375e667351c99d24ee7fcff6e0f85bd8be9d39b50a17", size = 4632923, upload-time = "2026-10-02T23:35:55.318Z" },
{ url = "https://files.pythonhosted.org/packages/3c/94/03e98031dc9a1738c580c3bf01beeafe695f247eb647f4e8dda3fab2454f/sqlalchemy-2.1.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:efb1ebf695e80d9a69f5bbff9d812d2491fd168b27d6b42ad4c8a4d3d6faaef1", size = 4300224, upload-time = "2026-10-02T23:46:45.495Z" },
{ url = "https://files.pythonhosted.org/packages/3a/1f/388c87607eb6c7abee4866355eeebb6221b3bcb413a64ce4a5cfc3edd22f/sqlalchemy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a95754928d47b37ea5c4b5180101db2afabef1da0dd5cd0c573b547d5d036ff0", size = 4507170, upload-time = "2026-10-02T23:38:29.697Z" },
{ url = "https://files.pythonhosted.org/packages/57/73/5515a3f555d31748564fbeb0b9e0b6ab7a647bf2ea0cdb6d233f20f710e3/sqlalchemy-2.1.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7606b413730ac6002930faa96f52195ed6969040b78071a3336370661f5e9e09", size = 4301439, upload-time = "2026-10-02T23:46:47.845Z" },
{ url = "https://files.pythonhosted.org/packages/a3/a0/0eae9b96f6f8a09f3e5d388f6fc5d6fe8d5ff2aced2a5c75bea7333d445e/sqlalchemy-2.1.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:76c3d5c7ada0925ff12e8c2395c547f88fcfccc13368ff8e8dd55ab5e28dcdab", size = 4594441, upload-time = "2026-10-03T02:46:16.928Z" },
{ url = "https://files.pythonhosted.org/packages/f5/b1/b6d681015414ce9048c07ef3ac49b693c1b8373de4716ee4bdeff4ec19a0/sqlalchemy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:cb6fc5c856e8f1c0228bf74b636c0a03a6c12dd502ced7aa82ced21dcdc7f547", size = 2371547, upload-time = "2026-10-03T02:49:23.549Z" },
{ url = "https://files.pythonhosted.org/packages/56/ad/35aa4d737016640c6da7573a4bcd9d91b59a87359bc6df075627963a4781/sqlalchemy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:82d4c63d338737d593f1d29c4dae0a124577a9a76186d90b5c0f9ec9cbeeab35", size = 2421558, upload-time = "2026-10-03T02:49:25.448Z" },
{ url = "https://files.pythonhosted.org/packages/fc/42/0dcc7cd92a3f4773376a5488b6ce0d6d75d2d029ad7e7f9e0ecc5fb48fcd/sqlalchemy-2.1.3-cp313-cp313-win_arm64.whl", hash = "sha256:0ab6f42761cc5c48d2f3e8a70810d10630d8222a51a7111cf9b3a48f22c3f148", size = 2382507, upload-time = "2026-10-03T02:42:42.812Z" },
{ url = "https://files.pythonhosted.org/packages/11/55/d626c933b7d034619c57120fafb47c0d44b2553ad2b8f74e904a40c01258/sqlalchemy-2.1.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:660c6e16d254568874d4b77fd593aa5426f55cf40d0acc8b5364a5e61cc1044d", size = 2460626, upload-time = "2026-10-03T02:39:21.751Z" },
{ url = "https://files.pythonhosted.org/packages/35/90/9b276c8eed59694d5c2291588fae9c51f25a5c63efb00ef9a7c6e754a2cd/sqlalchemy-2.1.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7277ba31ffb636666ed207d2aaf74de3cd2ee78436da7552bd4eeadf1f67740a", size = 4576390, upload-time = "2026-10-02T23:38:31.943Z" },
{ url = "https://files.pythonhosted.org/packages/60/21/818e89fe73ba4600abf5090e0d2356d92de9ca3a8aa9222d64801e510bd2/sqlalchemy-2.1.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b728c406b1b202e8998e8f9adacb56773bfeea67da74252ad3025bcb8f5863f1", size = 4606177, upload-time = "2026-10-03T02:46:19.291Z" },
{ url = "https://files.pythonhosted.org/packages/88/75/ab96bb0e27153d123340d6f1b14fac3713c03b7e3ca3f74f4c0715345775/sqlalchemy-2.1.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:85623c57c3696dcbc4fc909bbf335a2f0c27ae36f762d83810067fb49abc040e", size = 4300438, upload-time = "2026-10-02T23:46:50.03Z" },
{ url = "https://files.pythonhosted.org/packages/94/49/c7e0223fe5553ff1c31853c07f158f75245f8d435435755a789249f5121a/sqlalchemy-2.1.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0de6dc326cc99ac31206359e9fc2c13345f667edeb6ef556585fedac4ff698", size = 4506089, upload-time = "2026-10-02T23:38:34.209Z" },
{ url = "https://files.pythonhosted.org/packages/10/83/d6aad5e1a5b3a3e9257f932971b7f2105be6669205d4d9946f5f87d73216/sqlalchemy-2.1.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:65e002b794efa1bacbbde9fa973e5340d6c3c29b53e592b863a281802829319d", size = 4301730, upload-time = "2026-10-02T23:46:52.115Z" },
{ url = "https://files.pythonhosted.org/packages/04/54/310b9fd03ba7de6359185a0914bcd0cf45a76c6314e49d1ec814a50ae13e/sqlalchemy-2.1.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65e4813c78eaa679c9369b289e67fdba924db6be14067700c5ccbda37953af9a", size = 4565315, upload-time = "2026-10-03T02:46:21.64Z" },
{ url = "https://files.pythonhosted.org/packages/d4/8b/f6f7d46bc4e7bdc16fa78ec9a390f01852f2b080b7c75e83db4947b707c6/sqlalchemy-2.1.3-cp314-cp314-win32.whl", hash = "sha256:726987e7f8500614016be056ba5c9715b760ab465295741e2087b29753d72632", size = 2378402, upload-time = "2026-10-03T02:49:27.524Z" },
{ url = "https://files.pythonhosted.org/packages/9a/5f/dabd1ea48dd6c45383526c7f2e453fea15dcb53235edbadab49857fd7bfd/sqlalchemy-2.1.3-cp314-cp314-win_amd64.whl", hash = "sha256:0560458a775917bcd7a47eccf4f9e0b135fd9b72d7b819cb1bd60581de946f9a", size = 2428828, upload-time = "2026-10-03T02:49:29.481Z" },
{ url = "https://files.pythonhosted.org/packages/23/aa/ecba6a6141d24dbd4b002b451e0c410b7c6cb82a8a873f915f89314af830/sqlalchemy-2.1.3-cp314-cp314-win_arm64.whl", hash = "sha256:63c9289ed81991ba4d500611ce201531711430f5d23d7ae9a61a8842639b85ea", size = 2393599, upload-time = "2026-10-03T02:42:44.416Z" },
{ url = "https://files.pythonhosted.org/packages/33/41/f2c5eb9371cc1352a16001642702dfc2d73537995eea5cdeec7738f90a9d/sqlalchemy-2.1.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:08b2f345db8b0742e7814c49f9a210d5f5c8fb9d80d12c8a63bba762fa277477", size = 2500084, upload-time = "2026-10-02T23:35:35.774Z" },
{ url = "https://files.pythonhosted.org/packages/73/f1/fabd748dd8a1700579906649ac05414d65eca5bca44a3ac3c22856ed8d25/sqlalchemy-2.1.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:493b632f426579c4b0e97a5610f76c8c3d401cede442d462bf700b537035c91e", size = 4904105, upload-time = "2026-10-03T02:49:06.101Z" },
{ url = "https://files.pythonhosted.org/packages/ad/6b/a7a3d5c77c16386e16eb9dc8ba82e0cc6b03d6ee0ef6620921655df82413/sqlalchemy-2.1.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55e06933580f4faa5a7b1d2c9ad04f365fe01c40465eb982e38fda8758c1e95c", size = 4814161, upload-time = "2026-10-02T23:34:39.253Z" },
{ url = "https://files.pythonhosted.org/packages/63/72/275dac552b869a21774c08a7084bad16bd99f28384b8c95ffcded1a18884/sqlalchemy-2.1.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fdcee9536cb3281b8810787ba651f4e5f22bd687b177de89fba2155b055b07b0", size = 4494471, upload-time = "2026-10-02T23:59:11.642Z" },
{ url = "https://files.pythonhosted.org/packages/e8/3e/41d8071c58a20e2ba9fabc422938a219461f0b32aa537ab69afe06daa0e5/sqlalchemy-2.1.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d2cb28779dbf54067655030d7ab3a6726c7957c56bde5ff6745ed86b5cebff41", size = 4762070, upload-time = "2026-10-03T02:49:08.052Z" },
{ url = "https://files.pythonhosted.org/packages/75/73/b747233b8ad935467eb1f2aa1c466af69831693cab3495c88842bf1bd3ac/sqlalchemy-2.1.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:01a90cac3f8d93fcbda0df119ef56c4d58aad5601f6b7ee5a38bfabbd5d119b8", size = 4494691, upload-time = "2026-10-02T23:59:13.733Z" },
{ url = "https://files.pythonhosted.org/packages/71/aa/ba11b725b9529879aff489e36cdaccc3fd69620fb30b89186792523bc262/sqlalchemy-2.1.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:edbc28049ab8da07387ea2b85ceee747b36b25444ffa4aaba9f6e1461d5b2634", size = 4752692, upload-time = "2026-10-03T02:54:01.04Z" },
{ url = "https://files.pythonhosted.org/packages/b6/b8/86ec625db10e285ed8459e39ab3e56b7630554bc733e64a190e248eeea25/sqlalchemy-2.1.3-cp314-cp314t-win32.whl", hash = "sha256:724f587ced076d89291032b35476d6bdb401cce1dd680c1e3418c01d9bd55ad4", size = 2436746, upload-time = "2026-10-03T02:56:47.297Z" },
{ url = "https://files.pythonhosted.org/packages/18/be/1309b0b3acfcd6e0060c815762012c66d23eece8c9eb8091da5c850a1289/sqlalchemy-2.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:935085a5b8849d324c22bc3cc09fd07b3669cc22ba5bdd3ec208829e535cdef9", size = 2504160, upload-time = "2026-10-03T02:56:49.705Z" },
{ url = "https://files.pythonhosted.org/packages/9b/b2/ac9067f4d6c43887570df54e6c340ae9c591a8e75a5fd3029a9d7deba163/sqlalchemy-2.1.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2ebfff82cece947fea1ed6337d4f0613b9799f6db621c9729790fdd41edfb5ea", size = 2422266, upload-time = "2026-10-02T23:35:30.802Z" },
{ url = "https://files.pythonhosted.org/packages/a0/e5/54a863bf8e12fa97cd06bb2e012aeca53b5535bc07aabe0192a4ed77ed44/sqlalchemy-2.1.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:47a052dcd935a62326216bc5f0ed10ad941c77c3acc9200385e0c03c4f31d5a9", size = 2460525, upload-time = "2026-10-03T02:38:02.872Z" },
{ url = "https://files.pythonhosted.org/packages/f9/63/f6f8a8d8e8f8be456bea70ad2b9af90a60a7f98d349b178b9eaaea86431b/sqlalchemy-2.1.3-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e5916b43a9a8f346026f663583f203d0c43febad504ff2fed504dec6e078f20", size = 4579659, upload-time = "2026-10-02T23:35:35.789Z" },
{ url = "https://files.pythonhosted.org/packages/fa/7f/9f1064ef0f8f15e3c1cb8fa7ad5e4fc7e1e311ad0e1e46bc2e77940585bb/sqlalchemy-2.1.3-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af78a85af63114e2650f019ddf8fffb871e659e92bffb7d0bce5753f706caf", size = 4615129, upload-time = "2026-10-02T23:38:44.119Z" },
{ url = "https://files.pythonhosted.org/packages/c4/4a/f1a002235cc785d219738e096c7c65f68e9b7ed30c810997236567c50af9/sqlalchemy-2.1.3-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d218698c4f05728a941d6ad7c769b6491f03058e102cf811b7e4900f78fffa07", size = 4316528, upload-time = "2026-10-02T23:39:13.275Z" },
{ url = "https://files.pythonhosted.org/packages/a8/9c/6ee4e5ab55558408c16fe1713db415e45024f42cdb1c573ebceacc09d01c/sqlalchemy-2.1.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:80e6ccf111bc3f57530935f5541c6c1da50ac9713c418378840683532a45dd2d", size = 4509727, upload-time = "2026-10-03T02:41:25.314Z" },
{ url = "https://files.pythonhosted.org/packages/64/8c/a6bdf2874518c628fdedf24144194afa88cceb70339e5050459cdc2723cf/sqlalchemy-2.1.3-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:9a3ce1ff88dfb9f9f4305da07f4fab87720b609e264d134e47ae84a52df130bd", size = 4316490, upload-time = "2026-10-02T23:39:15.342Z" },
{ url = "https://files.pythonhosted.org/packages/10/3b/d859b638495a63ef7c155a8185369fa86ba2b860154d777e46eba922324f/sqlalchemy-2.1.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:77a365f7cb81bbd738c948b64e0b683f8f1b68efe51ca2815e8ad4baf6287ed9", size = 4576491, upload-time = "2026-10-02T23:38:45.97Z" },
{ url = "https://files.pythonhosted.org/packages/23/0b/c560177600613df0cfe27e27c346bac956b631cd5236178b81144e783e9e/sqlalchemy-2.1.3-cp315-cp315-win32.whl", hash = "sha256:83757320391b96cde715af0772675941d3e345da9ced3696c030f7f55ffa8eab", size = 2378116, upload-time = "2026-10-02T23:14:57.441Z" },
{ url = "https://files.pythonhosted.org/packages/d3/72/d39f60d0bc873653c0fec76aa088c969a2394ea7737f18d812fbe0e8c0c5/sqlalchemy-2.1.3-cp315-cp315-win_amd64.whl", hash = "sha256:f87533295b84c9e19ca2a72dc26a76e0192f0a2424f8fd575a82e75e7d58f9e1", size = 2428865, upload-time = "2026-10-02T23:14:58.959Z" },
{ url = "https://files.pythonhosted.org/packages/c5/03/953cf93136de44ee9d6cebee14666dcc2c4a4285fe159b2e459326631e49/sqlalchemy-2.1.3-cp315-cp315-win_arm64.whl", hash = "sha256:3e46a27b0c74af74aa6c08c983ca51db88d22dd86c316d72a634e989b7caef58", size = 2393289, upload-time = "2026-10-02T23:08:31.958Z" },
{ url = "https://files.pythonhosted.org/packages/a2/1e/5b5af9c0fe4e6f713d2ab684b88f10fd1cbe55bd34cca4d676fb930c9da3/sqlalchemy-2.1.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:06ef75e3e0c72d774b41f659bfc4f63d5e207d6c8418abe2b80843a4a25e2fa6", size = 2496326, upload-time = "2026-10-03T02:48:37.363Z" },
{ url = "https://files.pythonhosted.org/packages/2b/6c/6192d30fe24edc7c9e98c315341b7342ba7168d1729867c5d87e56285658/sqlalchemy-2.1.3-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70e05ae07dd9be29d4723bd0c880df622387eb51efb46a7b8efd49295248ffd7", size = 4880284, upload-time = "2026-10-03T02:49:10.221Z" },
{ url = "https://files.pythonhosted.org/packages/40/11/a758ae0116ab663f3520970cd870da5616cbb09c369b843239cce50696d7/sqlalchemy-2.1.3-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10fdff11782842696172489bd4861ae63c212e62ee7c4fc61fdda90961abd1f3", size = 4830599, upload-time = "2026-10-03T02:54:03.353Z" },
{ url = "https://files.pythonhosted.org/packages/8a/e8/faa500255fd90f421ef9e918d522aa4f02212b53859c35dadbafb3532bb2/sqlalchemy-2.1.3-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:edd3e6690f350c89c616e78e9b58833c3d8925607ac5faf3530f38f053f58e4d", size = 4481272, upload-time = "2026-10-02T23:59:15.592Z" },
{ url = "https://files.pythonhosted.org/packages/3c/4d/26fc9adb3fbc1d5fb8b92043a1639fd111761b8c52a2ce61df8b00ff405d/sqlalchemy-2.1.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:e3c165df26eca3d2612f60ea91af02648cb1bd492eadfa64dba15c9a3a0fb09c", size = 4737553, upload-time = "2026-10-03T02:49:12.413Z" },
{ url = "https://files.pythonhosted.org/packages/08/41/3639fc1d9fd7ea201ed4941215c14b87ce3c3b5f5f0190eb93321d673e48/sqlalchemy-2.1.3-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:70c073d28d043766eb1725010792f302f59b14636ffef478793045471efe92dd", size = 4479761, upload-time = "2026-10-02T23:59:17.572Z" },
{ url = "https://files.pythonhosted.org/packages/47/c8/541035b19b34dd581fb24b6fcf065db6fb4f210b9fbf93e1b2bf5f8cd5c0/sqlalchemy-2.1.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:bf5f63229512f14e7bffc9a0041386318f592e76bbeaf9b750b39056094d91f3", size = 4757307, upload-time = "2026-10-03T02:54:05.524Z" },
{ url = "https://files.pythonhosted.org/packages/40/a3/bb51886c02a60f1b7fb5461a25d6a3e57ce0557b916032dc3c7a72fae226/sqlalchemy-2.1.3-cp315-cp315t-win32.whl", hash = "sha256:697942762c40e31eb0fa39bf0e58e1ac1b5b1ff9022da62b74f7cdc53096a482", size = 2433533, upload-time = "2026-10-03T02:56:52.298Z" },
{ url = "https://files.pythonhosted.org/packages/e5/8e/1739b378a2b40bfc5541355c9d944b96dffcbe86aa1ed3aff31ad13f5d9b/sqlalchemy-2.1.3-cp315-cp315t-win_amd64.whl", hash = "sha256:a9c33c0d081a9d2c94229b9ae1834cb2ef81dd81253df4763e32111287b64c31", size = 2499211, upload-time = "2026-10-03T02:56:54.801Z" },
{ url = "https://files.pythonhosted.org/packages/5d/9a/ce4ea58551258203381017bc4724369cc8948ac5a46212671266b13424c4/sqlalchemy-2.1.3-cp315-cp315t-win_arm64.whl", hash = "sha256:c36ff11196dacf50312a2cc51a798bb41611331c174871f065be651a838a5361", size = 2418086, upload-time = "2026-10-03T02:44:11.611Z" },
{ url = "https://files.pythonhosted.org/packages/e5/12/23d8fb7ed90e5b674006ace415ede1c60cdfc38ace50968b59ef42c22754/sqlalchemy-2.1.3-py3-none-any.whl", hash = "sha256:6f0cf4debd86cb5623a50310784bd005c215761ca7f618d0d4f46193d033675a", size = 2052603, upload-time = "2026-10-03T02:34:19.115Z" },
]
[[package]]
name = "starlette"
version = "1.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7b/2b/3850dc6bf7ef71b088962eba31dafc6cffd2f96e577ebb0bb316df96da3e/starlette-1.7.0.tar.gz", hash = "sha256:c79f74ea63cff761804fbbfb182f1e0b440c2d07b164d24700c5a1bab5d6ff5d", size = 2736246, upload-time = "2026-09-23T07:30:26.35Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/d6/1ec1b290f9e0fb067899b61e1d37a30c923068bad260b216dbe37a7d2967/starlette-1.7.0-py3-none-any.whl", hash = "sha256:67f8e99895493dd2911a03f11314af6ceebeae4e704bb9f43dfc6a9db151c93e", size = 78980, upload-time = "2026-09-23T07:30:24.567Z" },
]
[[package]]
name = "typing-extensions"
version = "4.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
]
[[package]]
name = "typing-inspection"
version = "0.4.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" },
]
[[package]]
name = "tzdata"
version = "2026.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/31/3d74fa778a63b98b7374323befcc0be5ab3bd94afd4096a0124e7379152c/tzdata-2026.4.tar.gz", hash = "sha256:f1b8bd365d8d210c55353f4d7f8d6d8561c0ba50d704b700d195a9424bba0d79", size = 199350, upload-time = "2026-09-12T12:56:03.251Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/bc/8737e8d54cf51106118039b83f485a4783112fab49ea9d044b234978a46e/tzdata-2026.4-py2.py3-none-any.whl", hash = "sha256:c2169a8b0a7a5e9674da5a135ccdfb2b3e671b333ed9fed17b41f73c34476e81", size = 347494, upload-time = "2026-09-12T12:56:01.67Z" },
]
[[package]]
name = "uvicorn"
version = "0.54.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/da/34/30e9280707135d2cfc589dfff3cb796bd07a3aeb1a3e415ba09dd89d7bb4/uvicorn-0.54.0.tar.gz", hash = "sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620", size = 112283, upload-time = "2026-09-25T06:52:37.601Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl", hash = "sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf", size = 87427, upload-time = "2026-09-25T06:52:35.829Z" },
]
[package.optional-dependencies]
standard = [
{ 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.23.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fa/42/02c739ce85fb2ee8d99212c61417da8140c6b87e9d97c430bea520d76044/uvloop-0.23.0.tar.gz", hash = "sha256:28d160f51ab4da3b187063652e643dea6831072add4adc1e6d62afbe73b6be27", size = 2559185, upload-time = "2026-10-01T03:17:04.4Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/83/eb980d64e6dd5da46d4dc35755fa6afd6b5b47141437cf89615f1117c5a6/uvloop-0.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2dcff2d69be43e6559e5dad2c5a7a2dbfb60e05a77311b6c4b7a4a8123d86c65", size = 1412726, upload-time = "2026-10-01T03:15:52.49Z" },
{ url = "https://files.pythonhosted.org/packages/04/c1/02a725e7698134c647904bdee6589e2be14a0e7fc9942c74f86e2b90d48b/uvloop-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:19c64108b507cd0bc140e400e3396bacebd9d504956aa7726272bf6de7d9aabb", size = 779071, upload-time = "2026-10-01T03:15:54.02Z" },
{ url = "https://files.pythonhosted.org/packages/0b/1d/cde53c79e8c01884ad1cdca8e407e086d523362cfe4139e2c2a8dde27304/uvloop-0.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1748321e3c59a14a75404b1ae8d5a8d81c4e201803ea0e14c1b6fd84421024b5", size = 4395323, upload-time = "2026-10-01T03:15:55.549Z" },
{ url = "https://files.pythonhosted.org/packages/98/54/b12915bebbf99d7ae0796211e7f5977b95f069830dca45dc1a346d84125d/uvloop-0.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2cba180d6451822763eda8364f342435a873bcfb3849cbd82fdeca248ca65eb", size = 4480449, upload-time = "2026-10-01T03:15:57.362Z" },
{ url = "https://files.pythonhosted.org/packages/f7/8e/da6de68c31549a052a105fc76f5a9a204f6df22cb0909440aa4dbb06f9a2/uvloop-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dc61e4f9e37b507069dc7e659ae28bca7adcb04c993c3508214315d12c63f848", size = 4219177, upload-time = "2026-10-01T03:15:59.351Z" },
{ url = "https://files.pythonhosted.org/packages/a1/c3/1b53c6a89dc9c9d5cb75eb9a0b891ad69b32e1421ad3aa01617a9cbdcc78/uvloop-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7337b06a9f9ed9ea3049f04b76f65819db9b19bb832ee598e97b388eadf25e5f", size = 4346132, upload-time = "2026-10-01T03:16:01.064Z" },
{ url = "https://files.pythonhosted.org/packages/4e/a4/00e85345871c59c834a23c136c1771205856028ecc8ba940b3951178e59b/uvloop-0.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b90397a50ad6332ed3e459c648ac20d182cce24a557354363ad85fc9ea4a17cd", size = 1421363, upload-time = "2026-10-01T03:16:02.599Z" },
{ url = "https://files.pythonhosted.org/packages/d0/a9/e5f0f3cfde30af3ec32eba8ec07bccdba2b5116afbd1ecc53edfeb0a0790/uvloop-0.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:be53e1d5f83de43dc175c87612ecc128d444b38e5c56cb3f807f5a73d6887476", size = 785177, upload-time = "2026-10-01T03:16:04.018Z" },
{ url = "https://files.pythonhosted.org/packages/9e/79/9ddf78f8cd75a15c14a09a57f59c587b8cd9d82802c5c8368b9c3ebefa0b/uvloop-0.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b3cbc4f96ddfa1fb88a78a69dd851369825b7816d9702eee8c4461505ba172e", size = 4381060, upload-time = "2026-10-01T03:16:05.642Z" },
{ url = "https://files.pythonhosted.org/packages/1e/20/57d63c44d32326878fcad5c63854afc9deb394ed95673c1b1a429178c79d/uvloop-0.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31e0cf90bc8fd88784f6802cdba968a51fb1aec1cc3feec74d862b2d371d1330", size = 4418891, upload-time = "2026-10-01T03:16:07.326Z" },
{ url = "https://files.pythonhosted.org/packages/12/c5/0795abecda2cc3dfe41033f880a32a9ff103be4e6b177ac736833c153a0e/uvloop-0.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa8ed556fcc87a4091cf61587ef172fa104323dc89ecc085a618ba7ff8629a8f", size = 4214811, upload-time = "2026-10-01T03:16:09.13Z" },
{ url = "https://files.pythonhosted.org/packages/20/18/9010dacd5221eec1bd79a4a83ac68f3db6a42d7bb657f7b640c4838ca6b6/uvloop-0.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f3fbfe82829d8e381426a289b87e59e585278728361db9ce975b88b51f64f410", size = 4294876, upload-time = "2026-10-01T03:16:10.875Z" },
{ url = "https://files.pythonhosted.org/packages/b1/08/f6384a03c771d00067cba4f542a69b2fc1a982e9fd78b357c2f788678d72/uvloop-0.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:7e35c9bc977760981693e1a7a51493b58ee5a501f9ebb1e547565ee40b6c6208", size = 1494811, upload-time = "2026-10-01T03:16:12.399Z" },
{ url = "https://files.pythonhosted.org/packages/ac/01/756a4fb24a449f313cf4a153eb0c6210b49cfe5539255ec9fb1e17d2c4ef/uvloop-0.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5bb9be71d9ee39b4359b832f9569518ec9bc08704194034e79e4958e6bc4d46d", size = 819396, upload-time = "2026-10-01T03:16:14.094Z" },
{ url = "https://files.pythonhosted.org/packages/3e/45/e314b0c600b14f53dad3a3c2d7a922a249a88225fd727652b53e1854b9dd/uvloop-0.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e84575f11873c109cf3962ad0bdf679094466184125f4cadcc41a73febff41f", size = 4734966, upload-time = "2026-10-01T03:16:15.815Z" },
{ url = "https://files.pythonhosted.org/packages/66/0d/8686a7f0b1b2d55ebd770ba21f8e0e4ffa0cde5ab738f43ffb8264499052/uvloop-0.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbbdb8fcd5e7062e546eec1ac78c28bb21ae7df54c18f8e4b06e15a18d661a49", size = 4584963, upload-time = "2026-10-01T03:16:18.198Z" },
{ url = "https://files.pythonhosted.org/packages/78/b2/034a2d47e435ac02357c42956246887167bdc0357bdd6ad31c5f6d94497b/uvloop-0.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76345f51367fb1f23e08605c6efb18374f669be5b223658fbab6b17627950507", size = 4421388, upload-time = "2026-10-01T03:16:19.953Z" },
{ url = "https://files.pythonhosted.org/packages/f0/77/131f4b583e6b4b715c404a66b51c812d701db20f25c9018b188a2b00062c/uvloop-0.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c7ef4701a96553514b2688e342ef1bf2beae6cfd172d89a76c768292aabf405", size = 4402414, upload-time = "2026-10-01T03:16:21.716Z" },
{ url = "https://files.pythonhosted.org/packages/58/3d/ee11f4718ea1280595c67ed25c83d4c92115dc100bbdfd192d3ed9339168/uvloop-0.23.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:f1341c6abcee1c31277cfe28d34e46196f2143ec3d755e6efe7452126e1f626d", size = 1418095, upload-time = "2026-10-01T03:16:23.241Z" },
{ url = "https://files.pythonhosted.org/packages/f8/0c/7ca516a0671418517d79a09d3ff2ccbb44af94c75711afa6e4cf58aa6f65/uvloop-0.23.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e095f9e105af76593b4c183bb0bcbdae64bd913a59ec595732dc108b48730ab5", size = 784837, upload-time = "2026-10-01T03:16:24.666Z" },
{ url = "https://files.pythonhosted.org/packages/35/95/75d4e28e596d505b7ae11de517646b4ca3d369fb8537ba755410380da11a/uvloop-0.23.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f673d835bdb1a60229cc3609a113fd2c9ce3f4a3c75ad4eaed111180c00199d2", size = 4380276, upload-time = "2026-10-01T03:16:26.389Z" },
{ url = "https://files.pythonhosted.org/packages/10/99/68daf827ad62efaf4667d1f3fda127046d42161178396bdd93aab3684082/uvloop-0.23.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3f23f403a273900d57de6ee5ca0614c650f7f58563065dad1a4744498960e53", size = 4451496, upload-time = "2026-10-01T03:16:28.364Z" },
{ url = "https://files.pythonhosted.org/packages/71/69/f67e696ee688f426a96f99099bae26fec14a1d0fa75dccdd6518ee267c0c/uvloop-0.23.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cbe8d03d4efcccdb7fcedecbaa1e1fa02913eaf3a74cb933634a6bc6d2ea9e2a", size = 4212541, upload-time = "2026-10-01T03:16:30.014Z" },
{ url = "https://files.pythonhosted.org/packages/f1/6a/c8c436a9d7453297b4be70bdf6a9f9fc9400da45e0059ddf7b28ab63f4c7/uvloop-0.23.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:4f1798f56c6f4ba5ac11fa2869e5717926e4470d97a1dd42b4f59219d43b5027", size = 4319377, upload-time = "2026-10-01T03:16:31.705Z" },
{ url = "https://files.pythonhosted.org/packages/3b/2c/8fc15a03489299aab8a6212dfe0f137dc39836f915c87f7fd9d9ddd814de/uvloop-0.23.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:098a85e1393ef5202767b7e5fb41a32cd8bd81e6ee4af364c179801c4aa3f6d4", size = 1493428, upload-time = "2026-10-01T03:16:33.859Z" },
{ url = "https://files.pythonhosted.org/packages/b7/7c/05e4a210790229607f71460fcb2ed4a2c7bc72668d8a928ce577c22e38f8/uvloop-0.23.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:5a2bbad3a63007f7e9524d4903ba04fee252557c2acd86f9a3d4f91786695254", size = 818115, upload-time = "2026-10-01T03:16:35.45Z" },
{ url = "https://files.pythonhosted.org/packages/65/14/a40b11c6c024213803b13955664a15754c72f64c873a33d986b26ec9ff5b/uvloop-0.23.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a08875543bbd4519faf30497506c9cda8a48470467ffdf967c7313c7a5981a8", size = 4734149, upload-time = "2026-10-01T03:16:37.025Z" },
{ url = "https://files.pythonhosted.org/packages/9f/83/f421a077712c1e87603bfec62744c3cd3a2f4b47378025db3d740df9af0d/uvloop-0.23.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12634f15e6625f78b3f2922f91404c4d7173487eba11746764153f556e9852dc", size = 4661763, upload-time = "2026-10-01T03:16:38.719Z" },
{ url = "https://files.pythonhosted.org/packages/f5/62/25dcaa6b7e7b48f82ce633854ce96597ab768f9650931f4f86c572de392c/uvloop-0.23.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:378188efbb1524f2219d05246a3e1e5907217848d2882144dff59585f1b81d55", size = 4421324, upload-time = "2026-10-01T03:16:40.488Z" },
{ url = "https://files.pythonhosted.org/packages/05/46/04628239b43dcef703af314202a3307d6060918e2d76aa86c5b1188f5551/uvloop-0.23.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4b8e207c67d207a8608fec57e116511030af3495dc0109b8c333cf9cb412b16f", size = 4462501, upload-time = "2026-10-01T03:16:42.359Z" },
]
[[package]]
name = "watchfiles"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b3/68/e6aa0b77d217b31f8f486ec0cdfe5e00e6e38dc0be657e7d85819b9faf0a/watchfiles-1.3.0.tar.gz", hash = "sha256:99aee4a07847c06820765fd7b1b49ceac4f3f711ccb7d104655a33231de1c207", size = 108119, upload-time = "2026-09-21T09:08:55.664Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/68/fa/c0b840d5d8bafe640925408ce11095948c8945bb61be1e67d0ab629b872a/watchfiles-1.3.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:000b9688fc8133037a8b075c8ebf98f32844ff8964dda61e85c1db547dafc441", size = 399703, upload-time = "2026-09-21T09:08:13.518Z" },
{ url = "https://files.pythonhosted.org/packages/c7/8a/894799b485fe9473ad10422a0d9668e53fb78e3a2b5cc6061159572844ad/watchfiles-1.3.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:bbc1198edfdc90fda0600f825aa94150f428dfcbf8138746f55998e0e660d64c", size = 397647, upload-time = "2026-09-21T09:08:15.16Z" },
{ url = "https://files.pythonhosted.org/packages/5a/61/f277cd5ead05f5d37b98b41fbdd0c943ee0c04d49738f62e902cb291cf43/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af9c464d410e8c44b58ddf1cbec7c3a02c1cb2c6c80df9c74659ba2fbda0ab5", size = 454994, upload-time = "2026-09-21T09:08:16.587Z" },
{ url = "https://files.pythonhosted.org/packages/46/66/ea4c01382975e53b1ac1b0a0ea1b353416b8127fb6e04f5c1c313119aab6/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb94e40b9a0db19636b5e3fed0a9803452ecc7381df13b385272abb779ee83ca", size = 460105, upload-time = "2026-09-21T09:08:17.961Z" },
{ url = "https://files.pythonhosted.org/packages/31/c3/1a501b597817096c9831c255f6270c124ce0e2faeb74124be24dcc93b86d/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7398930a2b76b9dc67a4e6b8bfc7baf30946892a86f417a3920c07bdd6febc30", size = 493761, upload-time = "2026-09-21T09:08:19.327Z" },
{ url = "https://files.pythonhosted.org/packages/5a/16/77352d05e152c7786a016e9f1decdb4507e8aa5bcf3343c72142a0413d14/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:025b108f2d5cc2799cb941f79380df3e8b22bfb589c218e801767fe78d598c95", size = 577500, upload-time = "2026-09-21T09:08:20.723Z" },
{ url = "https://files.pythonhosted.org/packages/95/fe/81001c55b24466fe9911135e50269fd3a29f43e446627a92adfab54553b6/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be9ef3cd403d756a304a0a08a112163c63da7ec72c8c9d083c98818291ff8f29", size = 469209, upload-time = "2026-09-21T09:08:22.346Z" },
{ url = "https://files.pythonhosted.org/packages/8a/cf/e3ad894ed5909a7c44b0707e89bc25d19a3d78b3a7aec846119443b6ae56/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5768b49e426fd5b550b012c866db347cdf15c398ef98dc557b6e6b72fa74cd1", size = 458353, upload-time = "2026-09-21T09:08:23.7Z" },
{ url = "https://files.pythonhosted.org/packages/7f/f5/2f106829e6e2bc91ad74614730f6df484202b9a4d20d927dbf2f08d37d44/watchfiles-1.3.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:d36c72fcdc08f143d87316a4c4c3b064e340da2c7383d39803950740ed418935", size = 468707, upload-time = "2026-09-21T09:08:25.167Z" },
{ url = "https://files.pythonhosted.org/packages/15/37/2741ab776b978669c1f3bba1e573f1a184f727ab8b4d3a6d9c61090800ed/watchfiles-1.3.0-cp310-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f0c9865240e065a2247f4b5248528ae3d01e0e8ae071462c250c884dc16e3ee3", size = 632510, upload-time = "2026-09-21T09:08:26.532Z" },
{ url = "https://files.pythonhosted.org/packages/48/64/db4a4684275fe9b696ee5118c194705c1423b3d42181c3f8ee70194d5c73/watchfiles-1.3.0-cp310-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:380f513d26cc2e598266b88d66456e07d67ef4be8f0f435a1154d9c44b43d509", size = 661723, upload-time = "2026-09-21T09:08:28.004Z" },
{ url = "https://files.pythonhosted.org/packages/52/80/95ddb24f6a2f595c031c24432ab7b8aa7d6d1ada0c6d63a552166e06e8d1/watchfiles-1.3.0-cp310-abi3-win32.whl", hash = "sha256:509d9f74d2bec5c1f4cd868ddcfe0c9601f7ea53066dfcb19d8ede742c4de661", size = 277761, upload-time = "2026-09-21T09:08:29.335Z" },
{ url = "https://files.pythonhosted.org/packages/48/a1/1c4a1b3c8030cd03206232612be8a6e8799093490c220b0fcb935d02b75a/watchfiles-1.3.0-cp310-abi3-win_amd64.whl", hash = "sha256:1acabde19b67e673274e89a04d613b7fb1f122e2c4b8ec9a581e59c149264b61", size = 290643, upload-time = "2026-09-21T09:08:30.45Z" },
{ url = "https://files.pythonhosted.org/packages/82/e0/d2db577c632d26d08078c4390dd870b8555c4e558df25d22270f0166c71f/watchfiles-1.3.0-cp310-abi3-win_arm64.whl", hash = "sha256:d978cc1dd7ba44f5590d7de74f7a9c8be7262d9e469c2c0efb8bbfc2574321dc", size = 285510, upload-time = "2026-09-21T09:08:31.561Z" },
{ url = "https://files.pythonhosted.org/packages/4f/55/a122743607939429ab6a7272672f49303aa02bbd34c3f06676765a3861de/watchfiles-1.3.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e0204f90677b7fb8d3a824279a71a01166bea9f6e53923b8056592d87b33b0b6", size = 395978, upload-time = "2026-09-21T09:08:32.747Z" },
{ url = "https://files.pythonhosted.org/packages/3e/9d/0ee1b0ea42acfe72afa2484f0222e2386dd09992e9034a81e63cc3cdab0a/watchfiles-1.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7c1125e99f84934a92996ae343501285617373acd66f16b59fd5654b27ad8d80", size = 387997, upload-time = "2026-09-21T09:08:33.953Z" },
{ url = "https://files.pythonhosted.org/packages/b7/f2/4319c6b683e8bd28465dfc53c750617a2a07f072eb2e3ab0182ccf812229/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a85e243c87109d9f48a2e0d10d6abdee8768cc114848b32281ec83d7f81d86ef", size = 448067, upload-time = "2026-09-21T09:08:35.104Z" },
{ url = "https://files.pythonhosted.org/packages/b7/71/9f37ac192a51a24de3a2d7fe9aac1e99b2b2668300674ff7a9f85d95beb1/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd538c71766c59c732e2c5cedc39323c99c11897b99f93eac773438e54af206a", size = 452881, upload-time = "2026-09-21T09:08:36.238Z" },
{ url = "https://files.pythonhosted.org/packages/34/07/1224667c32a4a96876ddd642da69733cbfe6a15897db0865aec5a882cebc/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da213822ec9082b62cadf3008e07a8c690ec7a3c89f6dc5e55a8cf07b56a1b26", size = 484802, upload-time = "2026-09-21T09:08:37.717Z" },
{ url = "https://files.pythonhosted.org/packages/bd/4d/7f29cd87e1f826df86fc0418d0b8f370ca0311c29c618406dc492aad9951/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4593392e87669836670f87d62fc754059eaeb0e6157af44060a46558cebe5a3", size = 567522, upload-time = "2026-09-21T09:08:38.94Z" },
{ url = "https://files.pythonhosted.org/packages/e3/ac/3aa1f84abd4dbbfbae99a817dd931f77b439261c911189cad4c30fdfd470/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26b81bc515a0f03f66a69a6fea86eea54c8eca4173c46dd7222569f79dd2f977", size = 461997, upload-time = "2026-09-21T09:08:40.142Z" },
{ url = "https://files.pythonhosted.org/packages/82/1c/27c66764d5b0dbb70a36770a32eca217eba739c28b24e8583a035087a3d7/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca39661934749df580d89f3dde266f2af7c6968f4c21c72ce2dc387d299e6aa7", size = 451856, upload-time = "2026-09-21T09:08:41.305Z" },
{ url = "https://files.pythonhosted.org/packages/1e/b3/a45827084d953c1d0dd3d75abf85dcac26a0f5d115ad2fe0b7e9e931573b/watchfiles-1.3.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:04c5500faa725d69a99b0f63750e80795e455bb951849e9c98a811b4324997c4", size = 459889, upload-time = "2026-09-21T09:08:42.58Z" },
{ url = "https://files.pythonhosted.org/packages/7a/3d/07740d23ce2bb23419bb24055c435a9abe5fe03efdaea1ba0720b831aab6/watchfiles-1.3.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:ec3cd4bb181a7b6329134204c2277c7144ab5f62cce9c5335d6f901ee7ce8d6a", size = 624482, upload-time = "2026-09-21T09:08:44.064Z" },
{ url = "https://files.pythonhosted.org/packages/91/46/9540f8123eaec86df989f26d7704cad0448d9fb7271763a4f8f9fe23617d/watchfiles-1.3.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e5871d4a7f788a9f64fb06f5793389d29d171e5c32d37c3ebfd520b017cc7694", size = 653724, upload-time = "2026-09-21T09:08:45.257Z" },
{ url = "https://files.pythonhosted.org/packages/25/9d/b9deb52f1e4528ffe44338c5ebab95c32ed79203082da661de50288e130a/watchfiles-1.3.0-cp314-cp314t-win32.whl", hash = "sha256:81c989d7267bfb7b676cd32c83bec97914e396870983cbf493e29415037bff5e", size = 270863, upload-time = "2026-09-21T09:08:46.518Z" },
{ url = "https://files.pythonhosted.org/packages/59/59/8b1881afccbafda06248be4185e28034dd73c80c85e4da24f2cb0c1321eb/watchfiles-1.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d754e049006e81a9be49dc78ba7802081cee426e968cbc63ee19e164fa2d0e39", size = 286879, upload-time = "2026-09-21T09:08:47.874Z" },
{ url = "https://files.pythonhosted.org/packages/46/dd/41c12ae9745d5d72f709333287679d71cbba68ad7678fed872207bba94f4/watchfiles-1.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a5b631db08fd3032db57ec6a92d1777cd60f88803287e4e941a4858c53aab1e0", size = 276924, upload-time = "2026-09-21T09:08:48.976Z" },
]
[[package]]
name = "websockets"
version = "17.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/72/fba934cb3dff7a85d811820efffcd141ddd52b5a2a01637f64551373ff4d/websockets-17.1.tar.gz", hash = "sha256:acfea4c20bf54384883ea33b1240fc1db4f52e190823a4e2b334bc3e8bfca96a", size = 187520, upload-time = "2026-08-26T17:25:33.063Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/31/5f6450a7879f4f063ef08897cc385ea3ce3f1fe17f08b11e3fd959abdf27/websockets-17.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a0162a6372110a5601cb5c9fd826635cedf69f3e110c545dd19774e040b970e", size = 217006, upload-time = "2026-08-26T14:56:10.509Z" },
{ url = "https://files.pythonhosted.org/packages/d0/2a/c1b006fc861695d2aa4e35327b842015ce1d98cf8f99241829b3d6460bfc/websockets-17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:829dba1bc049779de9b332088c1a6a9858e96bd67e50b6b644a95e02b67836bc", size = 214690, upload-time = "2026-08-26T14:56:11.681Z" },
{ url = "https://files.pythonhosted.org/packages/46/69/66e5b7d01445e0eeb1d4ab419c30315f2c90cf7a8a8cd4ecc47f894dba54/websockets-17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd8f47dbf2e8adb15c847215f83436de3fdb120b51fdae0fbbdf69fd97a3ad80", size = 214947, upload-time = "2026-08-26T14:56:12.923Z" },
{ url = "https://files.pythonhosted.org/packages/07/ce/033cafe2d2538562efa876b9149a2c7a0f7787870a4b1bb6e28adc9ceb6b/websockets-17.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9f4c0377a83e163a303514fdfab501dbe379bdc13e5b9312a91d112658b29dce", size = 224329, upload-time = "2026-08-26T14:56:14.212Z" },
{ url = "https://files.pythonhosted.org/packages/34/c7/e1c2e8a67f6cc0aa43abe0046fb3b7a020980649e6a843751dc7ce9eb170/websockets-17.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c3241d684a76eaaef8b2dc789afde4343cd3aad55ea81e4e8ab3605b529bae51", size = 224611, upload-time = "2026-08-26T14:56:15.702Z" },
{ url = "https://files.pythonhosted.org/packages/be/de/07c6d48eb3d2069709410c851e7de10ab83d752c4bd09862899627c2729b/websockets-17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5f5c7a893507d0e83a80b88aefd6522f7e882cd53f9722c6f23f5a020c9557c", size = 225848, upload-time = "2026-08-26T14:56:16.962Z" },
{ url = "https://files.pythonhosted.org/packages/f3/dd/3c68572d20509648cc2fb6f50ccf3deeb4b87270f2c8966e99476e278ea3/websockets-17.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00bf34b64501e3477e81fc281532ff3cbf4da26633c10b63979d5085d46602d3", size = 227290, upload-time = "2026-08-26T14:56:18.204Z" },
{ url = "https://files.pythonhosted.org/packages/0a/4a/8f6651c8a22093539c9215af0c5bbf217b87b382c99d2112039b92d593c2/websockets-17.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ce0305b702b20d1e1d60a9aaace6bc89970e1753565543f310d549eab22c2435", size = 226476, upload-time = "2026-08-26T14:56:19.459Z" },
{ url = "https://files.pythonhosted.org/packages/f5/be/f6fc33cea86b1127fd1297b18c107e81580ab55a73a39f9a934441ef321f/websockets-17.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29176d8b429cfa0fa443c473878d37a5c06cfd0cb36b71ba4314accc71e05906", size = 225233, upload-time = "2026-08-26T14:56:20.939Z" },
{ url = "https://files.pythonhosted.org/packages/cb/83/65edaf05f7c9b1dea82f4d252fdc37706a84571646f06119a27b0a16fe19/websockets-17.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3709a1ab30b4b922027d22f68d2b61a0656a91680ac894a537624e6be7dd7f7c", size = 222488, upload-time = "2026-08-26T14:56:22.208Z" },
{ url = "https://files.pythonhosted.org/packages/07/42/d1169c2f7f1f0032b0d4b0c00f0711a070cd7c735de37bfeb876bc0f9606/websockets-17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:43bd0c1ceb924d67f5c1a5254d8361dd9d94246e6331a726064dfa2917880780", size = 225295, upload-time = "2026-08-26T14:56:23.445Z" },
{ url = "https://files.pythonhosted.org/packages/a6/f4/64e2a386c3899b917c2933225c9b47887874229d159797f3bf1a11c20d51/websockets-17.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:1fce0f43e0d41422e0b2cad6561e1970df22f212f4c7e884967df7cf591b031c", size = 223891, upload-time = "2026-08-26T14:56:24.647Z" },
{ url = "https://files.pythonhosted.org/packages/26/b3/dfb5c482f7e310a3432fdbb045ddfe6d34114680e89a233d4ff900a32961/websockets-17.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4031152769179ab8dcdeafc7b0e58052a49117560a28671700b47b2c7b717aad", size = 224661, upload-time = "2026-08-26T14:56:26.027Z" },
{ url = "https://files.pythonhosted.org/packages/a4/cf/94865130a336029f46412adc127c4fbe380f46172b90ce251369e35c4302/websockets-17.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a06f3b5085176763182449559e20391d7ce616a8972a9f7a33deda87ea6d4f3c", size = 225766, upload-time = "2026-08-26T14:56:27.455Z" },
{ url = "https://files.pythonhosted.org/packages/96/34/eb8c658f86dfe562ed49a887a27424bfe9e618c26ea6f865b093d075d3a6/websockets-17.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:77b37cceca17291897c3c73bd30a7c7c7909593554b5da574ec852af83c1742a", size = 223323, upload-time = "2026-08-26T14:56:28.807Z" },
{ url = "https://files.pythonhosted.org/packages/1b/7e/2629609652ece5ca0c7ac235927dd4511b08131e3a5d53439b798fddf002/websockets-17.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d8e83333385cac6030a5167fd18bf96cc6c58b914c308e683f05b0cf94bc8dd0", size = 224276, upload-time = "2026-08-26T14:56:29.991Z" },
{ url = "https://files.pythonhosted.org/packages/a1/6b/8525737fe840b38e5f40956c198fb586a4fac1e07144d41a5b949b989cf8/websockets-17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:073c5c3f7e127041fa9d34a9e29ceefee8c3cafbd267ed2927318f425144380d", size = 224558, upload-time = "2026-08-26T14:56:31.184Z" },
{ url = "https://files.pythonhosted.org/packages/74/ab/3a958c6cbcf74b118f601c20a80ac8bd5e8dfec0bcf7345116feaeefb121/websockets-17.1-cp313-cp313-win32.whl", hash = "sha256:2afb58c7ba48b329d56769f8dfd89f394efe587b65ef806bae810a484d6d3608", size = 217475, upload-time = "2026-08-26T14:56:32.431Z" },
{ url = "https://files.pythonhosted.org/packages/22/36/fb521f0f2994c25509651f169efe5582dddd8713d57a0757ba87859372ef/websockets-17.1-cp313-cp313-win_amd64.whl", hash = "sha256:0340bbef6bfbe16da888b3983d666a4db4954ac3253c38f13bc7aba0c7db5a2f", size = 217784, upload-time = "2026-08-26T14:56:33.608Z" },
{ url = "https://files.pythonhosted.org/packages/68/92/9b8419584681a12a7534b746dfb2737c466efe2455483e2fbf8b941a04ec/websockets-17.1-cp313-cp313-win_arm64.whl", hash = "sha256:7a72efa3bf4fa3a6669a54420a472ad056da3973d827f10e3a536da463f926c2", size = 217715, upload-time = "2026-08-26T14:56:34.865Z" },
{ url = "https://files.pythonhosted.org/packages/90/0d/500cf5daea09d4669dff3a7d67159094a0bd6c4ef130381404f6edd3eb5f/websockets-17.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0c9982938980e086da59f70d05f9418cd143401a601a0faac10fa48f7bb1cd3e", size = 217048, upload-time = "2026-08-26T14:56:36.03Z" },
{ url = "https://files.pythonhosted.org/packages/97/12/5b12c6168aa269cffbfd24d177cd492b130120403a418c7e89462e27b4ac/websockets-17.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:57b39dc8541cf7ed3f639da82bf7451060483967f9e733da1f8173e4095f0642", size = 214737, upload-time = "2026-08-26T14:56:37.43Z" },
{ url = "https://files.pythonhosted.org/packages/0c/36/e453e5106e4e2416f008ac222837c2f1637a063b08008afcd1088889b631/websockets-17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:96abdecbaae746851b87c3a36cb4a661df93ca3d92f114270f79228bf1d00de6", size = 214955, upload-time = "2026-08-26T14:56:38.71Z" },
{ url = "https://files.pythonhosted.org/packages/dd/30/0204bb86176db02cdfc678ce65ed808a66fab87d250ce61a8790800a60b0/websockets-17.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9fc873e239c5abeb150bc24dbd1a7af23a9254526383ce0a077f5e20adbeb19", size = 224331, upload-time = "2026-08-26T14:56:39.924Z" },
{ url = "https://files.pythonhosted.org/packages/46/c8/d8372256e00c4e3cab1115c45075d1eeedb642a3f2b42bd70c4deae03f06/websockets-17.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f42912fa9eb4cb7c7ec9fde9b3332ba339eb8a8811981043d4029599f3d950b", size = 224685, upload-time = "2026-08-26T14:56:41.169Z" },
{ url = "https://files.pythonhosted.org/packages/12/7d/650355b8f67f908ff99603351d4458d1a0b787d627950a47c38db7e25308/websockets-17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f98bf378d7a5be047a044a1a27c987a8f355e10e3b5754617dbe756248cbc5ce", size = 225927, upload-time = "2026-08-26T14:56:42.359Z" },
{ url = "https://files.pythonhosted.org/packages/34/6c/a9ffa5b903579eed76017870f055d75ecc73988d9d0c9b65a92ba0bf2a27/websockets-17.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d334d11398086bb5559606cb42d51c013ea7c061c7db701521392373d3c087f5", size = 227300, upload-time = "2026-08-26T14:56:43.538Z" },
{ url = "https://files.pythonhosted.org/packages/9b/5d/4551c2269066af7481ee44605a0813770961615b5b5da3e87a8f5cb859ea/websockets-17.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c27336b1a0ac56569493e858497870347854372395f50483725f8cdacc5a45c", size = 226533, upload-time = "2026-08-26T14:56:44.669Z" },
{ url = "https://files.pythonhosted.org/packages/3c/43/237a99233e5c445759a613831b3a92e91905afc064dc3bd0ad33c35fd1e2/websockets-17.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67258b00302a5aaf0b267771c7014b13429abd7ea17eebc4c55bd935ff101555", size = 225280, upload-time = "2026-08-26T14:56:45.83Z" },
{ url = "https://files.pythonhosted.org/packages/d3/b5/e9407a91613d1d1cd932414143a1012096b26674a782fc55a0bd23217ee4/websockets-17.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:455ffeea0879d313205df1e745e5883e1feb7f31ecd26be882f5f0babd3db04f", size = 222540, upload-time = "2026-08-26T14:56:47.053Z" },
{ url = "https://files.pythonhosted.org/packages/db/d2/db76628db0577b783205d9779f64d8e373416b04c62d1546be4b75dc8540/websockets-17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f7233eaf441a345a5943a929fd4b5ea3278f11aed35a9ed0f3106b8cb3ca846a", size = 225354, upload-time = "2026-08-26T14:56:48.32Z" },
{ url = "https://files.pythonhosted.org/packages/a9/4c/2174181c067b89a74ae18e2650c2ac29959f4b796afe876ab3f4d30d642c/websockets-17.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c65da239a5ad553619804c1f9d65c1a0b3005381c6158ee14da2c7444cbd0c78", size = 223867, upload-time = "2026-08-26T14:56:49.579Z" },
{ url = "https://files.pythonhosted.org/packages/df/75/274decb9a8253561b5be3261e02a6676fc8ecdf31e95b722e53d5bfb8fd2/websockets-17.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9fa1ffa08c81a4f809cdab6129f8e55bee4650b9d6d3461019dda73aacd146b6", size = 224652, upload-time = "2026-08-26T14:56:50.885Z" },
{ url = "https://files.pythonhosted.org/packages/9f/e6/49824f1fb4db7656d2f7492b1d8be16147b759d909490e32f4776843ee64/websockets-17.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:406b8107943a43ef4649b1e0cb0cdc052bbf08fe1c8905a623c4af9586e5cebb", size = 225822, upload-time = "2026-08-26T14:56:52.356Z" },
{ url = "https://files.pythonhosted.org/packages/b8/6a/5dc43838c0b02a95f42c47a0de33c5ddd7767a9feeb4d0d8777ac1cfefe4/websockets-17.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:4e8ffcb486c8490a34a4cef5e4409d8da5a1cb1681e5bf7d786ce5e84aa8540d", size = 223379, upload-time = "2026-08-26T14:56:53.699Z" },
{ url = "https://files.pythonhosted.org/packages/c2/62/585637cf06d6b321232f79c55dc14d65518d12cf87c94c44f5864068810e/websockets-17.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fb88076df585b69c5761c387c0081aa87d7b9eb1b205a6535ca4777e25650d81", size = 224330, upload-time = "2026-08-26T14:56:55.184Z" },
{ url = "https://files.pythonhosted.org/packages/de/68/c3b234a6a1366b6ab5bbfaa4434a1b946e1dc4e8ddd6824bfd93a8835b7f/websockets-17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5d4724255fb8398acd9e583b97eb2279cec20e0bd0f9a94bf75f6056ef9f13da", size = 224622, upload-time = "2026-08-26T14:56:56.393Z" },
{ url = "https://files.pythonhosted.org/packages/6a/d4/84cf3d1376f5d8207f55f43c1c818babd6b89447f5dcd01f18a6d5526796/websockets-17.1-cp314-cp314-win32.whl", hash = "sha256:be3f0129c5654517b2abf07dcb75bb1d9479759a4ccfb569e8293579e9fc029a", size = 217036, upload-time = "2026-08-26T14:56:57.652Z" },
{ url = "https://files.pythonhosted.org/packages/d0/0f/9e7ac63c5d7cb642952200814f584318e65146df008b7d375d5d9c6b2c97/websockets-17.1-cp314-cp314-win_amd64.whl", hash = "sha256:2a4dc6ef83f4559e0d05f313a375cb38f63c986096a9da99fe94fdd779d313e5", size = 217382, upload-time = "2026-08-26T14:56:59.065Z" },
{ url = "https://files.pythonhosted.org/packages/54/bb/1ae6b91f7f3ac05f5c9f14a72dc2181c115ff370bcd8a7f10f02c174adfd/websockets-17.1-cp314-cp314-win_arm64.whl", hash = "sha256:46c0331c9eaaf73a559f3a9e388466be0df96eb83d40f06f1ca6ab6613b35c82", size = 217268, upload-time = "2026-08-26T14:57:00.654Z" },
{ url = "https://files.pythonhosted.org/packages/b3/f0/f65644d0e0b2b90918a8c41503841cc4072a58f2bf76c09bc36e751fc0dd/websockets-17.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d411ea5ca18ac1b12c0c94be88b60c18ca641ac43bcdfdf1c9f79d46cdbe1603", size = 217379, upload-time = "2026-08-26T14:57:02.181Z" },
{ url = "https://files.pythonhosted.org/packages/ff/35/4c46d1f620ac1a30f92b6eae78ee40a772a93f568647ca7ccdc5ea283cf8/websockets-17.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:07fa3e7c30e2c577928d359b56bf872a3e0cbcc15553eaa0907c1ee86344b56f", size = 214911, upload-time = "2026-08-26T14:57:03.478Z" },
{ url = "https://files.pythonhosted.org/packages/04/6e/4587e8406d7c1188e97b9cf466c081e93399380d447f885bfce81626cd37/websockets-17.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6de9acef07e3a78e9567fcd26c29011a4da8f050b13004bbf880a0fd82a6eea5", size = 215115, upload-time = "2026-08-26T14:57:04.692Z" },
{ url = "https://files.pythonhosted.org/packages/ec/06/1381c8fff525041025909eb80ace32489194a00ba22a0a8d428030afcc84/websockets-17.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ea0ed9373b880115911d9d39634bccc95b8ce590c9c42e8589f5cacc3ef3cee2", size = 224696, upload-time = "2026-08-26T14:57:05.899Z" },
{ url = "https://files.pythonhosted.org/packages/36/9d/9034e867dc85340be058619751742b895f722326e83100d110063461ca07/websockets-17.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50903d335bfda026c2fa11dd9aed09d8cbee0c451e3a85122a9acb041b7dc69b", size = 224975, upload-time = "2026-08-26T14:57:07.262Z" },
{ url = "https://files.pythonhosted.org/packages/40/eb/ed03aa3cae748ebf6397e5d44028f433f746bad09dc568ff754fda3a3c9b/websockets-17.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a74531ce81af587f906ab42f194032388fcff8fc7938402e5917c9147a39441", size = 226151, upload-time = "2026-08-26T14:57:08.524Z" },
{ url = "https://files.pythonhosted.org/packages/b1/c9/cc1964a096d16f3b73cb1ee5f14f277f5a3bcac07c6e8f9a1dcded99f4c8/websockets-17.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8fbf28e639544503b7d1c96452a5e5e043e4108d89b1f3fa02910603622d19db", size = 228292, upload-time = "2026-08-26T14:57:09.846Z" },
{ url = "https://files.pythonhosted.org/packages/1a/26/46da6dd0363c2db2e4876fd59a40fd40c1943a82d7018d0a33afbce47d52/websockets-17.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f612dc57f00c07cf4aa2673f7cbceabd654ad2457b7e639f061b794d6e11f9fd", size = 226722, upload-time = "2026-08-26T14:57:11.118Z" },
{ url = "https://files.pythonhosted.org/packages/78/98/ecd8f5e1c5d0e54c08ebc5c66852271112166db68107cb0e17ca1bf25009/websockets-17.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c7ac77401227212dc6e849182feee50d57cf456ec6329ffd6979c94bb136c5c", size = 225451, upload-time = "2026-08-26T14:57:12.601Z" },
{ url = "https://files.pythonhosted.org/packages/65/4d/da8d2760db53e17aae763738b6ba834b1fcf16813d3632f3edb6951e1ec8/websockets-17.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32a2a68d989d6e5b74a9d5095415c51189ebae29fceb7cf2b64a1c0318a81256", size = 223003, upload-time = "2026-08-26T14:57:13.875Z" },
{ url = "https://files.pythonhosted.org/packages/a4/40/ea401c141a79c5b1d0021a0dab9d0df2051c108f1620fbb39a6e7c714c3b/websockets-17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:aec00f018d34c67500ff0438dc314b40277be4a1b983cbacbf53ccf7db63e257", size = 225704, upload-time = "2026-08-26T14:57:15.091Z" },
{ url = "https://files.pythonhosted.org/packages/e1/8e/07ab3f44215d89840d5385fdcaaab1fed8caeffa67c6899e15062957c12c/websockets-17.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0014eaff8ad5b3b43feda2279f9d34bf2eaae040720b9fbbb55944b10f40b14d", size = 224192, upload-time = "2026-08-26T14:57:16.3Z" },
{ url = "https://files.pythonhosted.org/packages/58/93/ccf1af0a23e5748d4e22292a377d78d15cf294d7e707bbb11a8990ae6bd5/websockets-17.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:db9d7ee47f3ba531e278be539af39e2c7c7d28fb94897b6cd1120d63b0ef5922", size = 225082, upload-time = "2026-08-26T14:57:17.531Z" },
{ url = "https://files.pythonhosted.org/packages/e2/db/e32200f99ce282e728d2929f2c429db353cf3282db7d0eba99eb32c9fec1/websockets-17.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:ff3e2ba7a9f0a110b0555452e9b5a03a34e11662544e01beea15f144b48ba7b7", size = 226101, upload-time = "2026-08-26T14:57:18.802Z" },
{ url = "https://files.pythonhosted.org/packages/28/3d/e7a6e9777b29433620167c98f3caaff0d6b08b1239a273ef7f7fd1393349/websockets-17.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6da17fc94bd270f5987b10bee113461ac36a36a98b0481ddcc98056e5a90001a", size = 223794, upload-time = "2026-08-26T14:57:20.313Z" },
{ url = "https://files.pythonhosted.org/packages/48/05/ac569090726dedd6656f3ee28b0c02dfb1ba76e898dceaccc2987a237cef/websockets-17.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:e8dc3fa6d6b7ead3f9de57895f41b116a28787548e066365d9d90f7356bcaad2", size = 224567, upload-time = "2026-08-26T14:57:21.634Z" },
{ url = "https://files.pythonhosted.org/packages/14/50/4ef62941111db6b31193f4fabbb65f845a5177579040cb8fe0d774d25034/websockets-17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b65d5fe48219dc2d5e158de9e6514e75600f379cc7e37108d35f31764c155566", size = 224993, upload-time = "2026-08-26T14:57:22.86Z" },
{ url = "https://files.pythonhosted.org/packages/28/42/2b95ada4ea19bf3a2072b68669ce4f4afb212690b727d31640576287fd68/websockets-17.1-cp314-cp314t-win32.whl", hash = "sha256:2cce251f3e2469b99b6802b55435bcdd07123b41870f54c87b336183af9d7e68", size = 217168, upload-time = "2026-08-26T14:57:24.466Z" },
{ url = "https://files.pythonhosted.org/packages/32/0a/67d5ee08dd8060a37d612fd40a625b5376ad19ae48fe1c8ad428c278b817/websockets-17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f6c38cdcaf98a911d7acc25577f2f9e710f3a2fc2bde1563556784320196b51", size = 217508, upload-time = "2026-08-26T14:57:25.983Z" },
{ url = "https://files.pythonhosted.org/packages/76/a3/822005d0c674451d2411027b878cdc128a2b7ea5a30d337d9e279da22eba/websockets-17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:d1e2f5fa2b6d01f0d85b4f223fea7ed1d504be282a02a81bd2be4817ef7a2f03", size = 217425, upload-time = "2026-08-26T14:57:27.324Z" },
{ url = "https://files.pythonhosted.org/packages/de/d5/99a6c6a1eb5d5ae9f45f59a3c97f4e3b21f310eb404a547fb3e7d2fc054c/websockets-17.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:88381602e379165b66244b2ebc29f9b23ea0851fbe63ae157f91ca324f072d6f", size = 216970, upload-time = "2026-08-26T14:57:28.575Z" },
{ url = "https://files.pythonhosted.org/packages/a6/0e/1e7f6e833728193958d3ed3d67b5d57c3c7cfa948abf94d4bc553257c954/websockets-17.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:88bc5138e53903a85c354e59df7ba73ce306f7b09724cef74dba121e60a88ce2", size = 214699, upload-time = "2026-08-26T14:57:29.862Z" },
{ url = "https://files.pythonhosted.org/packages/07/00/95d39549f86e34425a0412bcbe61708dd1fc46af654e2134a6c4389102ad/websockets-17.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3546ef55b3a074494106508bc6505c73825970d2d9505f7bf53882b3e88b0d1e", size = 214927, upload-time = "2026-08-26T14:57:31.148Z" },
{ url = "https://files.pythonhosted.org/packages/4c/ff/b442415fc4f7f9943b0fc8e8eebaa13923ca73361e167c439ba634eecbd9/websockets-17.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9ae55d24241fc055f22aea3ac924559069848bd0ad4ea065fdd72d2194685fe8", size = 224373, upload-time = "2026-08-26T14:57:32.833Z" },
{ url = "https://files.pythonhosted.org/packages/a8/dd/b83537aae4cf61615b9d8b2dbb235c0030ba85457a6d934798273814600f/websockets-17.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d7b349265fad6244013eecd99df8d83c12bf3013943e431f4fadd5bffc37db42", size = 224801, upload-time = "2026-08-26T14:57:34.041Z" },
{ url = "https://files.pythonhosted.org/packages/76/83/5ab0abed58454909e8dbab45086ac68ee4556d7a8ada26735addc909b903/websockets-17.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc5789e5ea182b77a38881383ada5347202a6c66f4857d054e075290e80b604b", size = 225967, upload-time = "2026-08-26T14:57:35.292Z" },
{ url = "https://files.pythonhosted.org/packages/4b/26/e2412f2b998a8c1dfc00c0709ff6ee0c634dd0b0b4f92bdfe9667876b71c/websockets-17.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce13c7d233239e739600a57d4a73c1192ad8259e655a4d55aa1a454242bc809d", size = 227664, upload-time = "2026-08-26T14:57:36.493Z" },
{ url = "https://files.pythonhosted.org/packages/ec/25/0dd4495df3c0e02f6db705312ba85ab9b2dd42257dc23eb0da10066e4844/websockets-17.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1036189bd34b0bc1b10a4679321e2c7968af317efe6e8e4c1c5141c4254fb5bb", size = 226447, upload-time = "2026-08-26T14:57:37.781Z" },
{ url = "https://files.pythonhosted.org/packages/be/67/6df3f63ffc48f08126ed0cd2fd2a41092967c3e364f8ec100deae90b6d77/websockets-17.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e78fd4b7b2c5086a38671c9c882c1e643385eccea360b5b1fda4a105e590087e", size = 225343, upload-time = "2026-08-26T14:57:39.133Z" },
{ url = "https://files.pythonhosted.org/packages/b1/8d/a8479bbb09ff054907d141123d8f52fb6ae5ac39c6dbe39e6a02a8408309/websockets-17.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:46e7a10bf04318c7b0c0273791925ae5e1cbe4a11e34aa934d2ef27862058a80", size = 222748, upload-time = "2026-08-26T14:57:40.478Z" },
{ url = "https://files.pythonhosted.org/packages/40/fb/4c3d2a3269cde3f3087916de9c3d9fc5d7196b46846d8c3a9ae59ad0a884/websockets-17.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:33e45c7ea38428e740a7f233555d71df0b875cef7fc080acebc9654475e35335", size = 225453, upload-time = "2026-08-26T14:57:41.859Z" },
{ url = "https://files.pythonhosted.org/packages/7f/1c/6467b401d19408f34e1c7389c222c2c7e1dfdf08c551190269b5eabc726c/websockets-17.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:6e63c01803be425ff062b7f7fc201a74def1d49fc94a2410dd17375df75936e9", size = 224112, upload-time = "2026-08-26T14:57:43.136Z" },
{ url = "https://files.pythonhosted.org/packages/c5/5f/744e032ac80e11039a7447657ebabb46e9b5c2dbcec83be571335212932f/websockets-17.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:722ec21717eec6477bce582147a28acdfe034e604239466a6a95daedb863e774", size = 224646, upload-time = "2026-08-26T14:57:44.871Z" },
{ url = "https://files.pythonhosted.org/packages/9f/47/bcb9128d9afc4d0934d9192e2a24897ca2f7a63df2654904915349c6c46d/websockets-17.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:e74e41f0ad12ff1e8983e349daef79d37cc8280c743ce9d134d6c74c18dab5d6", size = 225797, upload-time = "2026-08-26T14:57:46.338Z" },
{ url = "https://files.pythonhosted.org/packages/c7/e0/b058047b7cf565e1105b10ef6b6b24a6ebe3575678c7dc75a645334705a7/websockets-17.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:12fe8984a32dbfd084e0603f1a8d740c0180cb85b3174585c54a80d2455a8394", size = 223605, upload-time = "2026-08-26T14:57:48.175Z" },
{ url = "https://files.pythonhosted.org/packages/b9/69/fc1555bff884de363f1bf9eebf2836dbeb29fa7e4f957debb7bbcf43abba/websockets-17.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:01dcb47deebc40b38fd4a493b9b9f4d0a704b7bec6f35e4d34085b329abce71a", size = 224508, upload-time = "2026-08-26T14:57:49.407Z" },
{ url = "https://files.pythonhosted.org/packages/e7/f9/648d4e68621688b19093b06f7b497d520952e68cdea1c1b54371fe9491de/websockets-17.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f4c45ee2512d3757b5e6c67c5a34e435143f2ecb7df3324f9fd888688c45c0f4", size = 224767, upload-time = "2026-08-26T14:57:50.799Z" },
{ url = "https://files.pythonhosted.org/packages/58/93/f8342b55864f71df13eb8e9ef7dce691b87a87f04f75bb8a1385b3336e7c/websockets-17.1-cp315-cp315-win32.whl", hash = "sha256:0f4f50dfe2cc810fc4e2de979b35e83bf8bb4bccdc6fe472d93762ea7b1d5927", size = 217003, upload-time = "2026-08-26T14:57:52.122Z" },
{ url = "https://files.pythonhosted.org/packages/ea/f0/7b5fdb774c245e0b6217009e2a24d2105c1a64923949f33be41aa7959302/websockets-17.1-cp315-cp315-win_amd64.whl", hash = "sha256:4af784f3e436f65b355c117c6497320f2b5cf6a559295cb1c4c7338e335d45cc", size = 217300, upload-time = "2026-08-26T14:57:53.492Z" },
{ url = "https://files.pythonhosted.org/packages/76/33/1fe6ed1b5087516115ca451b2c240314b010647071f8fc3bd78a21e4dddb/websockets-17.1-cp315-cp315-win_arm64.whl", hash = "sha256:d58159af7835fde09c462394293c0d7aaf8fb4557d8f8e5699f5e722ccae013d", size = 217214, upload-time = "2026-08-26T14:57:54.88Z" },
{ url = "https://files.pythonhosted.org/packages/94/ca/ed02e75996a266d76c5fcb5dd9b930db4cf2b388ca5fa3d2a72086f81568/websockets-17.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:1a5cf4e7bbe3ca499e6a289206cb4fcb7444b09919e129bd517f57d5fa192c13", size = 217282, upload-time = "2026-08-26T14:57:56.108Z" },
{ url = "https://files.pythonhosted.org/packages/bd/7d/d536f5bc89ea5b52fd1c1727c59fabafee6bc41f5ce92c3bd2f83047908c/websockets-17.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:416b4bc8789a1865a3ff643ec4ee073a5f52402d0dbeafd27b1798d5dd6b6a51", size = 214863, upload-time = "2026-08-26T14:57:57.355Z" },
{ url = "https://files.pythonhosted.org/packages/37/37/944cf17bad668e9be1247e6314f88a48b9faf7c250e383410db8b38af0b9/websockets-17.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:259f45358c76d3b18489e3e80636cdbe807e05ecf1b10fdf1a779106d23d0c8e", size = 215073, upload-time = "2026-08-26T14:57:58.719Z" },
{ url = "https://files.pythonhosted.org/packages/74/bf/3267966cc1bbc2b8fa62fd329651b0af502df1f5d1c0eed027ff339d6aa8/websockets-17.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9d01e8ede41fea4f5a847dad9d628355f74905f437a5b6856d67aa66d193800", size = 225229, upload-time = "2026-08-26T14:58:00.235Z" },
{ url = "https://files.pythonhosted.org/packages/7f/d8/85ea722f483510abb39fc71aafb4465d17cf9051a275ab036874ff3c300c/websockets-17.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7b35181a14cbfcae163b4de545d22abfd07d06c2c41ca69cfcd99251d6888ab", size = 225500, upload-time = "2026-08-26T14:58:01.994Z" },
{ url = "https://files.pythonhosted.org/packages/50/ce/64c7d00005bd0d15ecb5c5fcb7fb2597b6b92ddd16c4fa6bbc3d2835ad63/websockets-17.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a8e768a048c2220697477ce2e67e4345dc9f693d0ee6af53945b5e30227c6a7", size = 226829, upload-time = "2026-08-26T14:58:03.327Z" },
{ url = "https://files.pythonhosted.org/packages/b4/dc/096c67940fb957e667ca3c542818150434eb0388c6fdc90b3a502f3c3e96/websockets-17.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:880069d21cc33a558dcf180924a546d1ecf8ada5be3e4e70acee87019d706a24", size = 228457, upload-time = "2026-08-26T14:58:04.78Z" },
{ url = "https://files.pythonhosted.org/packages/51/fe/f2331b6b7ccc67589891da354fa46a5cb79e95f83b9fd0e734d77f1f2140/websockets-17.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cec1bb8f22abccc8d20f8ca63df9be41600c26c190f4b97ee86c675fd4a863a6", size = 227265, upload-time = "2026-08-26T14:58:06.102Z" },
{ url = "https://files.pythonhosted.org/packages/47/a5/fb1642302f8ec77ca922203074f155a9831a5128ad75e725059a476d1227/websockets-17.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f3a1d577e081667dda7f8e5b4796e6e32f9713c93e2a3d930669519840a3c623", size = 226143, upload-time = "2026-08-26T14:58:07.464Z" },
{ url = "https://files.pythonhosted.org/packages/d7/41/7133fcfb63f5562750b269d6a845c689dde6a2c6407286da395beea19ddd/websockets-17.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc053f9e95a76213c5eb7ed95779f7daf0d2bf0e4e03073629ebfa43a033f151", size = 223501, upload-time = "2026-08-26T14:58:08.766Z" },
{ url = "https://files.pythonhosted.org/packages/64/b1/82b36bfabc79ff2d383a1fc043cee6a13f794ef4f6bf1b4810ad6988cf6f/websockets-17.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:bb0efe019480a1c93e168ce96479273aaebd672fc8c350d5eed1e507ababb1b8", size = 226330, upload-time = "2026-08-26T14:58:09.987Z" },
{ url = "https://files.pythonhosted.org/packages/41/7d/5b511b9bf6e9ad331e6ff902fcbcc71c3794d10ef3b5efe80ccb8f0a7861/websockets-17.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:615746b12b26a3fd4077bc6fbeb277a1c192a45dd57b531d07ad9ed5c52a9a7a", size = 224980, upload-time = "2026-08-26T14:58:11.303Z" },
{ url = "https://files.pythonhosted.org/packages/e0/50/aed08f25301f8eef23be903ff9319fcf35630ca2bdec9d226f7d804dd5b3/websockets-17.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:1a20136d61f9ca3a31493732762661fafc2c20e8861930214e21afc6a8a692a2", size = 225478, upload-time = "2026-08-26T14:58:12.543Z" },
{ url = "https://files.pythonhosted.org/packages/3e/47/0d63d4168536b4682c9d19b7399443b1176f25dbb68878374fa716670230/websockets-17.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:2786cbd273ab69c22612db8a41229ddf2c158060b17b5928884bf388d07887f3", size = 226588, upload-time = "2026-08-26T14:58:14.457Z" },
{ url = "https://files.pythonhosted.org/packages/b3/dd/844bd0b6386fc81ed6a55f4b6dd26f01c6987eda205afa10175ea12b2164/websockets-17.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:b1c323fc3be1dc3f87f6c59458cb7d9e13dcbbf971d6c3f3e2bbaf58d3bfcdfe", size = 224336, upload-time = "2026-08-26T14:58:15.778Z" },
{ url = "https://files.pythonhosted.org/packages/96/18/03709c84bc88ec4dcea68d4be4ccd07d611073dec111203a5bf45af8809d/websockets-17.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:12c8e2b25df59755954a04dfa09c990b96691025aaf7eafd19ed6da24b09c18d", size = 225197, upload-time = "2026-08-26T14:58:17.141Z" },
{ url = "https://files.pythonhosted.org/packages/27/cf/0d1c694b6466c89e875b85b32b51312c472cf6708eee91914866f5087dde/websockets-17.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f58f58b4b29bbea2a3635e2c56eff4a3adab011fe383802a9e542e31b97085fc", size = 225493, upload-time = "2026-08-26T14:58:18.521Z" },
{ url = "https://files.pythonhosted.org/packages/4e/f5/99857c3dd9676749f33e3668665a34ad6099505fb8d75eb084f49f7807a9/websockets-17.1-cp315-cp315t-win32.whl", hash = "sha256:f78a3ffb1994304db2c0c4588e4d1a518079b557054fa3bb985a6f5e50ff49a3", size = 217130, upload-time = "2026-08-26T14:58:20.037Z" },
{ url = "https://files.pythonhosted.org/packages/2c/84/77599922ab441bfe61508f97dab2c71f8e114d31793993ea54011db16199/websockets-17.1-cp315-cp315t-win_amd64.whl", hash = "sha256:ad68c28a27246fed109a4409393d677b7e1388345cbbd2f5aee5c182d8506110", size = 217448, upload-time = "2026-08-26T14:58:21.382Z" },
{ url = "https://files.pythonhosted.org/packages/ce/3c/8b9a225b523f06a9389be81f1b0ab07c49bec6014742e6aa359c1f920f1f/websockets-17.1-cp315-cp315t-win_arm64.whl", hash = "sha256:e552e0037230ac16e5f568de7012041344d1b18c9feed30ec2891b8eba55af81", size = 217372, upload-time = "2026-08-26T14:58:22.807Z" },
{ url = "https://files.pythonhosted.org/packages/41/63/23572870e01836a98346075b9e17a8bc24a6ddd9800a3204ceee58677f3c/websockets-17.1-py3-none-any.whl", hash = "sha256:f221081107b8c48184d99f7019604486376e7ef826037e70aad6b02540732c23", size = 211134, upload-time = "2026-08-26T17:25:31.397Z" },
]