13 Commits

37 changed files with 3491 additions and 125 deletions
+1
View File
@@ -15,6 +15,7 @@ key-pw.xml
secrets.local.xml
*.local.xml
config/javis.toml
/javis.toml
config/*secret*.xml
!config/*.example.xml
+19
View File
@@ -18,6 +18,12 @@ Alle wesentlichen Projektänderungen werden hier in verständlicher Form dokumen
- SQLite-Sitzungen mit geordneter Nachrichtenhistorie außerhalb des Repositories.
- CLI-Befehle für neue, gelistete, geladene, geleerte und beendete Sitzungen.
- Unit-Tests für Konfiguration, Speicher, Chatkern, Provider und CLI.
- Sicherer Gemini-Key-Speicher über Betriebssystem-Keyring.
- Offizieller Gemini-Provider mit lokalem Datenschutzrouter
(`allowed`/`ask`/`never`) und sichtbarem Ollama-Fallback.
- Strikter Nullkostenmodus, lokale Schutzlimits und inhaltsfreies Provider-Audit.
- Bereinigte CLI-Befehle `/provider`, `/privacy` und `/status`.
- Sicheres PowerShell-Startskript ohne Systemänderungen.
### Changed
@@ -29,8 +35,21 @@ Alle wesentlichen Projektänderungen werden hier in verständlicher Form dokumen
- Offizielle VS-Code-Erweiterungen für Python, Pylance und Python Environments eingerichtet.
- `qwen3:8b` mit echten deutschen Antworten und GPU-/Zeitmessungen auf der RTX 3060 geprüft.
- Projektstatus, Architektur und Datenmodell auf den funktionierenden Textchat aktualisiert.
- Chatlaufzeit auf austauschbares Hybridrouting umgestellt; alte Ollama-Sitzungen
bleiben ladbar.
- Nicht geheime Providerkonfiguration liegt lokal außerhalb von Git; ein Key wird
erst unmittelbar vor einem zulässigen Cloudaufruf geladen.
- Allgemeine unpersönliche Technikfragen werden präziser als `allowed`
klassifiziert; persönliche, interne und geheime Inhalte behalten Vorrang.
- Privacy-Ausgabe unterscheidet lokalen Modus von technischem Fallback.
### Security
- Lokale Secret-Dateien, Laufzeitdaten, Modelle, Datenbanken und Logs werden von Git ausgeschlossen.
- Das Sprachmodell darf keine Secret-Werte erhalten.
- Gemini bleibt ohne lokale Aktivierung, Key, Nullkostenmodus und bestätigtes
Nicht-Billing deaktiviert; Javis aktiviert niemals Billing.
- Cloudkontext wird lokal klassifiziert und minimiert; 429, Netzwerk-, Timeout-,
Key- und Modellfehler fallen auf Ollama zurück.
- Gemini-/Datenschutzablauf wurde am 30.07.2026 zusätzlich von Pascal manuell
erfolgreich abgenommen; kein Keywert erschien in der Ausgabe.
+26 -14
View File
@@ -4,34 +4,46 @@ Javis ist ein langfristiges, lokal-first ausgerichtetes Projekt für einen pers
## Aktueller Stand
Der erste lokale Textchat funktioniert auf Windows mit Ollama und `qwen3:8b`.
Er bietet echte lokale Modellantworten ohne Cloud-Fallback, neue und fortsetzbare
Sitzungen sowie SQLite-Persistenz außerhalb des Repositories.
Der lokale Textchat funktioniert auf Windows mit Ollama und `qwen3:8b`.
Ein optionaler Gemini-Free-Provider ist auf einem Feature-Branch vorbereitet;
Datenschutzrouter, Nullkostenmodus und lokales Fallback sind automatisiert
getestet, aber noch nicht von Pascal mit einem echten Key abgenommen.
Noch nicht implementiert sind Obsidian-Zugriff, Serverzugriff, Sprache, Mobile
Client und Living Mind.
## Lokaler Start
## Einfacher Start
Ollama läuft in einem eigenen Terminal ausschließlich auf localhost:
Aus dem Repository:
```powershell
$env:OLLAMA_MODELS = 'D:\Javis-Data\ollama-models'
$env:OLLAMA_HOST = '127.0.0.1:11434'
& 'D:\Javis-Tools\ollama\ollama.exe' serve
powershell -ExecutionPolicy Bypass -File .\scripts\start-javis.ps1
```
Der Chat startet aus dem Repository in einem zweiten Terminal:
Das Skript findet die portablen Werkzeuge standardmäßig relativ zum Laufwerk des
Repositories, setzt Variablen nur für seinen Prozess, prüft Ollama ausschließlich
auf `127.0.0.1:11434`, startet es bei Bedarf versteckt und öffnet den Chat. Es
ändert weder PATH, Registry, Autostart, Windows-Dienste noch die systemweite
PowerShell-Ausführungsrichtlinie.
```powershell
$env:UV_CACHE_DIR = 'D:\Javis-Tools\uv-cache'
$env:JAVIS_DATA_DIR = 'D:\Javis-Data\runtime'
& 'D:\Javis-Tools\uv\uv.exe' run javis chat
```
Abweichende Pfade sind über die Skriptparameter `-UvPath`, `-OllamaPath`,
`-OllamaModels`, `-DataDir`, `-UvCacheDir` und `-PythonInstallDir` oder die
entsprechenden `JAVIS_*`-Umgebungsvariablen möglich.
Im Chat zeigt `/help` alle Befehle. Ohne `JAVIS_DATA_DIR` nutzt Javis einen
sicheren benutzerspezifischen Plattformstandard.
Gemini bleibt standardmäßig aus. Die spätere lokale Einrichtung erfolgt ohne
Key in der Konfigurationsdatei:
```powershell
& 'D:\Javis-Tools\uv\uv.exe' run javis configure gemini
& 'D:\Javis-Tools\uv\uv.exe' run javis secrets set gemini
```
Der Key wird bei der zweiten Zeile verdeckt eingegeben und im
Betriebssystem-Keyring gespeichert, niemals in Git oder TOML.
## Grundregeln
- Plattformunabhängiger modularer Monolith, voraussichtlich in Python.
+8 -2
View File
@@ -1,5 +1,11 @@
# Konfiguration
`javis.example.toml` dokumentiert ausschließlich nicht geheime Einstellungen. Für den lokalen Betrieb wird später eine ignorierte `javis.toml` erzeugt.
`javis.example.toml` dokumentiert ausschließlich nicht geheime Einstellungen.
Die lokale Datei liegt standardmäßig unter `JAVIS_DATA_DIR\javis.toml` und damit
außerhalb des Repositorys. `JAVIS_CONFIG_FILE` darf alternativ einen absoluten
Pfad angeben. Umgebungsvariablen überschreiben lokale TOML-Werte.
Secret-Werte gehören nicht in TOML, Code oder Git. Die bevorzugte Zielarchitektur verwendet einen Secret-Provider, beispielsweise Betriebssystem-Keyring, geschützte Umgebungsvariablen oder später Docker Secrets. Falls Pascal XML nutzt, liegt die echte Datei nur lokal, mit restriktiven Rechten und Git-Ausschluss. Das Modell erhält niemals ihren Inhalt.
Secret-Werte gehören nicht in TOML, Code oder Git. Der Gemini-Key liegt im
Betriebssystem-Keyring und wird mit `javis secrets` verwaltet. Die TOML-Datei
enthält nur die lokale Bestätigung, dass Pascal das Google-Projekt ohne Billing
betreibt. Javis kann den externen Billingstatus nicht technisch verifizieren.
+15 -20
View File
@@ -1,25 +1,20 @@
# Entwurf für eine spätere TOML-Konfiguration.
# Der erste Chat liest die in .env.example genannten Umgebungsvariablen.
[app]
display_name = "Javis"
language = "de"
[paths]
data_dir = ""
[providers]
active = "ollama"
active = "auto"
[providers.ollama]
model = "qwen3:8b"
ollama_base_url = "http://127.0.0.1:11434"
base_url = "http://127.0.0.1:11434"
timeout_seconds = 180
cloud_fallback = false
[obsidian]
[providers.gemini]
enabled = false
vault_path = ""
write_areas = []
[security]
secrets_provider = "none"
require_approval_for_writes = true
model = "gemini-3.6-flash"
optional_model = "gemini-3.5-flash-lite"
free_only = true
billing_confirmed_disabled = false
max_requests_per_day = 25
max_input_chars = 12000
max_output_tokens = 1024
timeout_seconds = 30
max_attempts = 2
max_context_messages = 6
+13 -10
View File
@@ -8,12 +8,12 @@ Javis beginnt als modularer Monolith. Module besitzen klare Zuständigkeiten und
1. Interface: implementierte CLI; später möglicherweise Sprache, Mobilzugriff und Living Mind.
2. Core: implementierter Gesprächsablauf und Sitzungslogik.
3. Providers: kleine Schnittstelle und implementierter lokaler Ollama-Adapter.
4. Memory: implementierte SQLite-Sitzungen; ein semantischer Index ist nur geplant.
3. Providers: kleine Schnittstelle, lokaler Ollama- und optionaler Gemini-Adapter.
4. Memory: SQLite-Sitzungen und separates inhaltsfreies Provider-Audit.
5. Obsidian: zunächst lesender Adapter, später eng begrenzte Entwürfe.
6. Tools und Status: feste Schemas, Allowlists, Timeouts und bereinigte Ausgaben.
7. Security: Freigaben, Secret-Provider und Berechtigungsprüfung.
8. Audit: nachvollziehbare Aktionen ohne geheime Inhalte.
7. Security: lokaler Datenschutzrouter, Freigaben und Betriebssystem-Keyring.
8. Audit: Provider-Metadaten ohne Prompts, Antworten oder geheime Inhalte.
## Plattformgrenzen
@@ -24,10 +24,13 @@ Javis beginnt als modularer Monolith. Module besitzen klare Zuständigkeiten und
## Implementierter Chatfluss
`interface.cli` nimmt Text an, `core.chat_service` verbindet Sitzungsverlauf und
Provider, `providers.ollama` spricht nur die lokale Ollama-HTTP-API an und
`memory.sqlite_store` speichert erfolgreiche Benutzer-/Assistentenpaare atomar.
Scheitert das Modell, wird keine erfundene Antwort gespeichert.
`interface.cli` nimmt Text an und verbindet den `core.provider_router`.
Dieser klassifiziert die aktuelle Anfrage vollständig lokal, minimiert den
freigegebenen Kontext und wählt Gemini oder Ollama. `core.chat_service` speichert
erst eine erfolgreiche Antwort zusammen mit der Benutzernachricht atomar.
Es existiert kein Cloudadapter, Toolaufruf, Obsidian-Zugriff oder eigener
öffentlicher Netzwerkdienst. Der Ollama-Endpunkt muss eine Loopback-Adresse sein.
Gemini ist nur bei lokaler Aktivierung, vorhandenem Key, `free_only = true`,
bestätigtem Nicht-Billing und passender Datenschutzentscheidung erreichbar.
Ollama bleibt Loopback-, Datenschutz-, Offline-, Quota- und Fehlerfallback.
Es existiert weiterhin kein Toolaufruf, Obsidian-Zugriff oder eigener öffentlicher
Netzwerkdienst.
+104 -19
View File
@@ -58,11 +58,52 @@ Rootserver:
- Modelle liegen außerhalb von Git unter `D:\Javis-Data\ollama-models`.
- Der lokale Ollama-Dienst bindet nur an `127.0.0.1:11434`; PATH, Registry,
Autostart und Windows-Dienste blieben unverändert.
- CLI-Befehle: `/new`, `/sessions`, `/load <ID>`, `/clear`, `/help`, `/exit`.
- CLI-Befehle: `/new`, `/sessions`, `/load <ID>`, `/clear`, `/provider`,
`/privacy`, `/status`, `/help`, `/exit`.
- Sitzungsdaten liegen über `JAVIS_DATA_DIR` außerhalb von Git; der bestätigte
Smoke-Test nutzte `D:\Javis-Data\runtime\core-chat-smoke`.
- Kein Obsidian-, Laptop- oder Rootserverzugriff wurde implementiert.
- Keine Tool-, Sprach-, Mobil-, Status- oder Living-Mind-Funktion ist implementiert.
- Keine Tool-, Sprach-, Mobil- oder Living-Mind-Funktion ist implementiert.
- Offizielle Gemini-Prüfung abgeschlossen: `gemini-3.6-flash` und
`gemini-3.5-flash-lite` sind stabile GA-Modelle mit Free-Tier-Textnutzung;
Standard dieses Pakets wird ausschließlich `gemini-3.6-flash`.
- Sicherer Secret-Provider mit `keyring` 25.7.0 implementiert; unter Windows wird
der Windows Credential Locker verwendet.
- Secret-CLI: `javis secrets set gemini`, `status` und `delete gemini`; Setzen
verwendet verdeckte Eingabe und kein Befehl gibt den Schlüsselwert aus.
- Lokaler Datenschutzrouter klassifiziert jeden späteren Cloudaufruf konservativ
als `allowed`, `ask` oder `never`; `never` kann auch durch einen ausdrücklichen
Cloudwunsch nicht überstimmt werden.
- Nullkosten-Vorgaben und lokale Schutzgrenzen sind zentral konfiguriert:
Gemini standardmäßig deaktiviert, `free_only = true`, keine bestätigte
Billing-Freiheit ohne Pascals lokale Bestätigung, höchstens ein Retry.
- Provider-Nutzung wird separat in SQLite ausschließlich als technische Metadaten
protokolliert; Prompts, Antworten und Secrets werden dort nicht gespeichert.
- Isolierter Gemini-Provider nutzt das offizielle `google-genai` 2.16.0 mit API
`v1`, begrenztem Timeout und maximal einem Retry; 429 wird nicht wiederholt.
- Fehlender/ungültiger Schlüssel, 429, Netzwerkfehler und ungültige Antworten
werden als getrennte, bereinigte Fehler signalisiert.
- Hybridrouter erzwingt lokal Datenschutz, Freigabe, Billing-Bestätigung,
Tages-/Größenlimits und sichtbares Ollama-Fallback vor jedem Cloudaufruf.
- Ein abgelehnter Schlüssel deaktiviert Gemini für den restlichen CLI-Prozess;
alte reine Ollama-Sitzungen bleiben mit dem Hybridprovider ladbar.
- Nicht geheime Provider-Einstellungen werden aus
`JAVIS_DATA_DIR\javis.toml` geladen; Umgebungsvariablen können sie sicher
überschreiben. Die Aktivierungsvorlage enthält `free_only = true` und die
lokale Billing-Bestätigung, aber niemals einen API-Key.
- `javis configure gemini` verlangt zwei ausdrückliche Bestätigungen und lässt
Gemini bei leerer oder negativer Antwort deaktiviert.
- Der echte CLI-Chat verwendet nun `hybrid/auto`; der Key wird erst vor einem
zulässigen Cloudaufruf gelesen. `/provider`, `/privacy` und `/status` zeigen
ausschließlich bereinigte Modi, Entscheidungen und Zähler.
- `scripts/start-javis.ps1` verifiziert das Repository, setzt nur
Prozessvariablen, prüft Ollama auf Loopback, startet es bei Bedarf versteckt
und wartet begrenzt. Pfade sind per Parameter oder `JAVIS_*` überschreibbar.
- Allgemeine unpersönliche Technikfragen zu RAM/SSD, SQLite, HTTP, Git und
ähnlichen Begriffen werden gezielter `allowed`; `never` und `ask` behalten
Vorrang bei Secrets, interner Infrastruktur und persönlichem Kontext.
- `/privacy` trennt Inhaltsklassifikation, tatsächlichen Provider, Unterdrückung
durch Modus `local` und technischen Provider-Fallback.
## Aktuelle Architektur
@@ -72,8 +113,8 @@ Rootserver:
- Ollama-Provider akzeptiert nur lokale HTTP-Loopback-Adressen
- SQLite-Sitzungsspeicher mit atomaren Benutzer-/Assistentenpaaren
- Laufzeitdaten außerhalb von Git über `JAVIS_DATA_DIR` oder sicheren Plattformstandard
- aktuell ausschließlich lokales Modell; als separates Folgepaket ist Gemini als
bevorzugter kostenloser Provider für ausdrücklich cloudgeeignete Inhalte geplant
- Hybridprovider bevorzugt Gemini für cloudgeeignete Inhalte und verwendet
Ollama bei Datenschutz, Ablehnung, Offline-, Quota- und Providerfehlern
- strikter Nullkostenmodus ohne Billing-Aktivierung oder kostenpflichtigen Fallback;
lokales Modell bleibt Offline-, Datenschutz-, Ausfall- und Quota-Fallback
- Secret-Werte außerhalb von Code, Git, Prompts und Logs
@@ -104,6 +145,23 @@ Rootserver:
- Echten CLI-Chat ausgeführt und dieselbe Sitzung in einem neuen Prozess geladen,
aufgelistet und erfolgreich mit einer weiteren Modellantwort fortgesetzt.
- Obsidian, Ubuntu-Laptop und Rootserver blieben unverändert.
- Aktuelle offizielle Gemini-Modelle, Free-Tier-Preise, dynamische Quoten,
Auth-Keys, GA-SDK, Fehlercodes und Datenschutzbedingungen geprüft und unter
`docs/GEMINI_PROVIDER.md` kompakt dokumentiert.
- Betriebssystemgebundenen Secret-Provider und isolierte Fake-Backend-Tests ergänzt.
- Datenschutzrouter, minimale Cloud-Kontextauswahl, strikte Nullkosten-Defaults,
lokale Tages-/Größenlimits und metadatenbasiertes Provider-Audit ergänzt.
- Offiziellen Gemini-SDK-Client isoliert angebunden und ausschließlich mit
Test-Doubles gegen Erfolg, Key-, Quota-, Netzwerk- und Antwortfehler geprüft.
- Hybridrouting und Audit mit Test-Doubles integriert: `never` konstruiert keinen
Cloudclient, `ask` verlangt Freigabe, 429/Netzwerk/Keyfehler fallen lokal zurück.
- Lokalen TOML-Loader und sicheren Erzeuger ergänzt; vorhandene Konfigurationen
werden nicht ungefragt überschrieben und fehlerhafte TOML wird abgelehnt.
- Hybridprovider in die CLI verdrahtet, sicheren Aktivierungsdialog und
bereinigte Provider-/Datenschutz-/Statusbefehle ergänzt.
- Lesender Ollama-Status prüft nur Loopback-Erreichbarkeit und Modellliste.
- Sicheres PowerShell-Startskript und kompakte Startdokumentation ergänzt; keine
PATH-, Registry-, Autostart-, Dienst- oder Richtlinienänderung.
## Aktuelle Tests
@@ -118,9 +176,14 @@ Letzter bestätigter Projektstand:
- uv-Lock und `uv sync --dev`: bestanden
- Python in `.venv`: 3.12.13
- Ruff in `.venv`: 0.16.0
- Unit-Tests: 13 bestanden
- Unit-Tests: 66 bestanden; neue Technikklassifikation und Privacy-Ausgabe abgedeckt
- PowerShell-Syntax des Startskripts: erfolgreich geparst
- Ruff Lint: bestanden
- Ruff Formatprüfung: bestanden
- lokaler CLI-Smoke ohne Secretzugriff: bestanden
- Obsidian-Vault unverändert: 1 Markdown-, 11 Gesamtdateien, letzter
Schreibzeitpunkt weiterhin 30.07.2026 15:42:31
- Rootserver und Ubuntu-Laptop wurden nicht kontaktiert
Lokaler Modell-Smoke-Test:
@@ -138,10 +201,27 @@ CLI-/Persistenz-Smoke:
- `/sessions` listete Provider, Modell und Nachrichtenanzahl korrekt
- fortgesetzte Frage erhielt die echte lokale Antwort `4`
Abnahmestatus:
- automatisiert durch Codex getestet: Core-Chat, SQLite, CLI und lokales Modell
- manuell durch Pascal getestet: am 30.07.2026 erfolgreich; CLI-Start, echte
deutsche Antworten, Sitzungskontext, `/sessions`, `/exit`, Neustart,
`/load <UUID>`, vollständiger Verlauf und Fortsetzung
- Gemini/Datenschutz/Fallback automatisiert durch Codex getestet: ja
- Gemini/Datenschutz/Fallback manuell durch Pascal getestet: am 30.07.2026;
verdeckte Key-Speicherung ohne Wertausgabe, vollständiger `/status`, echter
Gemini-Erfolg, Cloudzähler, `ask`-Ablehnung, `never`, `/provider local` und
Startskript erfolgreich
- für Merge freigegeben: ja, nach den dokumentierten kleinen Korrekturen
- für produktive Nutzung freigegeben: nicht separat erklärt; keine Freigabe für
spätere Tools, Obsidian oder autonome Aktionen
## Git-Stand
- aktueller Branch: `main`
- aktueller Branch: `feat/gemini-privacy-router`
- stabiler Branch: `main`
- Feature-Branch ist zu `origin/feat/gemini-privacy-router` gepusht und verfolgt
diesen Remote-Branch; `main` und `origin/main` blieben auf `8a678ae`
- Grundgerüstcommit: `6d04171`
- letzter Commit vor der Handoff-Verdichtung: `0e82748`
- Handoff-Verdichtung auf `main`: `e6eb995`
@@ -152,6 +232,14 @@ CLI-/Persistenz-Smoke:
- Feature-Dokumentation: `5d567e5`
- lokaler Merge nach `main`: `f3d6dfb`
- geprüfter und gepushter Abschlussstand vor diesem Handoff-Update: `0a550e6`
- Secret-Provider: `570e7fa`
- Datenschutz/Nullkosten: `286d96d`
- isolierter Gemini-Provider: `f3e1183`
- Hybridrouter: `e57186f`
- lokale Konfiguration: `ab6ea48`
- CLI-Verdrahtung: `af9426e`
- Startskript: `141ff2e`
- Resttests: `a0af15a`
- `origin` verwendet HTTPS
- kein Force-Push und keine umgeschriebene Historie
@@ -164,16 +252,19 @@ CLI-/Persistenz-Smoke:
- aktuell: isolierte Python-Toolchain und reproduzierbare Entwicklungsumgebung
- aktuell: portable Ollama-Laufzeit und `qwen3:8b` außerhalb von Git verifiziert
- aktuell: lokaler CLI-Chat und persistente SQLite-Sitzungen vollständig abgenommen
- aktuell: Gemini-Free-Hybridrouting, Datenschutz und Nullkostenmodus
automatisiert und durch Pascal manuell vollständig abgenommen
## Offene Entscheidungen und Fehler
- endgültiger Produkt-/Repositoryname bleibt offen
- Startmechanismus für Ollama ist noch bewusst manuell und nur pro Prozess konfiguriert
- Startskript startet Ollama bei Bedarf pro Prozess; kein Autostart oder Dienst
- Antworten werden noch nicht gestreamt; Sitzungen besitzen noch keine Titel oder Suche
- genauer späterer Obsidian-Schreibbereich ist nicht freigegeben
- endgültiger Secret-Provider ist offen; lokale Klartext-XML wäre nur restriktiv geschützt und Git-ignoriert zulässig
- aktuelle kostenlose Gemini-Modelle, Modellkennungen und Kontingente müssen vor
der Implementierung anhand offizieller Google-Dokumentation neu geprüft werden
- normaler Secret-Provider ist festgelegt: Betriebssystem-Keyring; eine
Klartext-XML wird nicht für API-Schlüssel verwendet
- Google-Quoten bleiben projektabhängig und müssen in AI Studio geprüft werden;
lokale Limits sind nur zusätzliche Schutzgrenzen
- Gaming-PC zeigt derzeit nur 8 statt 16 logische CPU-Prozessoren; Ursache ungeklärt
- ASUS-Laptop ist nicht automatisch für dasselbe Modell wie die RTX 3060 geeignet
@@ -192,15 +283,9 @@ CLI-/Persistenz-Smoke:
## Nächster sinnvoller Auftrag
Als separates nächstes Featurepaket auf neuem Branch:
1. offizielle aktuelle Gemini-Free-Tier-Modelle und Kontingente neu prüfen
2. Secret-Provider ohne Schlüsselzugriff des Modells implementieren
3. Datenschutzrouting `allowed`/`ask`/`never` vor jedem Cloudaufruf erzwingen
4. Gemini als kostenlosen bevorzugten Provider und Ollama als lokalen
Datenschutz-/Offline-/429-Fallback implementieren
5. `free_only = true`, keine Billing-Aktivierung und alle geforderten Erfolgs-,
Key-, 429-, Netzwerk-, Fallback- und Datenschutztests nachweisen
Nach dem Merge ein separates, kleines Paket für Chatkomfort und Robustheit
planen, beispielsweise Sitzungstitel/Suche oder Streaming. Noch keine
Obsidian-Integration, Tools, Sprache oder Serversteuerung beginnen.
Keinen API-Key in Chat, Git, Obsidian, Logs oder Dokumentation übernehmen.
+2 -1
View File
@@ -10,7 +10,8 @@
| ADR-0006 | Ollama und qwen3:8b für den ersten Windows-Chat verwenden | angenommen |
| ADR-0007 | Gemini kostenlos bevorzugen, sensible Inhalte lokal halten und niemals Billing aktivieren | angenommen |
Offen bleiben insbesondere das endgültige Runtime-Secret-System, der genaue
Der normale Runtime-Secret-Speicher ist mit dem Betriebssystem-Keyring festgelegt;
keine XML-Secret-Datei. Offen bleiben insbesondere der genaue
Obsidian-Schreibbereich, das spätere Laptop-Modell und der Produktname.
Ausführliche Begründungen stehen unter [`docs/adr`](adr/README.md).
+18 -10
View File
@@ -35,19 +35,27 @@ $env:UV_CACHE_DIR = 'D:\Javis-Tools\uv-cache'
& 'D:\Javis-Tools\uv\uv.exe' run ruff format --check .
```
Vor dem Chat wird Ollama in einem eigenen Terminal gestartet:
## Komfortabler Start
Der normale Start aus dem Repository benötigt nur:
```powershell
$env:OLLAMA_MODELS = 'D:\Javis-Data\ollama-models'
$env:OLLAMA_HOST = '127.0.0.1:11434'
& 'D:\Javis-Tools\ollama\ollama.exe' serve
powershell -ExecutionPolicy Bypass -File .\scripts\start-javis.ps1
```
Danach:
`-ExecutionPolicy Bypass` gilt nur für diesen Prozess; das Skript ändert keine
systemweite Richtlinie. Es prüft Ollama auf Loopback, startet bei Bedarf die
portable Laufzeit versteckt und wartet höchstens 15 Sekunden. Werkzeug- und
Datenpfade können mit Parametern oder folgenden Umgebungsvariablen überschrieben
werden:
```powershell
$env:JAVIS_DATA_DIR = 'D:\Javis-Data\runtime'
& 'D:\Javis-Tools\uv\uv.exe' run javis chat
```
- `JAVIS_UV_PATH`
- `JAVIS_OLLAMA_PATH`
- `JAVIS_OLLAMA_MODELS`
- `JAVIS_DATA_DIR`
- `JAVIS_UV_CACHE_DIR`
- `JAVIS_PYTHON_INSTALL_DIR`
Die absoluten Pfade sind ausschließlich lokale Entwicklungsdokumentation. Der Anwendungscode darf keine festen Windows-Pfade enthalten.
Die Standardpfade werden vom Laufwerk des Repositorys abgeleitet; persönliche
Windows-Benutzerpfade sind nicht eingebrannt. PATH, Registry, Autostart und
Windows-Dienste bleiben unverändert.
+85
View File
@@ -0,0 +1,85 @@
# Gemini-Provider: bestätigte Rahmenbedingungen
Stand der Prüfung: 30.07.2026. Verwendet wurden ausschließlich offizielle
Google-Dokumentation und das offizielle Google-SDK-Repository.
## Modellauswahl
- aktiver Standard dieses Arbeitspakets: `gemini-3.6-flash`
- später optional konfigurierbar: `gemini-3.5-flash-lite`
- beide Kennungen sind stabile, allgemein verfügbare Modelle
- keine Preview-, Experimental- oder `latest`-Kennung wird Standard
`gemini-3.6-flash` ist für Standard-Textgenerierung im Free Tier mit kostenfreien
Ein- und Ausgabetokens aufgeführt. `gemini-3.5-flash-lite` ist ebenfalls im Free
Tier verfügbar, wird in diesem Paket aber nicht aktiv geroutet.
Quellen:
- <https://ai.google.dev/gemini-api/docs/models/gemini-3.6-flash>
- <https://ai.google.dev/gemini-api/docs/models/gemini-3.5-flash-lite>
- <https://ai.google.dev/gemini-api/docs/pricing>
- <https://ai.google.dev/gemini-api/docs/deprecations>
## Quoten und Kosten
Google misst Limits unter anderem als Requests pro Minute, Eingabetokens pro
Minute und Requests pro Tag. Die aktiven Werte hängen von Modell, Projekt und
Nutzungsstufe ab, sind nicht garantiert und müssen in Google AI Studio geprüft
werden. Javis bildet deshalb keine Google-Quote als feste Wahrheit ab, sondern
setzt zusätzliche konservative lokale Schutzlimits.
Der Free Tier erfordert kein Billingkonto. Der Wechsel in einen bezahlten Tier
erfordert laut Google eine aktive Billing-Verknüpfung. Javis kann den externen
Projektstatus nicht technisch beweisen und verlangt deshalb vor Aktivierung eine
lokale Bestätigung, dass Billing deaktiviert ist.
Quellen:
- <https://ai.google.dev/gemini-api/docs/rate-limits>
- <https://ai.google.dev/gemini-api/docs/billing>
## Datenschutz
Die Preisübersicht kennzeichnet kostenlose Standardnutzung grundsätzlich als für
Produktverbesserung nutzbar. Die zusätzlichen Nutzungsbedingungen enthalten eine
Sonderregel für EWR, Schweiz und Vereinigtes Königreich. Unabhängig davon bleibt
Javis konservativ: keine Secrets, Gesundheits-, Finanz- oder besonders sensiblen
Daten; persönliche Daten nur nach lokaler Freigabe und mit minimalem Kontext.
Quellen:
- <https://ai.google.dev/gemini-api/terms>
- <https://ai.google.dev/gemini-api/docs/logs-policy>
## Schlüssel und SDK
Neue Schlüssel aus Google AI Studio sind laut aktueller Dokumentation Auth-Keys,
standardmäßig auf die Gemini API beschränkt. Unbeschränkte Standard-Keys werden
abgelehnt beziehungsweise ausgemustert. Pascal soll einen neuen, ausschließlich
für Javis verwendeten Auth-Key in AI Studio erstellen und niemals in Chat, Git,
Obsidian, Logs oder Dokumentation einfügen.
Für Python wird das GA-SDK `google-genai` verwendet. Javis nutzt ausschließlich
zustandslose Textgenerierung über `generate_content`; keine Chatspeicherung,
Dateien, Tools, Grounding, Caches, Agenten oder Medienfunktionen.
Der reproduzierbare Lockfile löst aktuell `google-genai` 2.16.0 auf.
Quellen:
- <https://ai.google.dev/gemini-api/docs/api-key>
- <https://ai.google.dev/gemini-api/docs/libraries>
- <https://ai.google.dev/gemini-api/docs/generate-content/text-generation>
- <https://github.com/googleapis/python-genai>
## Fehler und Retries
Timeouts und geeignete Server-/Netzwerkfehler dürfen höchstens einmal kontrolliert
wiederholt werden. `429 RESOURCE_EXHAUSTED`, Authentifizierungs-, Datenschutz- und
Quota-Fehler werden nicht wiederholt. Anschließend wird im späteren Router sichtbar
lokal weitergearbeitet. Das SDK besitzt eigene Retry-Mechanismen; Javis konfiguriert
diese ausdrücklich auf höchstens zwei Gesamtversuche und schließt 429 aus.
Quelle:
- <https://ai.google.dev/gemini-api/docs/troubleshooting>
+17 -21
View File
@@ -1,30 +1,26 @@
# Nächste Sitzung
## Ziel
## Ausgangsstand
Nächstes separates Featurepaket: kostenlose Gemini-Anbindung mit strikt lokalem
Datenschutzrouting und Ollama als Ausfall-, Quota-, Offline- und Sensitivitätsfallback.
Der Core-Chat und der Gemini-/Datenschutz-Meilenstein sind automatisiert durch
Codex und am 30.07.2026 manuell durch Pascal abgenommen. Der Featurestand ist zum
Merge freigegeben.
## Vor Änderungen erneut prüfen
Bestätigt sind echter Gemini-Erfolg, verdeckte Key-Speicherung, Nullkostenmodus,
bereinigter Status, Cloudzähler, `ask`- und `never`-Routing, erzwungener lokaler
Modus sowie das Startskript.
1. `git status --short --branch`
2. `git log -1 --oneline`
3. `docs/PROJECT_STATUS.md`
4. offene Entscheidungen in `docs/DECISIONS.md`
## Nächstes sinnvolles Paket
## Vorgeschlagener Umfang
Nur ein kleines Chatkomfort-/Robustheitsthema auswählen, zum Beispiel:
- aktuelle kostenlose Gemini-Modelle, Kennungen und Kontingente ausschließlich in
offizieller Google-Dokumentation prüfen; Kandidaten nicht ungeprüft übernehmen
- Providerrouter mit `free_only = true` und ohne Billing-Konfiguration entwerfen
- lokale Datenschutzentscheidung `allowed`, `ask` oder `never` vor jedem Cloudaufruf
- Gemini-Key nur über lokalen Secret-Provider; Wert niemals an Modell, Git,
Obsidian, Logs, Dokumentation oder Chat geben
- Gemini-Erfolg, fehlenden/ungültigen Key, 429, Netzwerkausfall, lokalen Fallback,
`never`, `ask` und ausbleibende Billing-Aktivierung testen
- automatische, lokal erzeugte Sitzungstitel und Sitzungssuche oder
- Streaming lokaler und Cloudantworten mit sauberem Abbruchverhalten
## Nicht beginnen
Vorher wie immer Branch, Arbeitsbaum, aktuellen Handoff und Tests prüfen.
Keinen echten Schlüssel anfordern oder dokumentieren, kein Billingkonto verbinden,
keine kostenpflichtigen Fallbacks, keinen vollständigen Vault übertragen und
weiterhin kein Obsidian-Schreiben, SSH-/Serverzugriff, Sprache oder Living Mind.
## Weiterhin nicht beginnen
Noch keine Obsidian-Integration, Tools, SSH-/Serversteuerung, Sprache, Mobile
Client oder Living Mind. Der vorhandene Gemini-Key bleibt im Betriebssystem-
Keyring und darf nicht erneut in Chat, Logs oder Dokumentation erscheinen.
+55 -7
View File
@@ -1,6 +1,6 @@
# Projektstatus
Stand: lokaler Textchat auf `main`, 30.07.2026.
Stand: Gemini-Free-Feature automatisiert geprüft, 30.07.2026.
## Erreicht
@@ -24,15 +24,62 @@ Stand: lokaler Textchat auf `main`, 30.07.2026.
- echte CLI-Modellantwort sowie Laden und Fortsetzen derselben Sitzung in einem
neuen Prozess bestätigt
- 13 Unit-Tests, Ruff und Strukturprüfung erfolgreich
- lokaler Core-Chat am 30.07.2026 zusätzlich von Pascal manuell erfolgreich
geprüft: deutsche Antworten, Sitzungskontext, `/sessions`, `/exit`, Neustart,
`/load <UUID>`, vollständiger Verlauf und Fortsetzung
## Abnahmestatus
- Core-Chat automatisiert durch Codex getestet: ja
- Core-Chat manuell durch Pascal getestet: ja, am 30.07.2026
- Gemini/Datenschutz/Fallback automatisiert durch Codex getestet: ja
- Gemini/Datenschutz/Fallback manuell durch Pascal getestet: ja, am 30.07.2026
- für Merge freigegeben: ja, nach den zwei kleinen Korrekturen
- für produktive Nutzung freigegeben: nicht separat erklärt; keine spätere Tool-,
Obsidian- oder Autonomiefunktion
## Aktuelles Featurepaket
- Branch `feat/gemini-privacy-router`
- offizielle Gemini-Rahmenbedingungen geprüft; Standardmodell `gemini-3.6-flash`
- `keyring` 25.7.0 mit Windows Credential Locker als normalem Secret-Speicher
- verdeckte Secret-Befehle `set`, `status` und `delete`
- lokaler Datenschutzrouter mit `allowed`, `ask` und nicht übersteuerbarem `never`
- minimale Cloud-Kontextauswahl schließt sensible Verlaufsteile aus
- konservative Nullkosten-Defaults und harte lokale Tages-, Größen-, Timeout-
und Retry-Grenzen
- separates SQLite-Provider-Audit speichert nur Metadaten, keine Inhalte
- isolierter Gemini-Provider mit offiziellem `google-genai` 2.16.0, API `v1`,
begrenztem Timeout, höchstens einem Retry und keinem 429-Retry
- bereinigte, getrennte Fehler für fehlenden/ungültigen Key, Quote, Netzwerk,
Modellverfügbarkeit, Timeout und ungültige Antwort
- Hybridrouter erzwingt vor Cloudaufrufen lokal `allowed`/`ask`/`never`,
Aktivierung, Billing-Bestätigung, Tages-/Größenlimit und minimierten Kontext
- sichtbares lokales Fallback für fehlenden/ungültigen Key, 429, Netzwerk,
Datenschutz und lokale Grenzen; alte Ollama-Sitzungen bleiben ladbar
- lokale nicht geheime TOML-Konfiguration außerhalb von Git; konservativer
Erzeuger überschreibt keine vorhandene Datei
- `javis configure gemini` mit doppelter ausdrücklicher Bestätigung
- echter Chat auf Hybridprovider verdrahtet; Key wird verzögert geladen
- `/provider`, `/privacy` und `/status` geben keine Inhalte oder Keywerte aus
- allgemeine unpersönliche Technikbegriffe werden gezielter `allowed`, während
persönliche, interne oder geheime technische Inhalte `ask`/`never` bleiben
- `/privacy` trennt lokalen Modus von einem technischen Provider-Fallback
- sicheres `scripts/start-javis.ps1` mit relativer Repositoryprüfung,
Loopback-Ollama und überschreibbaren externen Pfaden
- 66 Unit-Tests aktuell erfolgreich; PowerShell-Syntax, Lockfile, Sync,
Strukturprüfung, Secret-Scan, Gitignore, Ruff-Format und Lint sauber
## Nicht implementiert
Obsidian-Adapter, Tools, Serverzugriff, Sprache, Mobile Client,
Living-Mind-Frontend und automatische Ollama-Prozessverwaltung.
Living-Mind-Frontend.
## Git
- Branch: `main`
- stabiler Branch: `main`
- aktueller Branch: `feat/gemini-privacy-router`
- Feature-Branch: zu `origin/feat/gemini-privacy-router` gepusht
- Grundgerüstcommit: `6d04171`
- Basis des Feature-Branches: `e6eb995`
- Toolchain: `fa90a5c`
@@ -44,8 +91,9 @@ Living-Mind-Frontend und automatische Ollama-Prozessverwaltung.
## Bekannte Einschränkung
Ollama wird bewusst manuell in einem eigenen Terminal gestartet. Antworten werden
noch nicht gestreamt; Sitzungen haben noch keine Titel, Suche oder Löschfunktion.
Das Startskript startet Ollama bei Bedarf lokal. Antworten werden noch nicht
gestreamt; Sitzungen haben noch keine Titel, Suche oder Löschfunktion.
`qwen3:8b` ist nicht automatisch für die GTX 1050 des späteren Zielhosts geeignet.
Gemini ist als separates kostenloses Folgepaket mit lokalem Datenschutzrouting
geplant, aber noch nicht implementiert oder mit einem API-Key konfiguriert.
Der Gemini-Key liegt ausschließlich im Betriebssystem-Keyring; sein Wert wurde
von Codex weder gelesen noch ausgegeben. Pascal hat den echten Cloud- und
Fallbackablauf erfolgreich geprüft.
+14 -1
View File
@@ -14,10 +14,23 @@
Secrets dürfen nie in Code, Git, Prompts, Chatverläufe oder Logs gelangen. Das Modell erfährt nur den Namen, die Verfügbarkeit und ein bereinigtes Aktionsergebnis.
Bevorzugt wird eine Secret-Provider-Abstraktion. Eine optionale lokale XML-Datei wie `key-pw.xml` oder `secrets.local.xml` ist nur als restriktiv geschützte, Git-ignorierte Klartextdatei zulässig. Ins Repository darf höchstens eine leere Beispielstruktur.
Die Secret-Provider-Abstraktion verwendet im normalen Betrieb das
Betriebssystem-Keyring, unter Windows den Credential Manager. Der Gemini-Key wird
verdeckt eingegeben, nie ausgegeben und erst unmittelbar vor einem zulässigen
Cloudaufruf gelesen. Für API-Keys wird keine XML-Secret-Datei verwendet.
Eine vorhandene SSH-Schlüsseldatei aus früherer LS25-/Gitea-Arbeit bleibt lokal. Pfad, privater Inhalt und Gerätekennung werden nicht dokumentiert.
## Cloud und Nullkostenmodus
- Datenschutzklassifizierung `allowed`, `ask` oder `never` erfolgt lokal.
- `ask` ist ohne ausdrückliches Ja abgelehnt; `never` ist nicht übersteuerbar.
- Prompts werden auf den minimal freigegebenen Kontext begrenzt.
- Gemini bleibt ohne Aktivierung, Key und lokale Nicht-Billing-Bestätigung aus.
- Javis aktiviert kein Billing und nutzt keine kostenpflichtigen Zusatzfunktionen.
- Die externe Billingfreiheit kann Javis nicht technisch garantieren; Pascal muss
sie in Google AI Studio beziehungsweise dem Google-Projekt selbst prüfen.
## Obsidian
- gesamter Vault zunächst nur lesend
+4 -1
View File
@@ -8,7 +8,10 @@ version = "0.0.0"
description = "Safe, local-first personal assistant project scaffold"
readme = "README.md"
requires-python = ">=3.11"
dependencies = []
dependencies = [
"google-genai>=2.6,<3",
"keyring>=25.7,<26",
]
[project.scripts]
javis = "javis.interface.cli:main"
+121
View File
@@ -0,0 +1,121 @@
[CmdletBinding()]
param(
[string]$UvPath,
[string]$OllamaPath,
[string]$OllamaModels,
[string]$DataDir,
[string]$UvCacheDir,
[string]$PythonInstallDir
)
$ErrorActionPreference = 'Stop'
function Get-ConfiguredPath {
param(
[string]$ParameterValue,
[string]$EnvironmentName,
[string]$DefaultValue
)
if (-not [string]::IsNullOrWhiteSpace($ParameterValue)) {
return [System.IO.Path]::GetFullPath($ParameterValue)
}
$environmentValue = [System.Environment]::GetEnvironmentVariable($EnvironmentName)
if (-not [string]::IsNullOrWhiteSpace($environmentValue)) {
return [System.IO.Path]::GetFullPath($environmentValue)
}
return [System.IO.Path]::GetFullPath($DefaultValue)
}
function Test-OllamaReady {
try {
$null = Invoke-RestMethod `
-Uri 'http://127.0.0.1:11434/api/version' `
-Method Get `
-TimeoutSec 2
return $true
}
catch {
return $false
}
}
$repositoryRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
if (-not (Test-Path -LiteralPath (Join-Path $repositoryRoot 'pyproject.toml') -PathType Leaf)) {
throw "Das Repository konnte relativ zum Startskript nicht verifiziert werden."
}
$driveRoot = [System.IO.Path]::GetPathRoot($repositoryRoot)
$resolvedUv = Get-ConfiguredPath `
-ParameterValue $UvPath `
-EnvironmentName 'JAVIS_UV_PATH' `
-DefaultValue (Join-Path $driveRoot 'Javis-Tools\uv\uv.exe')
$resolvedOllama = Get-ConfiguredPath `
-ParameterValue $OllamaPath `
-EnvironmentName 'JAVIS_OLLAMA_PATH' `
-DefaultValue (Join-Path $driveRoot 'Javis-Tools\ollama\ollama.exe')
$resolvedModels = Get-ConfiguredPath `
-ParameterValue $OllamaModels `
-EnvironmentName 'JAVIS_OLLAMA_MODELS' `
-DefaultValue (Join-Path $driveRoot 'Javis-Data\ollama-models')
$resolvedData = Get-ConfiguredPath `
-ParameterValue $DataDir `
-EnvironmentName 'JAVIS_DATA_DIR' `
-DefaultValue (Join-Path $driveRoot 'Javis-Data\runtime')
$resolvedCache = Get-ConfiguredPath `
-ParameterValue $UvCacheDir `
-EnvironmentName 'JAVIS_UV_CACHE_DIR' `
-DefaultValue (Join-Path $driveRoot 'Javis-Tools\uv-cache')
$resolvedPython = Get-ConfiguredPath `
-ParameterValue $PythonInstallDir `
-EnvironmentName 'JAVIS_PYTHON_INSTALL_DIR' `
-DefaultValue (Join-Path $driveRoot 'Javis-Tools\python')
if (-not (Test-Path -LiteralPath $resolvedUv -PathType Leaf)) {
throw "uv.exe wurde nicht gefunden: $resolvedUv"
}
if (-not (Test-Path -LiteralPath $resolvedModels -PathType Container)) {
throw "Der externe Ollama-Modellpfad wurde nicht gefunden: $resolvedModels"
}
$env:JAVIS_DATA_DIR = $resolvedData
$env:OLLAMA_MODELS = $resolvedModels
$env:OLLAMA_HOST = '127.0.0.1:11434'
$env:UV_CACHE_DIR = $resolvedCache
$env:UV_PYTHON_INSTALL_DIR = $resolvedPython
if (-not (Test-OllamaReady)) {
if (-not (Test-Path -LiteralPath $resolvedOllama -PathType Leaf)) {
throw "Ollama ist nicht erreichbar und ollama.exe wurde nicht gefunden: $resolvedOllama"
}
Write-Host "Ollama wird lokal auf 127.0.0.1:11434 gestartet ..."
$ollamaProcess = Start-Process `
-FilePath $resolvedOllama `
-ArgumentList 'serve' `
-WindowStyle Hidden `
-PassThru
$ready = $false
for ($attempt = 1; $attempt -le 30; $attempt++) {
if ($ollamaProcess.HasExited) {
throw "Ollama wurde beendet, bevor die lokale API bereit war."
}
Start-Sleep -Milliseconds 500
if (Test-OllamaReady) {
$ready = $true
break
}
}
if (-not $ready) {
throw "Ollama war nach 15 Sekunden nicht unter 127.0.0.1:11434 erreichbar."
}
}
Push-Location $repositoryRoot
try {
& $resolvedUv run javis chat
exit $LASTEXITCODE
}
finally {
Pop-Location
}
+113
View File
@@ -0,0 +1,113 @@
"""Non-secret local TOML configuration."""
from __future__ import annotations
import os
import tomllib
from collections.abc import Mapping
from pathlib import Path
class LocalConfigError(ValueError):
"""A local configuration file is malformed or unsafe to replace."""
def _mapping(value: object, section: str) -> Mapping[str, object]:
if value is None:
return {}
if not isinstance(value, dict):
raise LocalConfigError(f"TOML-Abschnitt [{section}] muss eine Tabelle sein.")
return value
def _value(values: Mapping[str, object], name: str) -> str | None:
value = values.get(name)
if value is None:
return None
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, (str, int, float)):
return str(value)
raise LocalConfigError(f"TOML-Wert '{name}' hat einen nicht unterstützten Typ.")
def load_local_defaults(path: Path) -> dict[str, str]:
"""Translate the supported non-secret TOML fields to settings inputs."""
if not path.exists():
return {}
try:
with path.open("rb") as handle:
document = tomllib.load(handle)
except (OSError, tomllib.TOMLDecodeError) as exc:
raise LocalConfigError(f"Lokale Konfiguration ist ungültig: {path}") from exc
providers = _mapping(document.get("providers"), "providers")
gemini = _mapping(providers.get("gemini"), "providers.gemini")
ollama = _mapping(providers.get("ollama"), "providers.ollama")
translated: dict[str, str] = {}
fields = (
(providers, "active", "JAVIS_PROVIDER_MODE"),
(ollama, "model", "JAVIS_MODEL"),
(ollama, "base_url", "JAVIS_OLLAMA_URL"),
(ollama, "timeout_seconds", "JAVIS_MODEL_TIMEOUT"),
(gemini, "enabled", "JAVIS_GEMINI_ENABLED"),
(gemini, "model", "JAVIS_GEMINI_MODEL"),
(gemini, "optional_model", "JAVIS_GEMINI_OPTIONAL_MODEL"),
(gemini, "free_only", "JAVIS_FREE_ONLY"),
(
gemini,
"billing_confirmed_disabled",
"JAVIS_GEMINI_BILLING_CONFIRMED_DISABLED",
),
(gemini, "max_requests_per_day", "JAVIS_MAX_CLOUD_REQUESTS_PER_DAY"),
(gemini, "max_input_chars", "JAVIS_MAX_CLOUD_INPUT_CHARS"),
(gemini, "max_output_tokens", "JAVIS_MAX_CLOUD_OUTPUT_TOKENS"),
(gemini, "timeout_seconds", "JAVIS_GEMINI_TIMEOUT"),
(gemini, "max_attempts", "JAVIS_GEMINI_MAX_ATTEMPTS"),
(gemini, "max_context_messages", "JAVIS_MAX_CLOUD_CONTEXT_MESSAGES"),
)
for table, toml_name, setting_name in fields:
value = _value(table, toml_name)
if value is not None:
translated[setting_name] = value
return translated
def write_gemini_activation(path: Path) -> None:
"""Create a conservative non-secret config without replacing existing data."""
if path.exists():
raise LocalConfigError(
f"Lokale Konfiguration existiert bereits und wurde nicht überschrieben: {path}"
)
content = """# Javis local configuration. This file must never contain secrets.
[providers]
active = "auto"
[providers.ollama]
model = "qwen3:8b"
base_url = "http://127.0.0.1:11434"
timeout_seconds = 180
[providers.gemini]
enabled = true
model = "gemini-3.6-flash"
optional_model = "gemini-3.5-flash-lite"
free_only = true
billing_confirmed_disabled = true
max_requests_per_day = 25
max_input_chars = 12000
max_output_tokens = 1024
timeout_seconds = 30
max_attempts = 2
max_context_messages = 6
"""
try:
path.parent.mkdir(parents=True, exist_ok=True)
temporary_path = path.with_suffix(path.suffix + ".tmp")
temporary_path.write_text(content, encoding="utf-8")
os.replace(temporary_path, path)
except OSError as exc:
raise LocalConfigError(
f"Lokale Konfiguration konnte nicht geschrieben werden: {path}"
) from exc
+117 -4
View File
@@ -8,11 +8,42 @@ from dataclasses import dataclass
from pathlib import Path
from urllib.parse import urlparse
from javis.config.local_config import LocalConfigError, load_local_defaults
class ConfigurationError(ValueError):
"""Raised when runtime configuration is unsafe or invalid."""
def _boolean(values: Mapping[str, str], name: str, default: bool) -> bool:
raw = values.get(name)
if raw is None:
return default
normalized = raw.strip().lower()
if normalized in {"1", "true", "yes", "on"}:
return True
if normalized in {"0", "false", "no", "off"}:
return False
raise ConfigurationError(f"{name} muss true oder false sein.")
def _positive_integer(
values: Mapping[str, str],
name: str,
default: int,
*,
maximum: int,
) -> int:
raw = values.get(name, str(default))
try:
result = int(raw)
except ValueError as exc:
raise ConfigurationError(f"{name} muss eine ganze Zahl sein.") from exc
if result <= 0 or result > maximum:
raise ConfigurationError(f"{name} muss zwischen 1 und {maximum} liegen.")
return result
def default_data_dir(
environ: Mapping[str, str] | None = None,
*,
@@ -42,35 +73,101 @@ class Settings:
model: str = "qwen3:8b"
ollama_base_url: str = "http://127.0.0.1:11434"
timeout_seconds: float = 180.0
provider_mode: str = "auto"
gemini_enabled: bool = False
gemini_model: str = "gemini-3.6-flash"
gemini_optional_model: str = "gemini-3.5-flash-lite"
free_only: bool = True
billing_confirmed_disabled: bool = False
max_cloud_requests_per_day: int = 25
max_cloud_input_chars: int = 12_000
max_cloud_output_tokens: int = 1_024
gemini_timeout_seconds: float = 30.0
gemini_max_retries: int = 1
max_cloud_context_messages: int = 6
@property
def database_path(self) -> Path:
return self.data_dir / "sessions.sqlite3"
@property
def usage_database_path(self) -> Path:
return self.data_dir / "provider-usage.sqlite3"
@property
def config_path(self) -> Path:
return self.data_dir / "javis.toml"
@classmethod
def from_env(cls, environ: Mapping[str, str] | None = None) -> Settings:
values = os.environ if environ is None else environ
configured_dir = values.get("JAVIS_DATA_DIR")
data_dir = Path(configured_dir).expanduser() if configured_dir else default_data_dir(values)
environment = os.environ if environ is None else environ
configured_dir = environment.get("JAVIS_DATA_DIR")
data_dir = (
Path(configured_dir).expanduser() if configured_dir else default_data_dir(environment)
)
if not data_dir.is_absolute():
raise ConfigurationError("JAVIS_DATA_DIR muss ein absoluter Pfad sein.")
data_dir = data_dir.resolve()
configured_file = environment.get("JAVIS_CONFIG_FILE")
config_path = (
Path(configured_file).expanduser() if configured_file else data_dir / "javis.toml"
)
if not config_path.is_absolute():
raise ConfigurationError("JAVIS_CONFIG_FILE muss ein absoluter Pfad sein.")
try:
values = {**load_local_defaults(config_path.resolve()), **environment}
except LocalConfigError as exc:
raise ConfigurationError(str(exc)) from exc
provider = values.get("JAVIS_PROVIDER", "ollama").strip()
model = values.get("JAVIS_MODEL", "qwen3:8b").strip()
base_url = values.get("JAVIS_OLLAMA_URL", "http://127.0.0.1:11434").strip()
timeout_raw = values.get("JAVIS_MODEL_TIMEOUT", "180")
provider_mode = values.get("JAVIS_PROVIDER_MODE", "auto").strip().lower()
gemini_enabled = _boolean(values, "JAVIS_GEMINI_ENABLED", False)
free_only = _boolean(values, "JAVIS_FREE_ONLY", True)
billing_disabled = _boolean(values, "JAVIS_GEMINI_BILLING_CONFIRMED_DISABLED", False)
gemini_timeout_raw = values.get("JAVIS_GEMINI_TIMEOUT", "30")
try:
timeout = float(timeout_raw)
except ValueError as exc:
raise ConfigurationError("JAVIS_MODEL_TIMEOUT muss eine Zahl sein.") from exc
try:
gemini_timeout = float(gemini_timeout_raw)
except ValueError as exc:
raise ConfigurationError("JAVIS_GEMINI_TIMEOUT muss eine Zahl sein.") from exc
settings = cls(
data_dir=data_dir.resolve(),
data_dir=data_dir,
provider=provider,
model=model,
ollama_base_url=base_url,
timeout_seconds=timeout,
provider_mode=provider_mode,
gemini_enabled=gemini_enabled,
gemini_model=values.get("JAVIS_GEMINI_MODEL", "gemini-3.6-flash").strip(),
gemini_optional_model=values.get(
"JAVIS_GEMINI_OPTIONAL_MODEL", "gemini-3.5-flash-lite"
).strip(),
free_only=free_only,
billing_confirmed_disabled=billing_disabled,
max_cloud_requests_per_day=_positive_integer(
values, "JAVIS_MAX_CLOUD_REQUESTS_PER_DAY", 25, maximum=1_000
),
max_cloud_input_chars=_positive_integer(
values, "JAVIS_MAX_CLOUD_INPUT_CHARS", 12_000, maximum=1_000_000
),
max_cloud_output_tokens=_positive_integer(
values, "JAVIS_MAX_CLOUD_OUTPUT_TOKENS", 1_024, maximum=65_536
),
gemini_timeout_seconds=gemini_timeout,
gemini_max_retries=_positive_integer(values, "JAVIS_GEMINI_MAX_ATTEMPTS", 2, maximum=2)
- 1,
max_cloud_context_messages=_positive_integer(
values, "JAVIS_MAX_CLOUD_CONTEXT_MESSAGES", 6, maximum=50
),
)
settings.validate()
return settings
@@ -84,6 +181,22 @@ class Settings:
raise ConfigurationError("JAVIS_MODEL darf nicht leer sein.")
if self.timeout_seconds <= 0:
raise ConfigurationError("JAVIS_MODEL_TIMEOUT muss größer als 0 sein.")
if self.provider_mode not in {"auto", "local", "gemini"}:
raise ConfigurationError("JAVIS_PROVIDER_MODE muss auto, local oder gemini sein.")
if not self.free_only:
raise ConfigurationError(
"Kostenpflichtiger Betrieb wird nicht unterstützt; JAVIS_FREE_ONLY muss true sein."
)
if self.gemini_model != "gemini-3.6-flash":
raise ConfigurationError(
"In diesem Paket ist nur das geprüfte Standardmodell gemini-3.6-flash aktiv."
)
if not self.gemini_optional_model:
raise ConfigurationError("JAVIS_GEMINI_OPTIONAL_MODEL darf nicht leer sein.")
if self.gemini_timeout_seconds <= 0 or self.gemini_timeout_seconds > 60:
raise ConfigurationError(
"JAVIS_GEMINI_TIMEOUT muss größer als 0 und höchstens 60 sein."
)
parsed = urlparse(self.ollama_base_url)
if parsed.scheme != "http" or parsed.hostname not in {
+7 -1
View File
@@ -35,7 +35,13 @@ class ChatService:
def load_session(self, session_id: str) -> LoadedSession:
session = self.store.get_session(session_id)
if session.provider != self.provider.name or session.model != self.provider.model:
supports_session = getattr(self.provider, "supports_session", None)
compatible = (
supports_session(session.provider, session.model)
if callable(supports_session)
else session.provider == self.provider.name and session.model == self.provider.model
)
if not compatible:
raise SessionProviderMismatchError(
"Die Sitzung verwendet "
f"{session.provider}/{session.model}, aktiv ist "
+252
View File
@@ -0,0 +1,252 @@
"""Local privacy and zero-cost routing between Gemini and Ollama."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from javis.memory.usage_store import ProviderEvent, SQLiteUsageStore, UsageStoreError
from javis.providers.base import (
ChatMessage,
InvalidApiKeyError,
LocalModelProvider,
MissingApiKeyError,
ProviderError,
ProviderUsage,
)
from javis.security.privacy import CloudPolicy, PrivacyDecision, PrivacyRouter
ApprovalCallback = Callable[[PrivacyDecision], bool]
NoticeCallback = Callable[[str], None]
CloudProviderFactory = Callable[[], LocalModelProvider]
@dataclass(frozen=True, slots=True)
class RouteStatus:
provider: str
privacy_policy: CloudPolicy
fallback: bool
cloud_suppressed_by_local_mode: bool
technical_fallback: bool
reason: str
class HybridProvider:
"""Provider contract that enforces privacy before any cloud construction."""
name = "hybrid"
model = "auto"
def __init__(
self,
*,
local_provider: LocalModelProvider,
cloud_provider_factory: CloudProviderFactory,
cloud_model: str,
privacy_router: PrivacyRouter,
usage_store: SQLiteUsageStore,
approval_callback: ApprovalCallback,
notice_callback: NoticeCallback,
mode: str,
cloud_enabled: bool,
billing_confirmed_disabled: bool,
free_only: bool,
max_cloud_requests_per_day: int,
max_cloud_input_chars: int,
max_cloud_context_messages: int,
) -> None:
if mode not in {"auto", "local", "gemini"}:
raise ValueError("Unbekannter Provider-Modus.")
if not free_only:
raise ValueError("Der Hybridprovider unterstützt ausschließlich den Nullkostenmodus.")
self.local_provider = local_provider
self._cloud_provider_factory = cloud_provider_factory
self._cloud_model = cloud_model
self._privacy_router = privacy_router
self._usage_store = usage_store
self._approval_callback = approval_callback
self._notice_callback = notice_callback
self.mode = mode
self._cloud_enabled = cloud_enabled
self._billing_confirmed_disabled = billing_confirmed_disabled
self._max_cloud_requests_per_day = max_cloud_requests_per_day
self._max_cloud_input_chars = max_cloud_input_chars
self._max_cloud_context_messages = max_cloud_context_messages
self._cloud_session_disabled = False
self.last_route: RouteStatus | None = None
def supports_session(self, provider: str, model: str) -> bool:
return (provider, model) in {
(self.name, self.model),
(self.local_provider.name, self.local_provider.model),
}
def set_mode(self, mode: str) -> None:
if mode not in {"auto", "local", "gemini"}:
raise ValueError("Provider-Modus muss auto, local oder gemini sein.")
self.mode = mode
def chat(self, messages: list[ChatMessage]) -> str:
current_text = messages[-1].content if messages else ""
cloud_requested = self.mode == "gemini"
decision = self._privacy_router.classify(
current_text,
cloud_requested=cloud_requested,
)
if self.mode == "local":
return self._local(
messages,
decision,
"lokaler Modus",
fallback=False,
cloud_suppressed_by_local_mode=True,
)
if decision.policy is CloudPolicy.NEVER:
return self._local(messages, decision, decision.reason, fallback=True)
approved = decision.policy is CloudPolicy.ALLOWED
if decision.policy is CloudPolicy.ASK:
approved = self._approval_callback(decision)
if not approved:
return self._local(messages, decision, "Cloudfreigabe abgelehnt", fallback=True)
unavailable_reason = self._cloud_unavailable_reason()
if unavailable_reason:
return self._local(messages, decision, unavailable_reason, fallback=True)
if len(current_text) > self._max_cloud_input_chars:
return self._local(
messages,
decision,
"Nachricht überschreitet das lokale Cloud-Größenlimit",
fallback=True,
)
cloud_messages = self._privacy_router.minimal_context(
messages,
current_policy=decision.policy,
approved=approved,
max_chars=self._max_cloud_input_chars,
max_messages=self._max_cloud_context_messages,
)
if not cloud_messages:
return self._local(messages, decision, "kein freigegebener Cloudkontext", fallback=True)
try:
cloud_provider = self._cloud_provider_factory()
response = cloud_provider.chat(cloud_messages)
except InvalidApiKeyError as exc:
self._cloud_session_disabled = True
return self._cloud_failure(messages, decision, exc, disable_notice=True)
except (MissingApiKeyError, ProviderError) as exc:
return self._cloud_failure(messages, decision, exc)
usage = getattr(cloud_provider, "last_usage", ProviderUsage())
self._record(
ProviderEvent(
provider=cloud_provider.name,
model=cloud_provider.model,
success=True,
error_category=None,
fallback=False,
privacy_policy=decision.policy.value,
input_tokens=usage.input_tokens,
output_tokens=usage.output_tokens,
)
)
self.last_route = RouteStatus(
provider=cloud_provider.name,
privacy_policy=decision.policy,
fallback=False,
cloud_suppressed_by_local_mode=False,
technical_fallback=False,
reason="Cloudaufruf erfolgreich",
)
return response
def _cloud_unavailable_reason(self) -> str | None:
if not self._cloud_enabled:
return "Gemini ist lokal nicht aktiviert"
if not self._billing_confirmed_disabled:
return "fehlende lokale Bestätigung, dass Billing deaktiviert ist"
if self._cloud_session_disabled:
return "Gemini ist nach einem Schlüssel-Fehler für diese Sitzung deaktiviert"
try:
cloud_requests_today = self._usage_store.cloud_requests_on()
except UsageStoreError:
return "lokale Cloudlimit-Prüfung nicht verfügbar"
if cloud_requests_today >= self._max_cloud_requests_per_day:
return "lokales tägliches Cloudlimit erreicht"
return None
def _cloud_failure(
self,
messages: list[ChatMessage],
decision: PrivacyDecision,
error: ProviderError,
*,
disable_notice: bool = False,
) -> str:
error_category = type(error).__name__
self._record(
ProviderEvent(
provider="gemini",
model=self._cloud_model,
success=False,
error_category=error_category,
fallback=False,
privacy_policy=decision.policy.value,
)
)
reason = "Gemini nicht verfügbar"
if disable_notice:
reason = "Gemini-Schlüssel abgelehnt; Cloud für diese Sitzung deaktiviert"
return self._local(
messages,
decision,
reason,
fallback=True,
technical_fallback=True,
)
def _local(
self,
messages: list[ChatMessage],
decision: PrivacyDecision,
reason: str,
*,
fallback: bool,
cloud_suppressed_by_local_mode: bool = False,
technical_fallback: bool = False,
) -> str:
if fallback:
self._notice_callback(f"{reason} lokale Antwort mit Ollama.")
response = self.local_provider.chat(messages)
self._record(
ProviderEvent(
provider=self.local_provider.name,
model=self.local_provider.model,
success=True,
error_category=None,
fallback=fallback,
privacy_policy=decision.policy.value,
)
)
self.last_route = RouteStatus(
provider=self.local_provider.name,
privacy_policy=decision.policy,
fallback=fallback,
cloud_suppressed_by_local_mode=cloud_suppressed_by_local_mode,
technical_fallback=technical_fallback,
reason=reason,
)
return response
def _record(self, event: ProviderEvent) -> None:
try:
self._usage_store.record(event)
except UsageStoreError:
self._notice_callback(
"Warnung: Provider-Nutzungsmetadaten konnten nicht gespeichert werden."
)
+230 -8
View File
@@ -3,28 +3,41 @@
from __future__ import annotations
import argparse
import getpass
import sys
from collections.abc import Callable, Sequence
from pathlib import Path
from javis.config.local_config import LocalConfigError, write_gemini_activation
from javis.config.settings import ConfigurationError, Settings
from javis.core.chat_service import ChatService, SessionProviderMismatchError
from javis.core.provider_router import HybridProvider
from javis.memory.sqlite_store import (
ChatSession,
SessionNotFoundError,
SessionStoreError,
SQLiteSessionStore,
)
from javis.providers.base import ProviderError
from javis.memory.usage_store import SQLiteUsageStore, UsageStoreError
from javis.providers.base import MissingApiKeyError, ProviderError
from javis.providers.gemini import GeminiProvider
from javis.providers.ollama import OllamaProvider
from javis.security.privacy import PrivacyDecision, PrivacyRouter
from javis.security.secrets import SecretProvider, SecretStoreError
InputFunction = Callable[[str], str]
OutputFunction = Callable[[str], None]
StatusFunction = Callable[[], list[str]]
HELP_TEXT = """Befehle:
/new neue Sitzung beginnen
/sessions gespeicherte Sitzungen auflisten
/load <ID> frühere Sitzung laden und fortsetzen
/clear Nachrichten der aktiven Sitzung leeren
/provider aktiven Providermodus anzeigen
/provider <MODUS> Modus auto, local oder gemini setzen
/privacy letzte lokale Datenschutzentscheidung anzeigen
/status bereinigten Providerstatus anzeigen
/help diese Hilfe anzeigen
/exit Chat ordentlich beenden"""
@@ -34,9 +47,82 @@ def build_parser() -> argparse.ArgumentParser:
subparsers = parser.add_subparsers(dest="command", required=True)
chat_parser = subparsers.add_parser("chat", help="lokalen Textchat starten")
chat_parser.add_argument("--session", help="vorhandene Sitzungs-ID laden")
configure_parser = subparsers.add_parser(
"configure",
help="nicht geheime lokale Einstellungen einrichten",
)
configure_parser.add_argument("provider_name", choices=["gemini"])
secrets_parser = subparsers.add_parser("secrets", help="lokale Geheimwerte sicher verwalten")
secret_subparsers = secrets_parser.add_subparsers(dest="secret_action", required=True)
set_parser = secret_subparsers.add_parser("set", help="Geheimwert verdeckt setzen")
set_parser.add_argument("secret_name", choices=["gemini"])
secret_subparsers.add_parser("status", help="nur Verfügbarkeit anzeigen")
delete_parser = secret_subparsers.add_parser("delete", help="Geheimwert löschen")
delete_parser.add_argument("secret_name", choices=["gemini"])
return parser
def _confirmed(answer: str) -> bool:
return answer.strip().lower() in {"j", "ja", "y", "yes"}
def run_gemini_configuration(
config_path: Path,
*,
input_fn: InputFunction = input,
output: OutputFunction = print,
) -> int:
if not _confirmed(input_fn("Gemini Free Tier lokal aktivieren? [j/N] ")):
output("Gemini bleibt deaktiviert.")
return 1
if not _confirmed(
input_fn("Bestätigst du, dass für dieses Google-Projekt kein Billing aktiviert ist? [j/N] ")
):
output("Gemini bleibt deaktiviert; keine Konfiguration wurde geschrieben.")
return 1
try:
write_gemini_activation(config_path)
except LocalConfigError as exc:
output(f"Konfigurationsfehler: {exc}")
return 2
output(f"Gemini-Free-Konfiguration gespeichert: {config_path}")
output("Nächster Schritt: javis secrets set gemini")
return 0
def run_secrets(
action: str,
*,
provider: SecretProvider | None = None,
password_fn: Callable[[str], str] = getpass.getpass,
output: OutputFunction = print,
) -> int:
secret_provider = provider or SecretProvider()
try:
if action == "status":
state = "verfügbar" if secret_provider.gemini_key_available() else "nicht verfügbar"
output(f"Gemini-Schlüssel: {state}")
return 0
if action == "set":
value = password_fn("Gemini API-Key: ")
secret_provider.set_gemini_key(value)
output("Gemini-Schlüssel wurde sicher gespeichert.")
return 0
if action == "delete":
deleted = secret_provider.delete_gemini_key()
output(
"Gemini-Schlüssel wurde gelöscht."
if deleted
else "Gemini-Schlüssel war nicht verfügbar."
)
return 0
except SecretStoreError as exc:
output(f"Fehler: {exc}")
return 2
output("Unbekannte Secret-Aktion.")
return 2
def _show_history(service: ChatService, session_id: str, output: OutputFunction) -> None:
loaded = service.load_session(session_id)
if not loaded.messages:
@@ -64,6 +150,7 @@ def run_chat(
session_id: str | None = None,
input_fn: InputFunction = input,
output: OutputFunction = print,
status_fn: StatusFunction | None = None,
) -> int:
try:
if session_id:
@@ -118,6 +205,42 @@ def run_chat(
except (SessionNotFoundError, SessionStoreError) as exc:
output(f"Fehler: {exc}")
continue
if entered.startswith("/provider"):
parts = entered.split()
provider = service.provider
if len(parts) == 1:
output(f"Aktiver Providermodus: {getattr(provider, 'mode', 'local')}")
continue
if len(parts) != 2 or parts[1] not in {"auto", "local", "gemini"}:
output("Verwendung: /provider auto|local|gemini")
continue
set_mode = getattr(provider, "set_mode", None)
if not callable(set_mode):
output("Dieser Chat unterstützt keinen wechselbaren Providermodus.")
continue
set_mode(parts[1])
output(f"Providermodus: {parts[1]}")
continue
if entered == "/privacy":
route = getattr(service.provider, "last_route", None)
if route is None:
output("Noch keine Datenschutzentscheidung in dieser Sitzung.")
else:
output(
"Datenschutzentscheidung: "
f"{route.privacy_policy.value}; Provider: {route.provider}; "
"Cloud durch Modus local unterdrückt: "
f"{'ja' if route.cloud_suppressed_by_local_mode else 'nein'}; "
f"Technischer Fallback: {'ja' if route.technical_fallback else 'nein'}"
)
continue
if entered == "/status":
if status_fn is None:
output("Keine erweiterte Statusprüfung verfügbar.")
else:
for line in status_fn():
output(line)
continue
if entered.startswith("/load"):
parts = entered.split(maxsplit=1)
if len(parts) != 2 or not parts[1].strip():
@@ -148,6 +271,88 @@ def run_chat(
output(f"Fehler: {exc}")
def build_runtime(
settings: Settings,
*,
input_fn: InputFunction,
output: OutputFunction,
secret_provider: SecretProvider | None = None,
) -> tuple[ChatService, StatusFunction]:
store = SQLiteSessionStore(settings.database_path)
usage_store = SQLiteUsageStore(settings.usage_database_path)
secrets = secret_provider or SecretProvider()
local = OllamaProvider(
settings.model,
settings.ollama_base_url,
settings.timeout_seconds,
)
def approve(decision: PrivacyDecision) -> bool:
answer = input_fn(
f"Cloudfreigabe erforderlich ({decision.reason}). Einmalig erlauben? [j/N] "
)
return _confirmed(answer)
def create_cloud_provider() -> GeminiProvider:
try:
key = secrets.get_gemini_key()
except SecretStoreError as exc:
raise MissingApiKeyError(
"Der Gemini-Schlüssel konnte nicht sicher geladen werden."
) from exc
return GeminiProvider(
model=settings.gemini_model,
api_key=key,
timeout_seconds=settings.gemini_timeout_seconds,
max_output_tokens=settings.max_cloud_output_tokens,
max_retries=settings.gemini_max_retries,
)
hybrid = HybridProvider(
local_provider=local,
cloud_provider_factory=create_cloud_provider,
cloud_model=settings.gemini_model,
privacy_router=PrivacyRouter(),
usage_store=usage_store,
approval_callback=approve,
notice_callback=output,
mode=settings.provider_mode,
cloud_enabled=settings.gemini_enabled,
billing_confirmed_disabled=settings.billing_confirmed_disabled,
free_only=settings.free_only,
max_cloud_requests_per_day=settings.max_cloud_requests_per_day,
max_cloud_input_chars=settings.max_cloud_input_chars,
max_cloud_context_messages=settings.max_cloud_context_messages,
)
def status() -> list[str]:
ollama = local.probe()
try:
key_available = secrets.gemini_key_available()
except SecretStoreError:
key_available = False
gemini_configured = (
settings.gemini_enabled and settings.free_only and settings.billing_confirmed_disabled
)
try:
cloud_requests = usage_store.cloud_requests_on()
local_fallbacks = usage_store.local_fallbacks_on()
except UsageStoreError:
cloud_requests = local_fallbacks = -1
return [
f"Ollama erreichbar: {'ja' if ollama.reachable else 'nein'}",
f"Lokales Modell vorhanden: {'ja' if ollama.model_available else 'nein'}",
f"Gemini-Schlüssel vorhanden: {'ja' if key_available else 'nein'}",
f"Gemini konfiguriert: {'ja' if gemini_configured else 'nein'}",
f"Nullkostenmodus: {'aktiv' if settings.free_only else 'inaktiv'}",
f"Aktiver Providermodus: {hybrid.mode}",
f"Heutige Cloudanfragen: {cloud_requests if cloud_requests >= 0 else 'unbekannt'}",
f"Heutige lokale Fallbacks: {local_fallbacks if local_fallbacks >= 0 else 'unbekannt'}",
]
return ChatService(store, hybrid), status
def main(
argv: Sequence[str] | None = None,
*,
@@ -155,26 +360,43 @@ def main(
output: OutputFunction = print,
) -> int:
args = build_parser().parse_args(argv)
if args.command == "secrets":
return run_secrets(
args.secret_action,
password_fn=input_fn if input_fn is not input else getpass.getpass,
output=output,
)
if args.command == "configure":
try:
settings = Settings.from_env()
except ConfigurationError as exc:
output(f"Konfigurationsfehler: {exc}")
return 2
return run_gemini_configuration(
settings.config_path,
input_fn=input_fn,
output=output,
)
if args.command != "chat":
return 2
try:
settings = Settings.from_env()
store = SQLiteSessionStore(settings.database_path)
provider = OllamaProvider(
settings.model,
settings.ollama_base_url,
settings.timeout_seconds,
service, status_fn = build_runtime(
settings,
input_fn=input_fn,
output=output,
)
except (ConfigurationError, SessionStoreError) as exc:
except (ConfigurationError, SessionStoreError, UsageStoreError) as exc:
output(f"Konfigurationsfehler: {exc}")
return 2
return run_chat(
ChatService(store, provider),
service,
session_id=args.session,
input_fn=input_fn,
output=output,
status_fn=status_fn,
)
+114
View File
@@ -0,0 +1,114 @@
"""Metadata-only provider audit and local usage limits."""
from __future__ import annotations
import sqlite3
from contextlib import closing
from dataclasses import dataclass
from datetime import UTC, date, datetime
from pathlib import Path
class UsageStoreError(RuntimeError):
"""Provider metadata could not be stored or read."""
@dataclass(frozen=True, slots=True)
class ProviderEvent:
provider: str
model: str
success: bool
error_category: str | None
fallback: bool
privacy_policy: str
input_tokens: int | None = None
output_tokens: int | None = None
class SQLiteUsageStore:
def __init__(self, database_path: Path) -> None:
self.database_path = database_path
try:
self.database_path.parent.mkdir(parents=True, exist_ok=True)
with closing(self._connect()) as connection:
connection.execute(
"""
CREATE TABLE IF NOT EXISTS provider_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
occurred_at TEXT NOT NULL,
provider TEXT NOT NULL,
model TEXT NOT NULL,
success INTEGER NOT NULL,
error_category TEXT,
fallback INTEGER NOT NULL,
privacy_policy TEXT NOT NULL,
input_tokens INTEGER,
output_tokens INTEGER
)
"""
)
connection.execute(
"""
CREATE INDEX IF NOT EXISTS idx_provider_events_time_provider
ON provider_events(occurred_at, provider)
"""
)
connection.commit()
except (OSError, sqlite3.Error) as exc:
raise UsageStoreError(
f"Die Nutzungsmetadatenbank kann nicht geöffnet werden: {database_path}"
) from exc
def _connect(self) -> sqlite3.Connection:
return sqlite3.connect(self.database_path, timeout=10)
def record(self, event: ProviderEvent) -> None:
try:
with closing(self._connect()) as connection:
connection.execute(
"""
INSERT INTO provider_events (
occurred_at, provider, model, success, error_category,
fallback, privacy_policy, input_tokens, output_tokens
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
datetime.now(UTC).isoformat(timespec="seconds"),
event.provider,
event.model,
int(event.success),
event.error_category,
int(event.fallback),
event.privacy_policy,
event.input_tokens,
event.output_tokens,
),
)
connection.commit()
except sqlite3.Error as exc:
raise UsageStoreError("Nutzungsmetadaten konnten nicht gespeichert werden.") from exc
def cloud_requests_on(self, day: date | None = None) -> int:
return self._count(day or datetime.now(UTC).date(), "provider = 'gemini'")
def local_fallbacks_on(self, day: date | None = None) -> int:
return self._count(
day or datetime.now(UTC).date(),
"provider = 'ollama' AND fallback = 1",
)
def _count(self, day: date, condition: str) -> int:
start = f"{day.isoformat()}T00:00:00+00:00"
end = f"{day.isoformat()}T23:59:59+00:00"
try:
with closing(self._connect()) as connection:
row = connection.execute(
f"""
SELECT COUNT(*) FROM provider_events
WHERE occurred_at BETWEEN ? AND ? AND {condition}
""",
(start, end),
).fetchone()
except sqlite3.Error as exc:
raise UsageStoreError("Nutzungsmetadaten konnten nicht gelesen werden.") from exc
return int(row[0])
+30 -4
View File
@@ -12,12 +12,18 @@ class ChatMessage:
content: str
@dataclass(frozen=True, slots=True)
class ProviderUsage:
input_tokens: int | None = None
output_tokens: int | None = None
class ProviderError(RuntimeError):
"""Base class for expected local provider failures."""
"""Base class for expected provider failures."""
class ProviderUnavailableError(ProviderError):
"""The local model runtime cannot be reached."""
"""The selected model runtime cannot be reached."""
class ModelNotInstalledError(ProviderError):
@@ -25,7 +31,7 @@ class ModelNotInstalledError(ProviderError):
class ProviderTimeoutError(ProviderError):
"""The local model did not respond in time."""
"""The model did not respond in time."""
class ResponseAbortedError(ProviderError):
@@ -33,7 +39,27 @@ class ResponseAbortedError(ProviderError):
class InvalidProviderResponseError(ProviderError):
"""The local runtime returned an unusable response."""
"""The runtime returned an unusable response."""
class MissingApiKeyError(ProviderError):
"""No API key is available for the selected cloud provider."""
class InvalidApiKeyError(ProviderError):
"""The cloud provider rejected its API key."""
class ProviderRateLimitError(ProviderError):
"""The free provider quota or rate limit is exhausted."""
class CloudNetworkError(ProviderError):
"""A cloud request failed at the network layer."""
class CloudModelUnavailableError(ProviderError):
"""The configured cloud model is unavailable."""
@runtime_checkable
+128
View File
@@ -0,0 +1,128 @@
"""Gemini provider using Google's official GA Python SDK."""
from __future__ import annotations
from collections.abc import Callable
from typing import Any
import httpx
from google import genai
from google.genai import errors, types
from javis.providers.base import (
ChatMessage,
CloudModelUnavailableError,
CloudNetworkError,
InvalidApiKeyError,
InvalidProviderResponseError,
MissingApiKeyError,
ProviderError,
ProviderRateLimitError,
ProviderTimeoutError,
ProviderUnavailableError,
ProviderUsage,
)
ClientFactory = Callable[..., Any]
class GeminiProvider:
name = "gemini"
def __init__(
self,
*,
model: str,
api_key: str | None,
timeout_seconds: float,
max_output_tokens: int,
max_retries: int,
client_factory: ClientFactory = genai.Client,
) -> None:
if not api_key:
raise MissingApiKeyError("Für Gemini ist kein API-Schlüssel hinterlegt.")
if max_retries not in {0, 1}:
raise ValueError("Gemini darf höchstens einen Wiederholungsversuch verwenden.")
self.model = model
self.last_usage = ProviderUsage()
self._config = types.GenerateContentConfig(
candidate_count=1,
max_output_tokens=max_output_tokens,
)
self._client = client_factory(
api_key=api_key,
http_options=types.HttpOptions(
api_version="v1",
timeout=max(1, round(timeout_seconds * 1_000)),
retry_options=types.HttpRetryOptions(
attempts=max_retries + 1,
http_status_codes=[408, 500, 502, 503, 504],
),
),
)
def chat(self, messages: list[ChatMessage]) -> str:
contents = [
types.Content(
role="model" if message.role == "assistant" else "user",
parts=[types.Part.from_text(text=message.content)],
)
for message in messages
]
self.last_usage = ProviderUsage()
try:
response = self._client.models.generate_content(
model=self.model,
contents=contents,
config=self._config,
)
except errors.APIError as exc:
self._raise_api_error(exc)
except httpx.TimeoutException as exc:
raise ProviderTimeoutError("Gemini hat nicht rechtzeitig geantwortet.") from exc
except httpx.NetworkError as exc:
raise CloudNetworkError(
"Gemini ist wegen eines Netzwerkfehlers nicht erreichbar."
) from exc
except (OSError, ConnectionError) as exc:
raise CloudNetworkError(
"Gemini ist wegen eines Netzwerkfehlers nicht erreichbar."
) from exc
text = getattr(response, "text", None)
if not isinstance(text, str) or not text.strip():
raise InvalidProviderResponseError(
"Gemini hat keine verwendbare Textantwort geliefert."
)
usage = getattr(response, "usage_metadata", None)
self.last_usage = ProviderUsage(
input_tokens=self._token_count(usage, "prompt_token_count"),
output_tokens=self._token_count(usage, "candidates_token_count"),
)
return text.strip()
@staticmethod
def _token_count(usage: object, name: str) -> int | None:
value = getattr(usage, name, None)
return value if isinstance(value, int) and value >= 0 else None
@staticmethod
def _raise_api_error(exc: errors.APIError) -> None:
if exc.code in {401, 403}:
raise InvalidApiKeyError("Der Gemini-API-Schlüssel wurde abgelehnt.") from exc
if exc.code == 429:
raise ProviderRateLimitError(
"Das kostenlose Gemini-Kontingent ist vorübergehend ausgeschöpft."
) from exc
if exc.code == 404:
raise CloudModelUnavailableError(
"Das konfigurierte Gemini-Modell ist nicht verfügbar."
) from exc
if exc.code == 408:
raise ProviderTimeoutError("Gemini hat nicht rechtzeitig geantwortet.") from exc
if exc.code >= 500:
raise ProviderUnavailableError("Gemini ist vorübergehend nicht verfügbar.") from exc
raise ProviderError(f"Gemini-Anfrage fehlgeschlagen (HTTP {exc.code}).") from exc
+28 -1
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import json
import socket
from dataclasses import dataclass
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
@@ -17,14 +18,40 @@ from javis.providers.base import (
)
@dataclass(frozen=True, slots=True)
class OllamaStatus:
reachable: bool
model_available: bool
class OllamaProvider:
name = "ollama"
def __init__(self, model: str, base_url: str, timeout_seconds: float) -> None:
self.model = model
self._endpoint = f"{base_url.rstrip('/')}/api/chat"
self._base_url = base_url.rstrip("/")
self._endpoint = f"{self._base_url}/api/chat"
self._timeout_seconds = timeout_seconds
def probe(self) -> OllamaStatus:
request = Request(f"{self._base_url}/api/tags", method="GET")
try:
with urlopen(request, timeout=min(self._timeout_seconds, 3)) as response:
result = json.loads(response.read())
except (HTTPError, URLError, TimeoutError, OSError, json.JSONDecodeError):
return OllamaStatus(False, False)
models = result.get("models")
if not isinstance(models, list):
return OllamaStatus(True, False)
names = {
value
for item in models
if isinstance(item, dict)
for value in (item.get("name"), item.get("model"))
if isinstance(value, str)
}
return OllamaStatus(True, self.model in names)
def chat(self, messages: list[ChatMessage]) -> str:
payload = {
"model": self.model,
+154
View File
@@ -0,0 +1,154 @@
"""Conservative local privacy classification for cloud routing."""
from __future__ import annotations
import re
from dataclasses import dataclass
from enum import StrEnum
from javis.providers.base import ChatMessage
class CloudPolicy(StrEnum):
ALLOWED = "allowed"
ASK = "ask"
NEVER = "never"
@dataclass(frozen=True, slots=True)
class PrivacyDecision:
policy: CloudPolicy
reason: str
_NEVER_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
(
re.compile(
r"(?i)\b(api[-_ ]?key|passwort|password|access[-_ ]?token|refresh[-_ ]?token|"
r"session[-_ ]?cookie|private[rn]? ssh[-_ ]?schl[üu]ssel)\b"
),
"mögliches Secret",
),
(
re.compile(r"(?i)(AIza[\w-]{20,}|sk-[\w-]{16,}|BEGIN [A-Z ]*PRIVATE KEY)"),
"Schlüsselmuster",
),
(
re.compile(
r"(?i)\b(iban|bic|kreditkart|bankkonto|bankdaten|zahlungsdaten|"
r"kontonummer|pin[- ]?code)\b"
),
"Finanz- oder Zahlungsdaten",
),
(
re.compile(
r"(?i)\b(diagnose|gesundheit|krankheit|medikament|patient|arztbericht|"
r"psychotherapie|blutwert)\b"
),
"Gesundheitsinformation",
),
(
re.compile(
r"(?i)\b(genaue anschrift|wohnanschrift|private[rs]? dokument|"
r"vollständige[rs]? personenprofil|rohe? obsidian|nur lokal|"
r"ausschließlich lokal)\b"
),
"ausdrücklich lokal oder besonders sensibel",
),
)
_ASK_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
(
re.compile(
r"(?i)\b(meine frau|mein mann|meine familie|mein kind|meine mutter|"
r"mein vater|meine beziehung|über mich|über pascal)\b"
),
"persönliche oder familiäre Information",
),
(
re.compile(r"(?i)\b[\w.+-]+@[\w.-]+\.[a-z]{2,}\b"),
"E-Mail-Adresse",
),
(
re.compile(r"(?<!\w)(?:\+?\d[\d ()/-]{7,}\d)(?!\w)"),
"mögliche Telefonnummer",
),
(
re.compile(
r"(?i)\b(intern(?:e|er|es|en)?\s+"
r"(?:hostname|server|domain|infrastruktur)|rootserver|"
r"privates? projekt|nicht öffentlich|systeminformation|"
r"private[rs]? termin)\b"
),
"nicht öffentliche Infrastruktur oder Planung",
),
(
re.compile(r"(?i)\b(ich|mein(?:e[rsn]?|em)?|mir|mich)\b"),
"möglicher persönlicher Bezug",
),
)
_ALLOWED_PATTERNS: tuple[re.Pattern[str], ...] = (
re.compile(
r"(?i)\b(python|programmier|quellcode|algorithmus|sqlite|linux|windows|"
r"git|ram|arbeitsspeicher|ssd|solid state drive|http|statuscode|"
r"transaktion|öffentliche dokumentation|allgemeine wissensfrage|"
r"was ist|wie funktioniert)\b"
),
)
class PrivacyRouter:
"""Rule-based, entirely local and intentionally conservative."""
def classify(self, text: str, *, cloud_requested: bool = False) -> PrivacyDecision:
normalized = text.strip()
for pattern, reason in _NEVER_PATTERNS:
if pattern.search(normalized):
return PrivacyDecision(CloudPolicy.NEVER, reason)
for pattern, reason in _ASK_PATTERNS:
if pattern.search(normalized):
return PrivacyDecision(CloudPolicy.ASK, reason)
if any(pattern.search(normalized) for pattern in _ALLOWED_PATTERNS):
return PrivacyDecision(CloudPolicy.ALLOWED, "allgemeiner oder technischer Inhalt")
if cloud_requested:
return PrivacyDecision(
CloudPolicy.ASK,
"expliziter Cloudwunsch bei uneindeutigem Inhalt",
)
return PrivacyDecision(CloudPolicy.ASK, "Inhalt nicht eindeutig klassifizierbar")
def minimal_context(
self,
messages: list[ChatMessage],
*,
current_policy: CloudPolicy,
approved: bool,
max_chars: int,
max_messages: int,
) -> list[ChatMessage]:
if current_policy is CloudPolicy.NEVER:
return []
if current_policy is CloudPolicy.ASK and not approved:
return []
selected: list[ChatMessage] = []
remaining = max_chars
for index in range(len(messages) - 1, max(-1, len(messages) - max_messages - 1), -1):
message = messages[index]
decision = self.classify(message.content)
is_current = index == len(messages) - 1
may_include = decision.policy is CloudPolicy.ALLOWED or (
is_current and current_policy is CloudPolicy.ASK and approved
)
if not may_include:
continue
content = message.content[:remaining]
if not content:
break
selected.append(ChatMessage(message.role, content))
remaining -= len(content)
if remaining <= 0:
break
selected.reverse()
return selected
+74
View File
@@ -0,0 +1,74 @@
"""Secret storage backed by the operating system credential store."""
from __future__ import annotations
from typing import Protocol, runtime_checkable
import keyring
from keyring.errors import KeyringError, PasswordDeleteError
SERVICE_NAME = "javis-ai.local"
GEMINI_ACCOUNT = "gemini-api-key"
class SecretStoreError(RuntimeError):
"""A secret operation failed without exposing the secret value."""
@runtime_checkable
class SecretBackend(Protocol):
def get_password(self, service: str, account: str) -> str | None: ...
def set_password(self, service: str, account: str, value: str) -> None: ...
def delete_password(self, service: str, account: str) -> None: ...
class KeyringBackend:
"""Adapter around Python keyring and the active OS credential backend."""
def get_password(self, service: str, account: str) -> str | None:
return keyring.get_password(service, account)
def set_password(self, service: str, account: str, value: str) -> None:
keyring.set_password(service, account, value)
def delete_password(self, service: str, account: str) -> None:
keyring.delete_password(service, account)
class SecretProvider:
"""Named Javis secrets without any value-bearing status output."""
def __init__(self, backend: SecretBackend | None = None) -> None:
self._backend = backend or KeyringBackend()
def get_gemini_key(self) -> str | None:
try:
return self._backend.get_password(SERVICE_NAME, GEMINI_ACCOUNT)
except KeyringError as exc:
raise SecretStoreError(
"Der Betriebssystem-Schlüsselspeicher ist nicht verfügbar."
) from exc
def gemini_key_available(self) -> bool:
return bool(self.get_gemini_key())
def set_gemini_key(self, value: str) -> None:
if not value or not value.strip():
raise SecretStoreError("Ein leerer Gemini-Schlüssel wird nicht gespeichert.")
try:
self._backend.set_password(SERVICE_NAME, GEMINI_ACCOUNT, value.strip())
except KeyringError as exc:
raise SecretStoreError(
"Der Gemini-Schlüssel konnte nicht sicher gespeichert werden."
) from exc
def delete_gemini_key(self) -> bool:
try:
self._backend.delete_password(SERVICE_NAME, GEMINI_ACCOUNT)
except PasswordDeleteError:
return False
except KeyringError as exc:
raise SecretStoreError("Der Gemini-Schlüssel konnte nicht gelöscht werden.") from exc
return True
+40
View File
@@ -0,0 +1,40 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from javis.core.chat_service import ChatService
from javis.memory.sqlite_store import SQLiteSessionStore
from javis.providers.base import ChatMessage
class _CompatibleHybrid:
name = "hybrid"
model = "auto"
def chat(self, messages: list[ChatMessage]) -> str:
return "Antwort"
def supports_session(self, provider: str, model: str) -> bool:
return (provider, model) in {
("hybrid", "auto"),
("ollama", "qwen3:8b"),
}
class HybridChatServiceTests(unittest.TestCase):
def test_old_local_session_remains_loadable_after_hybrid_upgrade(self) -> None:
with tempfile.TemporaryDirectory() as temporary_directory:
store = SQLiteSessionStore(Path(temporary_directory) / "sessions.sqlite3")
old_session = store.create_session("ollama", "qwen3:8b")
store.append_exchange(old_session.id, "Alt", "Verlauf")
service = ChatService(store, _CompatibleHybrid())
loaded = service.load_session(old_session.id)
self.assertEqual([message.content for message in loaded.messages], ["Alt", "Verlauf"])
if __name__ == "__main__":
unittest.main()
+124 -1
View File
@@ -1,11 +1,15 @@
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
from javis.config.settings import Settings
from javis.core.chat_service import ChatService
from javis.interface.cli import run_chat
from javis.core.provider_router import HybridProvider
from javis.interface.cli import build_runtime, run_chat, run_gemini_configuration
from javis.memory.sqlite_store import SQLiteSessionStore
from javis.providers.base import ChatMessage
from javis.security.privacy import CloudPolicy
class _FakeProvider:
@@ -16,6 +20,30 @@ class _FakeProvider:
return f"Echo: {messages[-1].content}"
class _FakeHybridProvider(_FakeProvider):
name = "hybrid"
model = "auto"
def __init__(self) -> None:
self.mode = "auto"
self.last_route = None
def set_mode(self, mode: str) -> None:
self.mode = mode
class _LazySecrets:
def __init__(self) -> None:
self.reads = 0
def get_gemini_key(self) -> str | None:
self.reads += 1
return None
def gemini_key_available(self) -> bool:
return False
class CliTests(unittest.TestCase):
def test_basic_chat_commands(self) -> None:
with tempfile.TemporaryDirectory() as directory:
@@ -35,6 +63,101 @@ class CliTests(unittest.TestCase):
self.assertTrue(any("Nachrichten" in line for line in output))
self.assertEqual(output[-1], "Chat beendet.")
def test_provider_privacy_and_status_commands_are_sanitized(self) -> None:
with tempfile.TemporaryDirectory() as directory:
store = SQLiteSessionStore(Path(directory) / "sessions.sqlite3")
provider = _FakeHybridProvider()
provider.last_route = SimpleNamespace(
privacy_policy=CloudPolicy.ASK,
provider="ollama",
cloud_suppressed_by_local_mode=True,
technical_fallback=False,
)
service = ChatService(store, provider)
inputs = iter(
[
"/provider",
"/provider local",
"/privacy",
"/status",
"/exit",
]
)
output: list[str] = []
result = run_chat(
service,
input_fn=lambda _prompt: next(inputs),
output=output.append,
status_fn=lambda: [
"Gemini-Schlüssel vorhanden: ja",
"Heutige Cloudanfragen: 0",
],
)
self.assertEqual(result, 0)
self.assertIn("Aktiver Providermodus: auto", output)
self.assertIn("Providermodus: local", output)
self.assertTrue(
any(
"Datenschutzentscheidung: ask; Provider: ollama; "
"Cloud durch Modus local unterdrückt: ja; "
"Technischer Fallback: nein" in line
for line in output
)
)
self.assertIn("Gemini-Schlüssel vorhanden: ja", output)
def test_gemini_configuration_defaults_to_no(self) -> None:
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "javis.toml"
output: list[str] = []
result = run_gemini_configuration(
path,
input_fn=lambda _prompt: "",
output=output.append,
)
self.assertEqual(result, 1)
self.assertFalse(path.exists())
self.assertIn("deaktiviert", output[-1])
def test_gemini_configuration_requires_both_confirmations(self) -> None:
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "javis.toml"
answers = iter(["ja", "ja"])
result = run_gemini_configuration(
path,
input_fn=lambda _prompt: next(answers),
output=lambda _line: None,
)
self.assertEqual(result, 0)
self.assertTrue(path.exists())
content = path.read_text(encoding="utf-8")
self.assertIn("billing_confirmed_disabled = true", content)
self.assertNotIn("api_key", content.lower())
def test_runtime_wires_hybrid_provider_without_loading_key_at_start(self) -> None:
with tempfile.TemporaryDirectory() as directory:
settings = Settings(data_dir=Path(directory))
secrets = _LazySecrets()
service, _status = build_runtime(
settings,
input_fn=lambda _prompt: "",
output=lambda _line: None,
secret_provider=secrets,
)
session = service.new_session()
self.assertIsInstance(service.provider, HybridProvider)
self.assertEqual(session.provider, "hybrid")
self.assertEqual(session.model, "auto")
self.assertEqual(secrets.reads, 0)
if __name__ == "__main__":
unittest.main()
+163
View File
@@ -0,0 +1,163 @@
from __future__ import annotations
import unittest
from types import SimpleNamespace
from typing import Any
import httpx
from google.genai import errors
from javis.providers.base import (
ChatMessage,
CloudNetworkError,
InvalidApiKeyError,
InvalidProviderResponseError,
MissingApiKeyError,
ProviderRateLimitError,
ProviderTimeoutError,
)
from javis.providers.gemini import GeminiProvider
class _FakeModels:
def __init__(self, result: object) -> None:
self.result = result
self.call: dict[str, Any] | None = None
def generate_content(self, **kwargs: object) -> object:
self.call = kwargs
if isinstance(self.result, BaseException):
raise self.result
return self.result
class _FakeClient:
def __init__(self, result: object) -> None:
self.models = _FakeModels(result)
class _ClientFactory:
def __init__(self, result: object) -> None:
self.client = _FakeClient(result)
self.arguments: dict[str, Any] | None = None
def __call__(self, **kwargs: object) -> _FakeClient:
self.arguments = kwargs
return self.client
def _response(text: str = "Cloud-Antwort") -> SimpleNamespace:
return SimpleNamespace(
text=text,
usage_metadata=SimpleNamespace(
prompt_token_count=12,
candidates_token_count=7,
),
)
class GeminiProviderTests(unittest.TestCase):
def _provider(
self, result: object, **overrides: object
) -> tuple[GeminiProvider, _ClientFactory]:
factory = _ClientFactory(result)
arguments: dict[str, object] = {
"model": "gemini-3.6-flash",
"api_key": "test-key-not-a-real-secret",
"timeout_seconds": 30,
"max_output_tokens": 1024,
"max_retries": 1,
"client_factory": factory,
}
arguments.update(overrides)
return GeminiProvider(**arguments), factory
def test_success_uses_bounded_sdk_configuration_and_tracks_usage(self) -> None:
provider, factory = self._provider(_response())
answer = provider.chat(
[
ChatMessage("user", "Hallo"),
ChatMessage("assistant", "Guten Tag"),
ChatMessage("user", "Was ist SQLite?"),
]
)
self.assertEqual(answer, "Cloud-Antwort")
self.assertEqual(provider.last_usage.input_tokens, 12)
self.assertEqual(provider.last_usage.output_tokens, 7)
self.assertNotIn("test-key-not-a-real-secret", repr(factory.client.models.call))
http_options = factory.arguments["http_options"]
self.assertEqual(http_options.api_version, "v1")
self.assertEqual(http_options.timeout, 30_000)
self.assertEqual(http_options.retry_options.attempts, 2)
self.assertNotIn(429, http_options.retry_options.http_status_codes)
call = factory.client.models.call
self.assertEqual(call["model"], "gemini-3.6-flash")
self.assertEqual([content.role for content in call["contents"]], ["user", "model", "user"])
self.assertEqual(call["config"].max_output_tokens, 1024)
self.assertIsNone(call["config"].tools)
def test_missing_key_stops_before_client_creation(self) -> None:
factory = _ClientFactory(_response())
with self.assertRaises(MissingApiKeyError):
GeminiProvider(
model="gemini-3.6-flash",
api_key=None,
timeout_seconds=30,
max_output_tokens=1024,
max_retries=1,
client_factory=factory,
)
self.assertIsNone(factory.arguments)
def test_invalid_key_is_sanitized(self) -> None:
provider, _ = self._provider(
errors.ClientError(
401,
{"message": "rejected test-key-not-a-real-secret"},
)
)
with self.assertRaisesRegex(InvalidApiKeyError, "wurde abgelehnt") as raised:
provider.chat([ChatMessage("user", "Hallo")])
self.assertNotIn("test-key-not-a-real-secret", str(raised.exception))
def test_rate_limit_is_a_distinct_fallback_signal(self) -> None:
provider, _ = self._provider(errors.ClientError(429, {"message": "quota"}))
with self.assertRaises(ProviderRateLimitError):
provider.chat([ChatMessage("user", "Was ist Python?")])
def test_network_failure_is_a_distinct_fallback_signal(self) -> None:
request = httpx.Request("POST", "https://example.invalid")
provider, _ = self._provider(httpx.ConnectError("offline", request=request))
with self.assertRaises(CloudNetworkError):
provider.chat([ChatMessage("user", "Was ist Python?")])
def test_timeout_is_a_distinct_fallback_signal(self) -> None:
request = httpx.Request("POST", "https://example.invalid")
provider, _ = self._provider(httpx.ReadTimeout("timeout", request=request))
with self.assertRaises(ProviderTimeoutError):
provider.chat([ChatMessage("user", "Was ist Python?")])
def test_empty_response_is_rejected(self) -> None:
provider, _ = self._provider(_response(" "))
with self.assertRaises(InvalidProviderResponseError):
provider.chat([ChatMessage("user", "Was ist Python?")])
def test_more_than_one_retry_is_rejected(self) -> None:
with self.assertRaises(ValueError):
self._provider(_response(), max_retries=2)
if __name__ == "__main__":
unittest.main()
+11
View File
@@ -26,6 +26,14 @@ class _OllamaHandler(BaseHTTPRequestHandler):
self.end_headers()
self.wfile.write(body)
def do_GET(self) -> None:
body = json.dumps({"models": [{"name": "test-model"}]}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, format: str, *args: object) -> None:
return
@@ -42,6 +50,7 @@ class OllamaProviderTests(unittest.TestCase):
2,
)
response = provider.chat([ChatMessage("user", "Hallo")])
status = provider.probe()
finally:
server.shutdown()
server.server_close()
@@ -50,6 +59,8 @@ class OllamaProviderTests(unittest.TestCase):
self.assertEqual(response, "Lokale Antwort")
self.assertEqual(_OllamaHandler.request_payload["model"], "test-model")
self.assertFalse(_OllamaHandler.request_payload["stream"])
self.assertTrue(status.reachable)
self.assertTrue(status.model_available)
if __name__ == "__main__":
+96
View File
@@ -0,0 +1,96 @@
import unittest
from javis.providers.base import ChatMessage
from javis.security.privacy import CloudPolicy, PrivacyRouter
class PrivacyRouterTests(unittest.TestCase):
def setUp(self) -> None:
self.router = PrivacyRouter()
def test_general_technical_question_is_allowed(self) -> None:
for question in (
"Erkläre in zwei Sätzen den Unterschied zwischen RAM und SSD.",
"Wie funktioniert eine SQLite-Transaktion?",
"Was bedeutet HTTP 404?",
"Wie funktioniert Git?",
):
with self.subTest(question=question):
self.assertEqual(
self.router.classify(question).policy,
CloudPolicy.ALLOWED,
)
def test_internal_server_question_requires_approval(self) -> None:
decision = self.router.classify(
"Wie konfiguriere ich den internen Server mit Hostname srv-intern-01?"
)
self.assertEqual(decision.policy, CloudPolicy.ASK)
def test_technical_question_with_api_key_is_never(self) -> None:
decision = self.router.classify("Wie nutze ich diesen API-Key in Python: AIza" + "x" * 25)
self.assertEqual(decision.policy, CloudPolicy.NEVER)
def test_personal_technical_situation_requires_approval(self) -> None:
decision = self.router.classify(
"Meine SSD ist voll. Wie verschiebe ich meine persönlichen Dateien?"
)
self.assertEqual(decision.policy, CloudPolicy.ASK)
def test_personal_information_requires_approval(self) -> None:
self.assertEqual(
self.router.classify("Ich möchte meinen Alltag besser planen.").policy,
CloudPolicy.ASK,
)
def test_family_information_requires_approval(self) -> None:
self.assertEqual(
self.router.classify("Meine Frau hat morgen einen Termin.").policy,
CloudPolicy.ASK,
)
def test_secret_pattern_is_never(self) -> None:
decision = self.router.classify("Mein API-Key ist AIza" + "x" * 25)
self.assertEqual(decision.policy, CloudPolicy.NEVER)
def test_financial_and_health_information_are_never(self) -> None:
self.assertEqual(
self.router.classify("Meine IBAN ist ein Geheimnis.").policy,
CloudPolicy.NEVER,
)
self.assertEqual(
self.router.classify("Meine Diagnose soll ausgewertet werden.").policy,
CloudPolicy.NEVER,
)
def test_unknown_content_is_conservatively_ask(self) -> None:
self.assertEqual(
self.router.classify("Erkläre das bitte genauer.").policy,
CloudPolicy.ASK,
)
def test_explicit_cloud_request_cannot_override_never(self) -> None:
decision = self.router.classify(
"Sende mein Passwort an die Cloud.",
cloud_requested=True,
)
self.assertEqual(decision.policy, CloudPolicy.NEVER)
def test_minimal_context_excludes_sensitive_history(self) -> None:
messages = [
ChatMessage("user", "Meine Diagnose ist privat."),
ChatMessage("assistant", "Das bleibt lokal."),
ChatMessage("user", "Wie funktioniert SQLite?"),
]
selected = self.router.minimal_context(
messages,
current_policy=CloudPolicy.ALLOWED,
approved=False,
max_chars=1_000,
max_messages=6,
)
self.assertEqual(selected, [ChatMessage("user", "Wie funktioniert SQLite?")])
if __name__ == "__main__":
unittest.main()
+271
View File
@@ -0,0 +1,271 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from javis.core.provider_router import HybridProvider
from javis.memory.usage_store import ProviderEvent, SQLiteUsageStore
from javis.providers.base import (
ChatMessage,
CloudModelUnavailableError,
CloudNetworkError,
InvalidApiKeyError,
LocalModelProvider,
MissingApiKeyError,
ProviderRateLimitError,
ProviderTimeoutError,
ProviderUsage,
)
from javis.security.privacy import CloudPolicy, PrivacyRouter
class _RecordingProvider:
def __init__(
self,
name: str,
model: str,
*,
answer: str = "Antwort",
error: Exception | None = None,
) -> None:
self.name = name
self.model = model
self.answer = answer
self.error = error
self.calls: list[list[ChatMessage]] = []
self.last_usage = ProviderUsage(11, 5)
def chat(self, messages: list[ChatMessage]) -> str:
self.calls.append(messages)
if self.error:
raise self.error
return self.answer
class HybridProviderTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary_directory = tempfile.TemporaryDirectory()
self.usage_store = SQLiteUsageStore(
Path(self.temporary_directory.name) / "provider-usage.sqlite3"
)
self.local = _RecordingProvider("ollama", "qwen3:8b", answer="Lokal")
self.cloud = _RecordingProvider(
"gemini",
"gemini-3.6-flash",
answer="Cloud",
)
self.notices: list[str] = []
self.approvals: list[object] = []
self.approval_result = False
def tearDown(self) -> None:
self.temporary_directory.cleanup()
def _router(self, **overrides: object) -> HybridProvider:
arguments: dict[str, object] = {
"local_provider": self.local,
"cloud_provider_factory": lambda: self.cloud,
"cloud_model": "gemini-3.6-flash",
"privacy_router": PrivacyRouter(),
"usage_store": self.usage_store,
"approval_callback": self._approve,
"notice_callback": self.notices.append,
"mode": "auto",
"cloud_enabled": True,
"billing_confirmed_disabled": True,
"free_only": True,
"max_cloud_requests_per_day": 25,
"max_cloud_input_chars": 12_000,
"max_cloud_context_messages": 6,
}
arguments.update(overrides)
return HybridProvider(**arguments)
def _approve(self, decision: object) -> bool:
self.approvals.append(decision)
return self.approval_result
def test_contract_and_existing_local_session_are_supported(self) -> None:
router = self._router()
self.assertIsInstance(router, LocalModelProvider)
self.assertTrue(router.supports_session("hybrid", "auto"))
self.assertTrue(router.supports_session("ollama", "qwen3:8b"))
self.assertFalse(router.supports_session("ollama", "other-model"))
def test_allowed_technical_content_uses_cloud_with_minimal_context(self) -> None:
router = self._router()
answer = router.chat(
[
ChatMessage("user", "Meine IBAN ist vertraulich"),
ChatMessage("assistant", "Verstanden"),
ChatMessage("user", "Wie funktioniert SQLite?"),
]
)
self.assertEqual(answer, "Cloud")
self.assertEqual(len(self.cloud.calls), 1)
self.assertEqual(
[message.content for message in self.cloud.calls[0]],
["Wie funktioniert SQLite?"],
)
self.assertEqual(router.last_route.provider, "gemini")
self.assertEqual(router.last_route.privacy_policy, CloudPolicy.ALLOWED)
self.assertFalse(router.last_route.fallback)
def test_never_policy_cannot_construct_cloud_provider(self) -> None:
constructed = False
def cloud_factory() -> _RecordingProvider:
nonlocal constructed
constructed = True
return self.cloud
router = self._router(cloud_provider_factory=cloud_factory, mode="gemini")
answer = router.chat([ChatMessage("user", "Mein API-Key ist geheim")])
self.assertEqual(answer, "Lokal")
self.assertFalse(constructed)
self.assertTrue(router.last_route.fallback)
self.assertIn("lokale Antwort", self.notices[-1])
def test_ask_defaults_to_local_when_approval_is_denied(self) -> None:
router = self._router()
answer = router.chat([ChatMessage("user", "Meine Familie plant Urlaub")])
self.assertEqual(answer, "Lokal")
self.assertEqual(len(self.approvals), 1)
self.assertFalse(self.cloud.calls)
def test_ask_uses_cloud_after_explicit_approval(self) -> None:
self.approval_result = True
router = self._router()
answer = router.chat([ChatMessage("user", "Meine Familie plant Urlaub")])
self.assertEqual(answer, "Cloud")
self.assertEqual(len(self.cloud.calls), 1)
def test_429_falls_back_without_losing_local_answer(self) -> None:
self.cloud.error = ProviderRateLimitError("quota")
router = self._router()
answer = router.chat([ChatMessage("user", "Wie funktioniert Python?")])
self.assertEqual(answer, "Lokal")
self.assertEqual(self.usage_store.cloud_requests_on(), 1)
self.assertEqual(self.usage_store.local_fallbacks_on(), 1)
self.assertIn("Gemini nicht verfügbar", self.notices[-1])
def test_network_failure_falls_back(self) -> None:
self.cloud.error = CloudNetworkError("offline")
router = self._router()
answer = router.chat([ChatMessage("user", "Wie funktioniert Python?")])
self.assertEqual(answer, "Lokal")
self.assertTrue(router.last_route.fallback)
def test_timeout_and_missing_model_fall_back(self) -> None:
for error in (
ProviderTimeoutError("timeout"),
CloudModelUnavailableError("missing model"),
):
with self.subTest(error=type(error).__name__):
self.cloud.error = error
router = self._router()
answer = router.chat([ChatMessage("user", "Wie funktioniert Python?")])
self.assertEqual(answer, "Lokal")
self.assertTrue(router.last_route.fallback)
def test_missing_key_falls_back(self) -> None:
def missing_key() -> _RecordingProvider:
raise MissingApiKeyError("missing")
router = self._router(cloud_provider_factory=missing_key)
answer = router.chat([ChatMessage("user", "Wie funktioniert Python?")])
self.assertEqual(answer, "Lokal")
self.assertTrue(router.last_route.fallback)
def test_invalid_key_disables_cloud_for_remaining_process(self) -> None:
self.cloud.error = InvalidApiKeyError("invalid")
router = self._router()
router.chat([ChatMessage("user", "Wie funktioniert Python?")])
self.cloud.error = None
router.chat([ChatMessage("user", "Wie funktioniert SQLite?")])
self.assertEqual(len(self.cloud.calls), 1)
self.assertEqual(len(self.local.calls), 2)
self.assertIn("deaktiviert", self.notices[-1])
def test_local_daily_limit_stops_cloud_before_construction(self) -> None:
self.usage_store.record(
ProviderEvent(
"gemini",
"gemini-3.6-flash",
True,
None,
False,
"allowed",
)
)
router = self._router(max_cloud_requests_per_day=1)
answer = router.chat([ChatMessage("user", "Wie funktioniert Python?")])
self.assertEqual(answer, "Lokal")
self.assertFalse(self.cloud.calls)
self.assertIn("tägliches Cloudlimit", self.notices[-1])
def test_cloud_requires_local_activation_and_no_billing_confirmation(self) -> None:
for override in (
{"cloud_enabled": False},
{"billing_confirmed_disabled": False},
):
with self.subTest(override=override):
router = self._router(**override)
answer = router.chat([ChatMessage("user", "Wie funktioniert Python?")])
self.assertEqual(answer, "Lokal")
self.assertFalse(self.cloud.calls)
def test_oversized_current_message_remains_local(self) -> None:
router = self._router(max_cloud_input_chars=10)
answer = router.chat([ChatMessage("user", "Wie funktioniert Python?")])
self.assertEqual(answer, "Lokal")
self.assertFalse(self.cloud.calls)
def test_local_mode_never_asks_or_calls_cloud(self) -> None:
router = self._router(mode="local")
answer = router.chat([ChatMessage("user", "Meine Familie plant Urlaub")])
self.assertEqual(answer, "Lokal")
self.assertFalse(self.approvals)
self.assertFalse(self.cloud.calls)
self.assertFalse(router.last_route.fallback)
self.assertTrue(router.last_route.cloud_suppressed_by_local_mode)
self.assertFalse(router.last_route.technical_fallback)
def test_network_failure_is_reported_as_technical_fallback(self) -> None:
self.cloud.error = CloudNetworkError("offline")
router = self._router()
router.chat([ChatMessage("user", "Wie funktioniert Python?")])
self.assertFalse(router.last_route.cloud_suppressed_by_local_mode)
self.assertTrue(router.last_route.technical_fallback)
if __name__ == "__main__":
unittest.main()
+80
View File
@@ -0,0 +1,80 @@
import unittest
from keyring.errors import PasswordDeleteError
from javis.interface.cli import run_secrets
from javis.security.secrets import GEMINI_ACCOUNT, SERVICE_NAME, SecretProvider
class _FakeBackend:
def __init__(self) -> None:
self.values: dict[tuple[str, str], str] = {}
def get_password(self, service: str, account: str) -> str | None:
return self.values.get((service, account))
def set_password(self, service: str, account: str, value: str) -> None:
self.values[(service, account)] = value
def delete_password(self, service: str, account: str) -> None:
try:
del self.values[(service, account)]
except KeyError as exc:
raise PasswordDeleteError("missing") from exc
class SecretProviderTests(unittest.TestCase):
def setUp(self) -> None:
self.backend = _FakeBackend()
self.provider = SecretProvider(self.backend)
def test_key_is_initially_not_available(self) -> None:
self.assertFalse(self.provider.gemini_key_available())
def test_set_and_delete_key(self) -> None:
self.provider.set_gemini_key("test-secret-value")
self.assertTrue(self.provider.gemini_key_available())
self.assertEqual(
self.backend.values[(SERVICE_NAME, GEMINI_ACCOUNT)],
"test-secret-value",
)
self.assertTrue(self.provider.delete_gemini_key())
self.assertFalse(self.provider.gemini_key_available())
def test_cli_set_uses_hidden_input_and_never_outputs_value(self) -> None:
output: list[str] = []
secret = "test-secret-value"
result = run_secrets(
"set",
provider=self.provider,
password_fn=lambda _prompt: secret,
output=output.append,
)
self.assertEqual(result, 0)
self.assertTrue(self.provider.gemini_key_available())
self.assertNotIn(secret, "\n".join(output))
def test_cli_status_only_reports_availability(self) -> None:
output: list[str] = []
self.provider.set_gemini_key("test-secret-value")
result = run_secrets("status", provider=self.provider, output=output.append)
self.assertEqual(result, 0)
self.assertEqual(output, ["Gemini-Schlüssel: verfügbar"])
self.assertNotIn("test-secret-value", "\n".join(output))
def test_cli_delete_handles_missing_key(self) -> None:
output: list[str] = []
result = run_secrets("delete", provider=self.provider, output=output.append)
self.assertEqual(result, 0)
self.assertEqual(output, ["Gemini-Schlüssel war nicht verfügbar."])
if __name__ == "__main__":
unittest.main()
+83
View File
@@ -1,6 +1,12 @@
import tempfile
import unittest
from pathlib import Path
from javis.config.local_config import (
LocalConfigError,
load_local_defaults,
write_gemini_activation,
)
from javis.config.settings import ConfigurationError, Settings, default_data_dir
@@ -34,6 +40,83 @@ class SettingsTests(unittest.TestCase):
}
)
def test_zero_cost_defaults_are_conservative(self) -> None:
settings = Settings.from_env({"JAVIS_DATA_DIR": str(Path.cwd())})
self.assertTrue(settings.free_only)
self.assertFalse(settings.gemini_enabled)
self.assertFalse(settings.billing_confirmed_disabled)
self.assertEqual(settings.gemini_model, "gemini-3.6-flash")
self.assertEqual(settings.provider_mode, "auto")
def test_paid_mode_is_rejected(self) -> None:
with self.assertRaises(ConfigurationError):
Settings.from_env(
{
"JAVIS_DATA_DIR": str(Path.cwd()),
"JAVIS_FREE_ONLY": "false",
}
)
def test_cloud_limits_are_bounded(self) -> None:
with self.assertRaises(ConfigurationError):
Settings.from_env(
{
"JAVIS_DATA_DIR": str(Path.cwd()),
"JAVIS_MAX_CLOUD_REQUESTS_PER_DAY": "1001",
}
)
def test_local_toml_enables_only_confirmed_free_gemini(self) -> None:
with tempfile.TemporaryDirectory() as directory:
data_dir = Path(directory)
write_gemini_activation(data_dir / "javis.toml")
settings = Settings.from_env({"JAVIS_DATA_DIR": str(data_dir)})
self.assertTrue(settings.gemini_enabled)
self.assertTrue(settings.free_only)
self.assertTrue(settings.billing_confirmed_disabled)
self.assertEqual(settings.provider_mode, "auto")
self.assertEqual(settings.gemini_max_retries, 1)
def test_environment_can_safely_disable_local_cloud_config(self) -> None:
with tempfile.TemporaryDirectory() as directory:
data_dir = Path(directory)
write_gemini_activation(data_dir / "javis.toml")
settings = Settings.from_env(
{
"JAVIS_DATA_DIR": str(data_dir),
"JAVIS_GEMINI_ENABLED": "false",
}
)
self.assertFalse(settings.gemini_enabled)
def test_activation_file_contains_no_secret_and_is_not_overwritten(self) -> None:
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "javis.toml"
write_gemini_activation(path)
content = path.read_text(encoding="utf-8")
self.assertNotIn("api_key", content.lower())
self.assertNotIn("password", content.lower())
self.assertEqual(
load_local_defaults(path)["JAVIS_GEMINI_BILLING_CONFIRMED_DISABLED"],
"true",
)
with self.assertRaises(LocalConfigError):
write_gemini_activation(path)
def test_malformed_local_config_is_rejected(self) -> None:
with tempfile.TemporaryDirectory() as directory:
data_dir = Path(directory)
(data_dir / "javis.toml").write_text("[providers", encoding="utf-8")
with self.assertRaises(ConfigurationError):
Settings.from_env({"JAVIS_DATA_DIR": str(data_dir)})
if __name__ == "__main__":
unittest.main()
+30
View File
@@ -0,0 +1,30 @@
import unittest
from pathlib import Path
class StartScriptTests(unittest.TestCase):
def test_script_is_local_overridable_and_avoids_system_changes(self) -> None:
repository_root = Path(__file__).resolve().parents[2]
script = (repository_root / "scripts" / "start-javis.ps1").read_text(encoding="utf-8")
normalized = script.lower()
self.assertIn("$psscriptroot", normalized)
self.assertIn("http://127.0.0.1:11434/api/version", normalized)
self.assertIn("-windowstyle hidden", normalized)
self.assertIn("javis_uv_path", normalized)
self.assertIn("javis_ollama_path", normalized)
self.assertIn("javis_data_dir", normalized)
self.assertIn("run javis chat", normalized)
self.assertNotIn("c:\\users\\", normalized)
for forbidden in (
"set-executionpolicy",
"setx ",
"new-service",
"schtasks",
"hkey_",
):
self.assertNotIn(forbidden, normalized)
if __name__ == "__main__":
unittest.main()
+46
View File
@@ -0,0 +1,46 @@
import tempfile
import unittest
from datetime import UTC, datetime
from pathlib import Path
from javis.memory.usage_store import ProviderEvent, SQLiteUsageStore
class UsageStoreTests(unittest.TestCase):
def test_records_only_metadata_and_counts_daily_usage(self) -> None:
with tempfile.TemporaryDirectory() as directory:
database = Path(directory) / "usage.sqlite3"
store = SQLiteUsageStore(database)
store.record(
ProviderEvent(
provider="gemini",
model="gemini-test",
success=False,
error_category="quota",
fallback=False,
privacy_policy="allowed",
input_tokens=12,
output_tokens=None,
)
)
store.record(
ProviderEvent(
provider="ollama",
model="local-test",
success=True,
error_category=None,
fallback=True,
privacy_policy="allowed",
)
)
today = datetime.now(UTC).date()
self.assertEqual(store.cloud_requests_on(today), 1)
self.assertEqual(store.local_fallbacks_on(today), 1)
raw_database = database.read_bytes()
self.assertNotIn(b"prompt", raw_database)
self.assertNotIn(b"answer", raw_database)
if __name__ == "__main__":
unittest.main()
Generated
+798
View File
@@ -1,11 +1,429 @@
version = 1
revision = 3
requires-python = ">=3.11"
resolution-markers = [
"python_full_version >= '3.14'",
"python_full_version < '3.14'",
]
[[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.14.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" },
]
[[package]]
name = "backports-tarfile"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" },
]
[[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 = "cffi"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", size = 183845, upload-time = "2026-07-06T21:32:26.32Z" },
{ url = "https://files.pythonhosted.org/packages/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", size = 184186, upload-time = "2026-07-06T21:32:28.025Z" },
{ url = "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", size = 211892, upload-time = "2026-07-06T21:32:29.565Z" },
{ url = "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", size = 218793, upload-time = "2026-07-06T21:32:30.951Z" },
{ url = "https://files.pythonhosted.org/packages/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", size = 205737, upload-time = "2026-07-06T21:32:32.216Z" },
{ url = "https://files.pythonhosted.org/packages/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", size = 204909, upload-time = "2026-07-06T21:32:33.655Z" },
{ url = "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565", size = 217883, upload-time = "2026-07-06T21:32:35.173Z" },
{ url = "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", size = 221251, upload-time = "2026-07-06T21:32:36.527Z" },
{ url = "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", size = 214250, upload-time = "2026-07-06T21:32:37.852Z" },
{ url = "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", size = 219441, upload-time = "2026-07-06T21:32:39.146Z" },
{ url = "https://files.pythonhosted.org/packages/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", size = 174496, upload-time = "2026-07-06T21:32:40.467Z" },
{ url = "https://files.pythonhosted.org/packages/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", size = 185113, upload-time = "2026-07-06T21:32:41.761Z" },
{ url = "https://files.pythonhosted.org/packages/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", size = 179927, upload-time = "2026-07-06T21:32:42.961Z" },
{ url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" },
{ url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" },
{ url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" },
{ url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" },
{ url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" },
{ url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" },
{ url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" },
{ url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" },
{ url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" },
{ url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" },
{ url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" },
{ url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" },
{ url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" },
{ url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" },
{ url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" },
{ url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" },
{ url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" },
{ url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" },
{ url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" },
{ url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" },
{ url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" },
{ url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" },
{ url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" },
{ url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" },
{ url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" },
{ url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" },
{ url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" },
{ url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" },
{ url = "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" },
{ url = "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" },
{ url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" },
{ url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" },
{ url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" },
{ url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" },
{ url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" },
{ url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" },
{ url = "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" },
{ url = "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" },
{ url = "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" },
{ url = "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" },
{ url = "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" },
{ url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" },
{ url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" },
{ url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" },
{ url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" },
{ url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" },
{ url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" },
{ url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" },
{ url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" },
{ url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" },
{ url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" },
{ url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" },
{ url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" },
{ url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" },
{ url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" },
{ url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" },
{ url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" },
{ url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" },
{ url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" },
{ url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" },
{ url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" },
{ url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" },
{ url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" },
{ url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" },
{ url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" },
{ url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" },
{ url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" },
{ url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" },
{ url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" },
{ url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" },
{ url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" },
{ url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" },
{ url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" },
{ url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" },
{ url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" },
{ url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" },
{ url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" },
{ url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" },
{ url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" },
{ url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" },
{ url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" },
{ url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" },
{ url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" },
{ url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" },
{ url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" },
{ url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" },
{ url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" },
{ url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" },
{ url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" },
{ url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" },
{ url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" },
{ url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" },
{ url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" },
{ url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" },
{ url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" },
{ url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" },
{ url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" },
{ url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" },
{ url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" },
{ url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" },
{ url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" },
{ url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" },
{ url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" },
{ url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" },
{ url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" },
{ url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" },
{ url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" },
{ url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" },
{ url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" },
{ url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" },
{ url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" },
{ url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" },
{ url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" },
{ url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" },
{ url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" },
{ url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" },
{ url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" },
{ url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" },
{ url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" },
{ url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" },
{ url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" },
{ url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" },
{ url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" },
{ url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" },
{ url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" },
{ url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" },
{ url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" },
{ url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" },
{ url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" },
{ url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" },
{ url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" },
{ url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" },
{ url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" },
{ url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" },
{ url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" },
{ url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" },
{ url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" },
{ url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" },
{ url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" },
]
[[package]]
name = "cryptography"
version = "49.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" },
{ url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" },
{ url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" },
{ url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" },
{ url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" },
{ url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" },
{ url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" },
{ url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" },
{ url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" },
{ url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" },
{ url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" },
{ url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" },
{ url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" },
{ url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" },
{ url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" },
{ url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" },
{ url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" },
{ url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" },
{ url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" },
{ url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" },
{ url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" },
{ url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" },
{ url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" },
{ url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" },
{ url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" },
{ url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" },
{ url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" },
{ url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" },
{ url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" },
{ url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" },
{ url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" },
{ url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" },
{ url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" },
{ url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" },
{ url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" },
{ url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" },
{ url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" },
{ url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" },
{ url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" },
{ url = "https://files.pythonhosted.org/packages/63/d3/4a83af35d65e3fad632c926fad684c193ea4398569ccb0bbbc7fe8f5dc9a/cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b", size = 3993685, upload-time = "2026-06-12T20:02:14.883Z" },
{ url = "https://files.pythonhosted.org/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", size = 4676239, upload-time = "2026-06-12T20:02:28.793Z" },
{ url = "https://files.pythonhosted.org/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", size = 4715584, upload-time = "2026-06-12T20:01:27.495Z" },
{ url = "https://files.pythonhosted.org/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", size = 4675885, upload-time = "2026-06-12T20:01:55.49Z" },
{ url = "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", size = 4715449, upload-time = "2026-06-12T20:02:05.469Z" },
{ url = "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", size = 3783731, upload-time = "2026-06-12T20:02:43.319Z" },
]
[[package]]
name = "distro"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
]
[[package]]
name = "google-auth"
version = "2.56.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "pyasn1-modules" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c7/33/dbc946a407401b975f0719658f18e664ece2109f79ffd1ff3bf226c205f4/google_auth-2.56.2.tar.gz", hash = "sha256:e28f103ca8091fb7012b99c44243d7366c29863713b8e34a220c3322b7a07051", size = 365820, upload-time = "2026-07-21T21:53:28.188Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/63/50636aae68c9bf17c891c7eb18b49baa9bd6b31d2a97b8de4813a9fc8d1c/google_auth-2.56.2-py3-none-any.whl", hash = "sha256:c8270ea95b2697b74e3d8438ae9c5b898e38b623b915c7b5c5635921e7de68a6", size = 258588, upload-time = "2026-07-21T21:53:26.399Z" },
]
[package.optional-dependencies]
requests = [
{ name = "requests" },
]
[[package]]
name = "google-genai"
version = "2.16.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "distro" },
{ name = "google-auth", extra = ["requests"] },
{ name = "httpx" },
{ name = "pydantic" },
{ name = "requests" },
{ name = "sniffio" },
{ name = "tenacity" },
{ name = "typing-extensions" },
{ name = "websockets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/81/e6/ff83088427072cc9d5d21036788cf0ed08cc4906e4a5810e469553a43185/google_genai-2.16.0.tar.gz", hash = "sha256:c4c2524926001b18073db927a5d75bb7c8be7b5fd13ab507d599f51fff2284c5", size = 647939, upload-time = "2026-07-30T14:34:37.366Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/c6/f111056110030b1a5fb949687d7f93c2b4e8996f6494ae32efb049482796/google_genai-2.16.0-py3-none-any.whl", hash = "sha256:f9eda6a7a3dd4491a0d2253c4bdd4536462d63838ed3f1b0e4fb9a0eb8f43331", size = 1050096, upload-time = "2026-07-30T14:34:35.578Z" },
]
[[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 = "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.18"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
]
[[package]]
name = "importlib-metadata"
version = "9.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "zipp" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" },
]
[[package]]
name = "jaraco-classes"
version = "3.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "more-itertools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" },
]
[[package]]
name = "jaraco-context"
version = "6.1.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backports-tarfile", marker = "python_full_version < '3.12'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" },
]
[[package]]
name = "jaraco-functools"
version = "4.6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "more-itertools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz", hash = "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280", size = 20837, upload-time = "2026-07-14T01:28:02.544Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl", hash = "sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30", size = 11677, upload-time = "2026-07-14T01:28:01.59Z" },
]
[[package]]
name = "javis-ai"
version = "0.0.0"
source = { editable = "." }
dependencies = [
{ name = "google-genai" },
{ name = "keyring" },
]
[package.dev-dependencies]
dev = [
@@ -13,10 +431,221 @@ dev = [
]
[package.metadata]
requires-dist = [
{ name = "google-genai", specifier = ">=2.6,<3" },
{ name = "keyring", specifier = ">=25.7,<26" },
]
[package.metadata.requires-dev]
dev = [{ name = "ruff", specifier = ">=0.12,<1" }]
[[package]]
name = "jeepney"
version = "0.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" },
]
[[package]]
name = "keyring"
version = "25.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "importlib-metadata", marker = "python_full_version < '3.12'" },
{ name = "jaraco-classes" },
{ name = "jaraco-context" },
{ name = "jaraco-functools" },
{ name = "jeepney", marker = "sys_platform == 'linux'" },
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
{ name = "secretstorage", marker = "sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" },
]
[[package]]
name = "more-itertools"
version = "11.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", size = 145772, upload-time = "2026-05-22T14:14:29.909Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" },
]
[[package]]
name = "pyasn1"
version = "0.6.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" },
]
[[package]]
name = "pyasn1-modules"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyasn1" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" },
]
[[package]]
name = "pycparser"
version = "3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
]
[[package]]
name = "pydantic"
version = "2.13.4"
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/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
]
[[package]]
name = "pydantic-core"
version = "2.46.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" },
{ url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" },
{ url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" },
{ url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" },
{ url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" },
{ url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" },
{ url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" },
{ url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" },
{ url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" },
{ url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" },
{ url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" },
{ url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" },
{ url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" },
{ url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" },
{ url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" },
{ url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" },
{ url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" },
{ url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" },
{ url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" },
{ url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" },
{ url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" },
{ url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" },
{ url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" },
{ url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" },
{ url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" },
{ url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" },
{ url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" },
{ url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" },
{ url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" },
{ url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" },
{ url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
{ url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
{ url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
{ url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
{ url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
{ url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
{ url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
{ url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
{ url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
{ url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
{ url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
{ url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
{ url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
{ url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
{ url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
{ url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
{ url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
{ url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
{ url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
{ url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
{ url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
{ url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
{ url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
{ url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
{ url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
{ url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
{ url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
{ url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
{ url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
{ url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
{ url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
{ url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
{ url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
{ url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
{ url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
{ url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
{ url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
{ url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
{ url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
{ url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
{ url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
{ url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
{ url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
{ url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
{ url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
{ url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" },
{ url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" },
{ url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" },
{ url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" },
{ url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" },
{ url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" },
{ url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" },
{ url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" },
{ url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" },
{ url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" },
{ url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" },
{ url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" },
{ url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" },
{ url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" },
{ url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" },
{ url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" },
]
[[package]]
name = "pywin32-ctypes"
version = "0.2.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
]
[[package]]
name = "requests"
version = "2.34.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
]
[[package]]
name = "ruff"
version = "0.16.0"
@@ -41,3 +670,172 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" },
{ url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" },
]
[[package]]
name = "secretstorage"
version = "3.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "jeepney" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]]
name = "tenacity"
version = "9.1.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" },
]
[[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.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
]
[[package]]
name = "urllib3"
version = "2.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
]
[[package]]
name = "websockets"
version = "16.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/f7/bc3a25c5ec26ce62ce487690becc2f3710bbc7b33338f005ad390db0b986/websockets-16.1.1.tar.gz", hash = "sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57", size = 182204, upload-time = "2026-07-17T22:51:05.858Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2b/03/47debfe28e9d6d354be5d777b67fd44c359b9eb299a5d103500bd7cc3e37/websockets-16.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c", size = 179566, upload-time = "2026-07-17T22:48:49.596Z" },
{ url = "https://files.pythonhosted.org/packages/72/93/31efa1ed78c17e5cfc229fd449e3966e1b9cc15753204cd585cc8dd01f4a/websockets-16.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a", size = 177250, upload-time = "2026-07-17T22:48:50.942Z" },
{ url = "https://files.pythonhosted.org/packages/01/4a/542378ab3972b0c1cf1df3df3eff9591cea0d30c58c3aa3c4ddbc244e787/websockets-16.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22", size = 177528, upload-time = "2026-07-17T22:48:52.59Z" },
{ url = "https://files.pythonhosted.org/packages/33/d9/162321f63c7eed558e9e1798ed7a1e34a4f6dab51f35419e4ed7a4907979/websockets-16.1.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2", size = 186859, upload-time = "2026-07-17T22:48:53.915Z" },
{ url = "https://files.pythonhosted.org/packages/de/09/87df740f7430ce564bd52402e9c9458d4d0459cc7d2ee29e530c8204851b/websockets-16.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01", size = 188095, upload-time = "2026-07-17T22:48:55.384Z" },
{ url = "https://files.pythonhosted.org/packages/d2/12/3d2703af7cc095f3c81904c92208cc1ae79affbc67376944b50ee9301f73/websockets-16.1.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0", size = 191385, upload-time = "2026-07-17T22:48:56.742Z" },
{ url = "https://files.pythonhosted.org/packages/1d/69/986aa0234a964a00f5149cfc46e136e96c8faad1c783474550f40d31aef4/websockets-16.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29", size = 188653, upload-time = "2026-07-17T22:48:58.134Z" },
{ url = "https://files.pythonhosted.org/packages/35/6b/10f9d03e3970a69ba67bd3b46b87a929b586d0300fadbfe14f57c1f85490/websockets-16.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512", size = 187426, upload-time = "2026-07-17T22:48:59.515Z" },
{ url = "https://files.pythonhosted.org/packages/56/db/bb3aad62bf63d8bb3f0634b2eabffcfb3677a34bd19492110ff6869cf703/websockets-16.1.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3", size = 184882, upload-time = "2026-07-17T22:49:00.916Z" },
{ url = "https://files.pythonhosted.org/packages/6c/4c/c09a2ea9bfbeccce52fdc383e5f28af4bc8843338aabac28c81489af6120/websockets-16.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57", size = 187584, upload-time = "2026-07-17T22:49:02.283Z" },
{ url = "https://files.pythonhosted.org/packages/c7/8b/31bb4eb4d9eaacf1fdd39d115772a8aeaedfc19b5dc262e57ffbc8a9d42c/websockets-16.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3", size = 186174, upload-time = "2026-07-17T22:49:03.973Z" },
{ url = "https://files.pythonhosted.org/packages/2f/e4/dc02d725610a1ad49e193ef91a548194d71bdc6cdf27da83067dd1f73995/websockets-16.1.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648", size = 187986, upload-time = "2026-07-17T22:49:05.553Z" },
{ url = "https://files.pythonhosted.org/packages/e0/73/30ed84c8bfd14c73d4af29d5ed9323c3073b48e0b7b23b67070f4e7fd59b/websockets-16.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d", size = 185565, upload-time = "2026-07-17T22:49:06.959Z" },
{ url = "https://files.pythonhosted.org/packages/7d/d3/4be8d4959f51e31b4f8fc0ece12b45bd3b6c0d15ea23b9990d9c11fc805f/websockets-16.1.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be", size = 186598, upload-time = "2026-07-17T22:49:08.293Z" },
{ url = "https://files.pythonhosted.org/packages/26/fa/abb38597a52d84ed9cfacadc7a0c6f2db282c0ab23cdf72b58a666a21227/websockets-16.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81", size = 186834, upload-time = "2026-07-17T22:49:09.766Z" },
{ url = "https://files.pythonhosted.org/packages/59/80/1119ad08a228b90c4eb77fbe48df7836731a605f5f881ba701ca826a4a65/websockets-16.1.1-cp311-cp311-win32.whl", hash = "sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57", size = 179940, upload-time = "2026-07-17T22:49:11.196Z" },
{ url = "https://files.pythonhosted.org/packages/71/b2/e511c1c6f64a95c2f3fc54bffda0e14eaa7e9442be605c29270f7589b918/websockets-16.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a", size = 180239, upload-time = "2026-07-17T22:49:12.519Z" },
{ url = "https://files.pythonhosted.org/packages/17/9d/681cda21c9eee743203a6cb79b9d3d05adad9aa60ec660c6c9bf4dd619ca/websockets-16.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00", size = 179600, upload-time = "2026-07-17T22:49:13.92Z" },
{ url = "https://files.pythonhosted.org/packages/fb/8d/6195a88b45e8d2a8f745fc2046e36f885a3c9763e6767d2c46229bf9510c/websockets-16.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b", size = 177272, upload-time = "2026-07-17T22:49:15.453Z" },
{ url = "https://files.pythonhosted.org/packages/73/e3/fe2d498c64dea0095c9a9f9a351af4cd6eef31b618395582bc1f38ba45ff/websockets-16.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175", size = 177542, upload-time = "2026-07-17T22:49:16.875Z" },
{ url = "https://files.pythonhosted.org/packages/fe/ed/f1831681fce0e3242346e5458486003c5f124ed69e5e0b847fd029db4973/websockets-16.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1", size = 187137, upload-time = "2026-07-17T22:49:18.323Z" },
{ url = "https://files.pythonhosted.org/packages/6f/79/4ff9dcc1bb46f6b4c536936dde1fd60f9b564f3304307274db97f4c9496d/websockets-16.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15", size = 188374, upload-time = "2026-07-17T22:49:19.65Z" },
{ url = "https://files.pythonhosted.org/packages/62/c3/5c49b6efb36cab733d23773f6de575e1dba65736ead17d5d2b2a1daef779/websockets-16.1.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa", size = 191155, upload-time = "2026-07-17T22:49:21.331Z" },
{ url = "https://files.pythonhosted.org/packages/6e/f6/56ccceda3a4838d18f1d40821480da4775397e8b1eecf4031e20c50e2e90/websockets-16.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab", size = 189011, upload-time = "2026-07-17T22:49:22.889Z" },
{ url = "https://files.pythonhosted.org/packages/86/d6/ad5286241a2bce1107e2798d3bfbd62cf79aee167bdb654f8cb1e9dbf949/websockets-16.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847", size = 187766, upload-time = "2026-07-17T22:49:24.339Z" },
{ url = "https://files.pythonhosted.org/packages/bc/67/d65c970b7e347fdca69479beb7811c2060529956730a7a4e3ae7c66b0e31/websockets-16.1.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428", size = 185173, upload-time = "2026-07-17T22:49:25.743Z" },
{ url = "https://files.pythonhosted.org/packages/1d/5b/14af3cd4ee69d8ea9baca58f3dc3cfb1ba78332a347fd478cb096549d60e/websockets-16.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf", size = 187809, upload-time = "2026-07-17T22:49:27.147Z" },
{ url = "https://files.pythonhosted.org/packages/7b/11/be301710d70de97e3e7b3586e6d492c9c06d6a61bf1c2202c36cf0c75607/websockets-16.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751", size = 186412, upload-time = "2026-07-17T22:49:28.611Z" },
{ url = "https://files.pythonhosted.org/packages/db/07/fe1435bf6fe738a3d3b54dbe0c18dabf12cba4d909ac8b58b539ce27c1f4/websockets-16.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f", size = 188290, upload-time = "2026-07-17T22:49:29.965Z" },
{ url = "https://files.pythonhosted.org/packages/8a/0a/81f394aff8efcbb01208c1ced77df0a3c7fcce584a88c7273663697946c2/websockets-16.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2", size = 185844, upload-time = "2026-07-17T22:49:31.447Z" },
{ url = "https://files.pythonhosted.org/packages/39/5c/dd485b995473f415510251fe9bd708f2d24458f439fce958daf8d66dc7c6/websockets-16.1.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383", size = 186823, upload-time = "2026-07-17T22:49:33.104Z" },
{ url = "https://files.pythonhosted.org/packages/9d/0b/f78de76ff446f1e66af12b43c48a35f31744de93cfdec2f4ea67d5d7bbf1/websockets-16.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3", size = 187102, upload-time = "2026-07-17T22:49:34.616Z" },
{ url = "https://files.pythonhosted.org/packages/37/a1/4cf892007778eaf84ad162bfc98046e0ed89b63ac55949e3236626b2a23f/websockets-16.1.1-cp312-cp312-win32.whl", hash = "sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747", size = 179943, upload-time = "2026-07-17T22:49:36.213Z" },
{ url = "https://files.pythonhosted.org/packages/d9/de/6abe251d28c3a3f217096575400b27750b18e0b1d2fff3a2a239960fea07/websockets-16.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7", size = 180243, upload-time = "2026-07-17T22:49:37.626Z" },
{ url = "https://files.pythonhosted.org/packages/ce/fd/6ec6c6d2850aea25b1b2aa9901a016980bb87d01e89b3eb00470b1b5d471/websockets-16.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1", size = 179587, upload-time = "2026-07-17T22:49:38.959Z" },
{ url = "https://files.pythonhosted.org/packages/5f/d8/1d299d2dd34087db39831a34cc645ef8a6f89d78efada6983093513cd81c/websockets-16.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df", size = 177272, upload-time = "2026-07-17T22:49:40.293Z" },
{ url = "https://files.pythonhosted.org/packages/3d/86/0a70d3ae2f0f2256bb41302d9804dbca65d4360281e7feb3e1f94102ac46/websockets-16.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac", size = 177530, upload-time = "2026-07-17T22:49:41.786Z" },
{ url = "https://files.pythonhosted.org/packages/b5/c2/c676c69444d9db448b3f0a55a98dcc534affce0bce961d9d2f0b8499b10a/websockets-16.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8", size = 187197, upload-time = "2026-07-17T22:49:43.658Z" },
{ url = "https://files.pythonhosted.org/packages/0b/13/88137fbaf726ebe29d62c1117fa11fa2bbb6209dc79d4ad738efbe36a2aa/websockets-16.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6", size = 188433, upload-time = "2026-07-17T22:49:45.147Z" },
{ url = "https://files.pythonhosted.org/packages/01/6d/46c2f2ce6751cb26f39293e1ecbf8544cb01321397cd476c2756b98c216d/websockets-16.1.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854", size = 189868, upload-time = "2026-07-17T22:49:46.581Z" },
{ url = "https://files.pythonhosted.org/packages/29/2b/170a9e8097636cfde4dc3c592b6e00b18a44a2f5407606d96ca542dd5838/websockets-16.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a", size = 189059, upload-time = "2026-07-17T22:49:47.972Z" },
{ url = "https://files.pythonhosted.org/packages/a7/48/f0d4ebc9ab4b473b8861b9e20fdb663d515d42f7befdf62cdb60fee7a1ec/websockets-16.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49", size = 187814, upload-time = "2026-07-17T22:49:49.344Z" },
{ url = "https://files.pythonhosted.org/packages/d5/ba/39a41d3ae8e72696a9492581900611c5a91e2b07563b0bcd2523adea9854/websockets-16.1.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785", size = 185229, upload-time = "2026-07-17T22:49:50.787Z" },
{ url = "https://files.pythonhosted.org/packages/3c/36/ac15b604f850d1907f0a85ed721cefe47cd45034b3620069b829746cccbe/websockets-16.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56", size = 187874, upload-time = "2026-07-17T22:49:52.228Z" },
{ url = "https://files.pythonhosted.org/packages/a8/f3/3fbd5d71d59299c3770faa5884d4f45070236ca5a35ab3a61830812c409a/websockets-16.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509", size = 186469, upload-time = "2026-07-17T22:49:53.776Z" },
{ url = "https://files.pythonhosted.org/packages/b4/fc/dd90349bba58af2a53ef2ddd9c32716c81eb6d59a0687939fff561860878/websockets-16.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1", size = 188347, upload-time = "2026-07-17T22:49:55.202Z" },
{ url = "https://files.pythonhosted.org/packages/4c/f3/f73ba86427682da59b78c11d77ba56d5b801c32e84afe79b274bbd6a9bb2/websockets-16.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead", size = 185903, upload-time = "2026-07-17T22:49:56.75Z" },
{ url = "https://files.pythonhosted.org/packages/34/7c/f95eb20e80104173b3a0a092291f89ea4047ef6e608e0a57ca06eb14eecb/websockets-16.1.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e", size = 186855, upload-time = "2026-07-17T22:49:58.467Z" },
{ url = "https://files.pythonhosted.org/packages/b0/35/dd875b3e050ff232d60fa377707f890e369f74d134f1be32e8f68879747c/websockets-16.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87", size = 187140, upload-time = "2026-07-17T22:50:00.016Z" },
{ url = "https://files.pythonhosted.org/packages/e8/dc/5cbfcb41824502f6af93b8f3943a4d06c67c23c7d2e31eb18748c4a5b2a7/websockets-16.1.1-cp313-cp313-win32.whl", hash = "sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea", size = 179928, upload-time = "2026-07-17T22:50:01.685Z" },
{ url = "https://files.pythonhosted.org/packages/b0/c1/71e5deb5b7f8f226997ab64908c184ac3105c0155ce2d486f318e5dd08a8/websockets-16.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68", size = 180242, upload-time = "2026-07-17T22:50:03.117Z" },
{ url = "https://files.pythonhosted.org/packages/73/a2/ba78a164eeea4620df4a4df4bd2ed6017438c4655cc0f36f2c0bc0432355/websockets-16.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8", size = 179635, upload-time = "2026-07-17T22:50:05.001Z" },
{ url = "https://files.pythonhosted.org/packages/b9/08/d26d7a7628cd4ac34cbbdb63ac80914ca842ed8e42938c40a53567806df3/websockets-16.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293", size = 177320, upload-time = "2026-07-17T22:50:06.427Z" },
{ url = "https://files.pythonhosted.org/packages/0f/45/ebec83e6269536aa5932533c67b0af5c781f3e73fdbcd68672dcf43f4f44/websockets-16.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051", size = 177544, upload-time = "2026-07-17T22:50:07.834Z" },
{ url = "https://files.pythonhosted.org/packages/c9/d5/abc614d2297f6c1c3e01e61260364457a47c25cc1cf6a879038902bc6aa8/websockets-16.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1", size = 187270, upload-time = "2026-07-17T22:50:09.275Z" },
{ url = "https://files.pythonhosted.org/packages/52/71/4c99af3b87dff1b2927981f6876607d4acb45338c665242168d3982f7758/websockets-16.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7", size = 188509, upload-time = "2026-07-17T22:50:10.722Z" },
{ url = "https://files.pythonhosted.org/packages/9b/b4/5c8ca14b0df7eb84ed0524165c5359150210140817a3312aee57bf62a1cf/websockets-16.1.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31", size = 189882, upload-time = "2026-07-17T22:50:12.293Z" },
{ url = "https://files.pythonhosted.org/packages/25/c1/bedfba9e70557129cb8083748d167bdcc01483dedf0f0df143676df05cbe/websockets-16.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0", size = 189114, upload-time = "2026-07-17T22:50:13.789Z" },
{ url = "https://files.pythonhosted.org/packages/df/09/aa835b2787835aebd839114be5de51b797cb480b63ba42b26d34dfe147cb/websockets-16.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3", size = 187861, upload-time = "2026-07-17T22:50:15.179Z" },
{ url = "https://files.pythonhosted.org/packages/20/26/f6408330694dbc9830857d9d23bc14ac4f6875127a480cfdda8d5ca21198/websockets-16.1.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562", size = 185286, upload-time = "2026-07-17T22:50:16.741Z" },
{ url = "https://files.pythonhosted.org/packages/17/9a/e0675e70dd8a80762cf35bb18799d3f290a4890ffe6439bc51d222796083/websockets-16.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b", size = 187935, upload-time = "2026-07-17T22:50:18.213Z" },
{ url = "https://files.pythonhosted.org/packages/33/c1/3234cfb86afde01b81e9bddcc6e534c440975d60a13991259e833069ab3e/websockets-16.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a", size = 186444, upload-time = "2026-07-17T22:50:19.67Z" },
{ url = "https://files.pythonhosted.org/packages/89/87/9c15206e1d778923d8daa9657de07aa62ea815e13448319c98458c37b281/websockets-16.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c", size = 188409, upload-time = "2026-07-17T22:50:21.28Z" },
{ url = "https://files.pythonhosted.org/packages/f2/00/cf5de5c67676de2d3eef8b2a518f168f6796595447a5b7161ba0d012915c/websockets-16.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499", size = 185958, upload-time = "2026-07-17T22:50:22.719Z" },
{ url = "https://files.pythonhosted.org/packages/62/c0/731b6ddede2e4136912ec4cff2cffbda35af73546be4762c3d7bd3bd79af/websockets-16.1.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985", size = 186911, upload-time = "2026-07-17T22:50:24.108Z" },
{ url = "https://files.pythonhosted.org/packages/8c/7f/39c634472c4469a24a7c09cecddffb08fac6d0e74f73881a94ee8a40a196/websockets-16.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9", size = 187204, upload-time = "2026-07-17T22:50:25.548Z" },
{ url = "https://files.pythonhosted.org/packages/26/89/9667c256c256dafcc62d21328ce7a40067da857969b68ee9af375b0aaf72/websockets-16.1.1-cp314-cp314-win32.whl", hash = "sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328", size = 179603, upload-time = "2026-07-17T22:50:27.086Z" },
{ url = "https://files.pythonhosted.org/packages/bd/dd/1c099d6c0fc5deb6b46ccdbb6981fdb4b12c917869cb3952408409dc18db/websockets-16.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc", size = 179948, upload-time = "2026-07-17T22:50:28.521Z" },
{ url = "https://files.pythonhosted.org/packages/35/25/9956b2d5e0529d5d23924f21bba1440d4c5c88a562e4f08550871ffa97a7/websockets-16.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573", size = 179963, upload-time = "2026-07-17T22:50:29.982Z" },
{ url = "https://files.pythonhosted.org/packages/17/06/55ffc976c488b6aee9ea05761ff7c4e88e7c1fd82818c8ca7b556ad2f90c/websockets-16.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999", size = 177497, upload-time = "2026-07-17T22:50:31.396Z" },
{ url = "https://files.pythonhosted.org/packages/0c/e8/f7dac2e980bacc92bdc26cebae4ae4d50cae5380732c50980598fc0bbae4/websockets-16.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe", size = 177698, upload-time = "2026-07-17T22:50:32.829Z" },
{ url = "https://files.pythonhosted.org/packages/b2/39/26762f734113e22da2b942c3aca85798e0c0405d64c256549540ff31e5a1/websockets-16.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d", size = 187561, upload-time = "2026-07-17T22:50:34.24Z" },
{ url = "https://files.pythonhosted.org/packages/11/94/c3f330851806b9b02138b774d593478323e73c99238681b4b93efe64e02d/websockets-16.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392", size = 188732, upload-time = "2026-07-17T22:50:36.088Z" },
{ url = "https://files.pythonhosted.org/packages/d1/f2/eb2c450f052de334ae33cf200ece6e87b0e14d186807074e4eb1cd2cdea2/websockets-16.1.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7", size = 190872, upload-time = "2026-07-17T22:50:38.008Z" },
{ url = "https://files.pythonhosted.org/packages/70/31/2ac8cecf3a74f7fed9132129fc3d90b3998a1554570c11a69b2a8c20332d/websockets-16.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499", size = 189305, upload-time = "2026-07-17T22:50:39.53Z" },
{ url = "https://files.pythonhosted.org/packages/6a/cf/8ab19650d3c0d4562c92e70ab47c257c4aa5c6a713ed87fe63766b31fefc/websockets-16.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43", size = 188033, upload-time = "2026-07-17T22:50:40.912Z" },
{ url = "https://files.pythonhosted.org/packages/66/d7/a49a38a6127a4acb134fb1912b215d900cc657605cff32445bf519f3acc4/websockets-16.1.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458", size = 185748, upload-time = "2026-07-17T22:50:42.559Z" },
{ url = "https://files.pythonhosted.org/packages/95/3e/ad1fa40388c7f2e0bb2c7930d0090b6c5498594bd1cdaec18864df3d9e97/websockets-16.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62", size = 188285, upload-time = "2026-07-17T22:50:43.974Z" },
{ url = "https://files.pythonhosted.org/packages/35/b8/d5db28ca264b9104f82196f92dc8843e35fd391f763d42e4ad358f5bc97e/websockets-16.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb", size = 186777, upload-time = "2026-07-17T22:50:45.474Z" },
{ url = "https://files.pythonhosted.org/packages/42/9c/726cb39d0cc43ae848dce4aa2acb04eecc6738b1264ec6d700bf6bcfb9f8/websockets-16.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51", size = 188682, upload-time = "2026-07-17T22:50:46.973Z" },
{ url = "https://files.pythonhosted.org/packages/be/c7/1168704de8c2dd483edabe4a22cbe4465dd8be8dd95561d214f9fe092871/websockets-16.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0", size = 186377, upload-time = "2026-07-17T22:50:48.413Z" },
{ url = "https://files.pythonhosted.org/packages/ca/40/f9ff2d630ffce4e7dfea0b2288e1caf9ebbf9ff8a9ec9396136ce8b94935/websockets-16.1.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217", size = 187148, upload-time = "2026-07-17T22:50:49.845Z" },
{ url = "https://files.pythonhosted.org/packages/b5/71/e177c8299f78d7cbe2d14df228643c10c70c0e86e108e092056bbcc16e46/websockets-16.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737", size = 187578, upload-time = "2026-07-17T22:50:51.619Z" },
{ url = "https://files.pythonhosted.org/packages/49/b2/b6987faf330f5af5c787a2610124c2e8403d51724f9001ec4fff6311fe7a/websockets-16.1.1-cp314-cp314t-win32.whl", hash = "sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7", size = 179729, upload-time = "2026-07-17T22:50:53.269Z" },
{ url = "https://files.pythonhosted.org/packages/a2/6e/fbac6ed878dd362fbad7d415fa4f84d38e3e33fed8cde45c64e783acf826/websockets-16.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231", size = 180072, upload-time = "2026-07-17T22:50:54.969Z" },
{ url = "https://files.pythonhosted.org/packages/e1/ed/71fea6e141590cafc40b14dc5943b0845606bee87bdb52a21b6a73eb4311/websockets-16.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869", size = 177185, upload-time = "2026-07-17T22:50:56.665Z" },
{ url = "https://files.pythonhosted.org/packages/01/ec/00e7eeca200facf9266a83e4cbbf1bed0e67fba1d4d45031d3e5b3d81b5c/websockets-16.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9", size = 177459, upload-time = "2026-07-17T22:50:58.197Z" },
{ url = "https://files.pythonhosted.org/packages/75/fd/5774c4b33f7c0d8f0c51809c8b3a93456c48e3543579262cfa64eb5f522e/websockets-16.1.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e", size = 178294, upload-time = "2026-07-17T22:50:59.641Z" },
{ url = "https://files.pythonhosted.org/packages/37/c3/48e2c03d2bd79bb45948841c592d24156312dd5f58cdf8f549febe652fb6/websockets-16.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d", size = 179190, upload-time = "2026-07-17T22:51:01.129Z" },
{ url = "https://files.pythonhosted.org/packages/2d/3f/73e511ecf2496ceac57dd4ed8388efe2bcf0769338a2dbf242c8366ae87e/websockets-16.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5", size = 180330, upload-time = "2026-07-17T22:51:02.603Z" },
{ url = "https://files.pythonhosted.org/packages/be/4d/2d0d67834092e354d2b0498f014a41249a89556bc406cf86f3e1557bb463/websockets-16.1.1-py3-none-any.whl", hash = "sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3", size = 173814, upload-time = "2026-07-17T22:51:04.184Z" },
]
[[package]]
name = "zipp"
version = "4.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" },
]