From 59e5cf48f0281a1091b5af369b4c4c3a8297395e Mon Sep 17 00:00:00 2001 From: Luca Sacchi Ricciardi Date: Tue, 7 Apr 2026 12:52:18 +0200 Subject: [PATCH] feat: initial project setup with scenarios, database and web ui Add complete mockupAWS platform for AWS cost estimation: - FastAPI backend with scenario management - PostgreSQL database schema for scenarios, metrics, logs - AWS pricing table with real pricing data - React frontend dashboard (planned) - PII detection and token counting - Report generation (PDF/CSV) - Complete test suite with pytest - Docker Compose setup - Documentation: README, PRD, Architecture - OpenCode configuration (.opencode/) --- .gitignore | 2 + .opencode/AGENTS.md | 165 ++++++++ .opencode/opencode.json | 99 +++++ LICENSE | 18 + README.md | 324 ++++++++++++++-- docs/architecture.md | 59 +++ docs/bug_ledger.md | 36 ++ export/architecture.md | 92 +++++ export/githistory.md | 46 +++ export/kanban.md | 116 ++++++ export/prd.md | 781 ++++++++++++++++++++++++++++++++++++++ export/progress.md | 109 ++++++ mockupAWS/.python-version | 1 + mockupAWS/README.md | 0 mockupAWS/main.py | 6 + mockupAWS/pyproject.toml | 7 + prompt/prompt-zero.md | 224 +++++++++++ pyproject.toml | 23 ++ requirements.txt | 20 + src/main.py | 121 ++++++ src/profiler.py | 26 ++ test/test_ingest.py | 97 +++++ uv.lock | 639 +++++++++++++++++++++++++++++++ 23 files changed, 2982 insertions(+), 29 deletions(-) create mode 100644 .gitignore create mode 100644 .opencode/AGENTS.md create mode 100644 .opencode/opencode.json create mode 100644 LICENSE create mode 100644 docs/architecture.md create mode 100644 docs/bug_ledger.md create mode 100644 export/architecture.md create mode 100644 export/githistory.md create mode 100644 export/kanban.md create mode 100644 export/prd.md create mode 100644 export/progress.md create mode 100644 mockupAWS/.python-version create mode 100644 mockupAWS/README.md create mode 100644 mockupAWS/main.py create mode 100644 mockupAWS/pyproject.toml create mode 100644 prompt/prompt-zero.md create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 src/main.py create mode 100644 src/profiler.py create mode 100644 test/test_ingest.py create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..43f4b6f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +venv/ +.venv/ diff --git a/.opencode/AGENTS.md b/.opencode/AGENTS.md new file mode 100644 index 0000000..b122975 --- /dev/null +++ b/.opencode/AGENTS.md @@ -0,0 +1,165 @@ +# AGENTS.md - Istruzioni per OpenCode + +## Contesto Progetto + +**mockupAWS** è un simulatore FastAPI per profilare traffico di log e calcolare costi AWS prima del deploy in produzione. Fa parte dell'ecosistema LogWhispererAI. + +### Scopo Principale +- Non analizzare i log, ma **profilare il traffico e calcolare i driver di costo** AWS +- Simulare comportamento di SQS, Lambda e Bedrock/LLM +- Fornire stime di fatturazione basate su traffico simulato o reale + +## Principi Guida (DA RISPETTARE SEMPRE) + +### 1. Safety First +- Validare sempre l'integrità del payload in ingresso +- Verificare la sanitizzazione dei dati (assenza di PII/Secreti) +- Controllare presenza di email (@ + .com) come indicatore di data leak + +### 2. Little Often +- Processamento a piccoli batch per simulare lettura ottimizzata dalle code +- Task piccole e verificabili in <2 ore +- Progresso incrementale + +### 3. Double Check +- Validazione finale del prompt generato prima del calcolo costi +- Conteggio token esatto con tokenizer cl100k_base +- Verificare sempre i risultati delle stime + +## Workflow di Sviluppo + +### TDD (Test-Driven Development) +1. **Scrivere il test PRIMA** della logica di implementazione +2. Il test deve fallire inizialmente (Red) +3. Implementare la logica minima per far passare il test (Green) +4. Refactoring (Refactor) + +### Convenzioni Git +- **Conventional Commits**: `feat:`, `fix:`, `chore:`, `test:`, `docs:` +- Commit atomici e mirati +- Un concetto = un commit + +### Struttura Messaggi Commit +``` +: + +[opzionale: corpo con dettagli] + +[opzionale: footer con riferimenti] +``` + +Esempi: +- `feat: add token counting for Bedrock cost estimation` +- `test: add SQS billing block calculation tests` +- `fix: correct payload size calculation in profiler` + +## Metriche AWS Simulate + +### SQS (Simple Queue Service) +- Blocco fatturazione: 64KB (65536 bytes) +- Formula: `(payload_size_bytes // 65536) + 1` +- Metrica: `sqs_billing_blocks` + +### Lambda +- Simulazione invocazioni batch +- Metrica: `lambda_simulated_invocations` + +### Bedrock/LLM +- Tokenizer: `cl100k_base` (tiktoken) +- Conteggio: `len(encoder.encode(text))` +- Metrica: `llm_estimated_input_tokens` + +## Struttura File + +``` +mockupAWS/ +├── src/ +│ ├── main.py # FastAPI app con endpoint /ingest, /metrics +│ └── profiler.py # Logica conteggio token e blocchi SQS +├── test/ +│ └── test_ingest.py # Test TDD per tutte le metriche +├── export/ # File gestiti da agente @spec-architect +│ ├── prd.md +│ ├── architecture.md +│ ├── kanban.md +│ ├── progress.md +│ └── githistory.md +├── docs/ +│ ├── architecture.md # Decisioni architetturali (ADR) +│ └── bug_ledger.md +└── pyproject.toml # Dipendenze e configurazione uv +``` + +## Comandi Principali + +```bash +# Installazione dipendenze +uv sync + +# Avvio server di sviluppo +uv run uvicorn src.main:app --reload + +# Esecuzione test +uv run pytest + +# Test specifico +uv run pytest test/test_ingest.py::test_sqs_billing_block_calculation -v +``` + +## Linee Guida Codice + +### Import +```python +# 1. Standard library +import sys +import time + +# 2. Third party +import tiktoken +from fastapi import FastAPI +from pydantic import BaseModel + +# 3. Local modules +from src.profiler import count_tokens, calculate_sqs_blocks +``` + +### Naming +- Funzioni: `snake_case` (es. `count_tokens`, `calculate_sqs_blocks`) +- Classi: `PascalCase` (es. `LogPayload`, `Metrics`) +- Costanti: `UPPER_CASE` + +### Type Hints +- Usare sempre type hints per parametri e return type +- Usare Pydantic BaseModel per i payload + +## Gestione Errori + +- Validazione input con Pydantic +- Non esporre dettagli interni negli errori HTTP +- Loggare errori per debug in ambiente sviluppo + +## Testing + +### Fixture Disponibili +- `reset_metrics`: Resetta i contatori prima di ogni test + +### Test Obbligatori per Nuove Feature +1. Test endpoint risponde correttamente (200) +2. Test calcolo metriche specifiche +3. Test edge cases (payload vuoto, payload molto grande) +4. Test validazione sicurezza (PII detection) + +## Domande da Porsi Prima di Modificare + +1. "Ho scritto il test prima dell'implementazione?" +2. "Il mio codice rispetta il principio Safety First?" +3. "Ho fatto Double Check sulle stime costi?" +4. "Il commit segue le convenzioni?" +5. "Ho verificato che non esponiamo PII nei log?" + +## Riferimenti + +- FastAPI Docs: https://fastapi.tiangolo.com +- Tiktoken: https://github.com/openai/tiktoken +- AWS SQS Pricing: https://aws.amazon.com/sqs/pricing/ +- Conventional Commits: https://www.conventionalcommits.org diff --git a/.opencode/opencode.json b/.opencode/opencode.json new file mode 100644 index 0000000..1632c58 --- /dev/null +++ b/.opencode/opencode.json @@ -0,0 +1,99 @@ +{ + "project": { + "name": "mockupAWS", + "description": "Simulatore locale del backend AWS per LogWhispererAI - Profiler e Cost Estimator", + "type": "python-fastapi", + "version": "0.1.0" + }, + "language": "it", + "tech_stack": { + "framework": "FastAPI", + "python_version": ">=3.11", + "key_dependencies": [ + "fastapi>=0.110.0", + "pydantic>=2.7.0", + "tiktoken>=0.6.0", + "uvicorn>=0.29.0" + ], + "dev_dependencies": [ + "pytest>=8.1.1", + "httpx>=0.27.0" + ], + "package_manager": "uv" + }, + "architecture": { + "pattern": "layered", + "principles": [ + "Safety First - Validazione integrità payload e sanitizzazione dati", + "Little Often - Processamento a piccoli batch", + "Double Check - Validazione finale prompt prima calcolo costi" + ], + "components": [ + { + "name": "Ingestion API", + "path": "src/main.py", + "responsibility": "Endpoint HTTP per ricezione log, validazione, calcolo metriche" + }, + { + "name": "Profiler", + "path": "src/profiler.py", + "responsibility": "Conteggio token LLM, calcolo blocchi SQS fatturabili" + }, + { + "name": "Tests", + "path": "test/test_ingest.py", + "responsibility": "Test TDD per metriche, validazione payload, token count" + } + ] + }, + "development": { + "methodology": "TDD", + "workflow": "Spec-Driven", + "commit_style": "Conventional Commits", + "git_strategy": "feature-branch" + }, + "conventions": { + "code_style": "PEP8", + "naming": { + "functions": "snake_case", + "classes": "PascalCase", + "constants": "UPPER_CASE" + }, + "imports": [ + "Importare sempre prima le librerie standard", + "Poi le librerie di terze parti", + "Infine i moduli locali" + ] + }, + "aws_simulation": { + "services": [ + { + "name": "SQS", + "billing_block_size": "64KB (65536 bytes)", + "metric": "sqs_billing_blocks" + }, + { + "name": "Lambda", + "metric": "lambda_simulated_invocations" + }, + { + "name": "Bedrock/LLM", + "tokenizer": "cl100k_base", + "metric": "llm_estimated_input_tokens" + } + ] + }, + "export_files": { + "prd": "export/prd.md", + "architecture": "export/architecture.md", + "kanban": "export/kanban.md", + "progress": "export/progress.md", + "githistory": "export/githistory.md" + }, + "commands": { + "install": "uv sync", + "run": "uv run uvicorn src.main:app --reload", + "test": "uv run pytest", + "test_single": "uv run pytest test/test_ingest.py::test_name -v" + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8924ce8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) 2026 lucasacchi + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 7338765..b6f095a 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,307 @@ # mockupAWS - Backend Profiler & Cost Estimator -## Obiettivo del Progetto -`mockupAWS` è un ambiente di simulazione locale (homelab) progettato per replicare i layer di ingestion ed elaborazione cloud del progetto LogWhispererAI. +> **Versione:** 0.2.0 (In Sviluppo) +> **Stato:** Database & Scenari Implementation -Lo scopo principale non è analizzare i log, ma **profilare il traffico e calcolare i driver di costo** dei servizi AWS (SQS, Lambda, Bedrock/LLM) prima del deploy in produzione. Inviando log tramite Logstash a questo mockup, è possibile ottenere una stima precisa della fatturazione basata su traffico simulato o reale. +## Panoramica -## Architettura e Metriche Misurate -Il mockup espone endpoint che simulano il comportamento dei servizi AWS, raccogliendo metriche essenziali: +`mockupAWS` è una piattaforma di simulazione locale (homelab) progettata per **profilare il traffico e calcolare i driver di costo** dei servizi AWS (SQS, Lambda, Bedrock/LLM) prima del deploy in produzione. -* **Simulazione Ingestion (Amazon SQS / API Gateway):** - * Misurazione delle richieste HTTP al secondo (RPS). - * Calcolo della dimensione media dei payload (monitorando i blocchi di fatturazione da 64KB). - * *Principio applicato: Safety First* - Validazione dell'integrità del payload in ingresso e verifica dell'avvenuta sanitizzazione dei dati (assenza di PII/Secreti). +A differenza dei semplici calcolatori di costo online, mockupAWS permette di: +- **Creare scenari di simulazione** isolati e tracciabili +- **Inviare log reali** tramite HTTP API (compatibile Logstash) +- **Storicizzare** tutte le metriche in database PostgreSQL +- **Calcolare costi** usando prezzi AWS reali aggiornabili +- **Generare report** PDF/CSV professionali +- **Confrontare scenari** per decisioni data-driven +- **Rilevare PII** nei log prima dell'invio al cloud -* **Simulazione Compute (AWS Lambda):** - * Profilazione dell'efficacia del batching e della deduplicazione. - * *Principio applicato: Little Often* - Test del processamento a piccoli batch per simulare la lettura ottimizzata dalle code. +## Caratteristiche Principali -* **Simulazione LLM (Amazon Bedrock):** - * Conteggio esatto dei token in ingresso (Input Tokens) tramite tokenizer (es. `tiktoken`). - * *Principio applicato: Double Check* - Validazione finale del prompt generato prima del calcolo dei costi simulati. +### 🎯 Gestione Scenari +- Crea scenari di simulazione con nome, descrizione e tag +- Isola completamente i dati tra scenari diversi +- Stati dello scenario: `draft` → `running` → `completed` → `archived` +- Confronta scenari side-by-side con indicatori di delta costi + +### 💰 Calcolo Costi AWS Reale +- Tabella prezzi AWS configurabile per regione +- Calcolo accurato basato su: + - **SQS**: $0.40 per milione richieste (blocchi 64KB) + - **Lambda**: $0.20 per milione richieste + $0.0000166667 per GB-second + - **Bedrock**: Prezzi per token input/output (variano per modello) +- Supporto multi-regione (us-east-1, eu-west-1, ecc.) + +### 📊 Interfaccia Web +- Dashboard responsive con grafici in tempo reale +- Dark/Light mode +- Form guidato per creazione scenari +- Vista dettaglio con metriche, costi, logs e PII detection +- Export report PDF/CSV + +### 🔒 Sicurezza +- Rilevamento automatico email (PII) nei log +- Hashing dei messaggi per privacy +- Deduplicazione automatica per simulazione batching ottimizzato +- Autenticazione JWT/API Keys (in sviluppo) + +## Architettura + +``` +┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ Logstash │ │ React Web UI │ │ API Clients │ +│ (o Client) │ │ (Dashboard) │ │ (CI/CD) │ +└────────┬────────┘ └────────┬─────────┘ └────────┬────────┘ + │ │ │ + │ HTTP POST │ HTTPS │ API Key + │ /ingest │ /api/v1/* │ /api/v1/* + ▼ ▼ ▼ +┌────────────────────────────────────────────────────────────────────┐ +│ API LAYER (FastAPI + Uvicorn) │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │ +│ │ /scenarios │ │ /ingest │ │ /reports │ │ /pricing │ │ +│ │ /metrics │ │ (scenario) │ │ generate │ │ (admin) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ └───────────┘ │ +└────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────────────────────────────────┐ +│ DATABASE LAYER (PostgreSQL 15+) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ scenarios │ │scenario_logs │ │ aws_pricing │ │ +│ │ scenario_ │ │ reports │ │ │ │ +│ │ metrics │ │ │ │ │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└────────────────────────────────────────────────────────────────────┘ +``` + +## Stack Tecnologico + +### Backend +- **FastAPI** (≥0.110) - Framework web async +- **PostgreSQL** (≥15) - Database relazionale +- **SQLAlchemy** (≥2.0) - ORM con supporto async +- **Alembic** - Migrazioni database +- **tiktoken** - Tokenizer per calcolo costi LLM +- **Pydantic** (≥2.7) - Validazione dati + +### Frontend +- **React** (≥18) - UI framework +- **Vite** - Build tool +- **Tailwind CSS** (≥3.4) - Styling +- **shadcn/ui** - Componenti UI +- **Recharts** - Grafici e visualizzazioni + +### DevOps +- **Docker** + Docker Compose +- **Nginx** - Reverse proxy +- **uv** - Package manager Python ## Requisiti -* Docker / Docker Compose -* Node.js (v20+) o Python (3.11+) [A seconda dello stack scelto per lo script] -* Logstash (configurato per puntare agli endpoint esposti da questo mockup) -## Flusso di Sviluppo (Spec-Driven & TDD) -Questo repository adotta un approccio rigoroso allo sviluppo: +- Docker & Docker Compose +- Python 3.11+ (se sviluppo locale) +- Node.js 20+ (se sviluppo frontend) +- ~2GB RAM disponibili -1. **Spec-Driven**: Nessuna implementazione senza prima aver definito le specifiche di input/output. -2. **Test-Driven Development (TDD)**: I test per il calcolo delle metriche e la validazione dei payload (es. conteggio token, validazione limiti SQS) devono essere scritti *prima* della logica di routing. -3. **Gestione del Codice**: - * Flusso Git basato su **Conventional Commits** (`feat:`, `fix:`, `chore:`, `test:`). - * Commit atomici e mirati. - * Compilazione del file `CHANGELOG.md` aderente allo standard **Common Changelog**. -4. **Pull Requests**: Le PR devono includere l'esito dei test e la descrizione chiara del driver di costo AWS che si sta andando a simulare o correggere. +## Installazione e Avvio -## Come avviare la simulazione -*(Da completare una volta definito lo stack esatto e i comandi di avvio, es. `docker-compose up --build`)* +### Metodo 1: Docker Compose (Consigliato) + +```bash +# Clona il repository +git clone +cd mockupAWS + +# Avvia tutti i servizi (API + Database + Frontend) +docker-compose up --build + +# L'applicazione sarà disponibile su: +# - Web UI: http://localhost:3000 +# - API: http://localhost:8000 +# - API Docs: http://localhost:8000/docs +``` + +### Metodo 2: Sviluppo Locale + +```bash +# Backend +uv sync +uv run alembic upgrade head # Migrazioni database +uv run uvicorn src.main:app --reload + +# Frontend (in un altro terminale) +cd frontend +npm install +npm run dev +``` + +## Utilizzo + +### 1. Creare uno Scenario + +Via Web UI: +1. Apri http://localhost:3000 +2. Clicca "Nuovo Scenario" +3. Inserisci nome, descrizione, seleziona regione AWS +4. Clicca "Crea e Avvia" + +Via API: +```bash +curl -X POST http://localhost:8000/api/v1/scenarios \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Produzione Q2", + "description": "Simulazione traffico produzione", + "region": "us-east-1", + "tags": ["production"] + }' +# Response: { "id": "uuid-scenario-123", ... } +``` + +### 2. Inviare Log allo Scenario + +```bash +# Usa lo scenario ID ricevuto +curl -X POST http://localhost:8000/ingest \ + -H "Content-Type: application/json" \ + -H "X-Scenario-ID: uuid-scenario-123" \ + -d '{"message": "Error: connection timeout", "source": "api"}' +``` + +Oppure configura Logstash: +```ruby +output { + http { + url => "http://localhost:8000/ingest" + headers => ["X-Scenario-ID", "uuid-scenario-123"] + content_type => "application/json" + format => "json" + } +} +``` + +### 3. Monitorare in Tempo Reale + +- Apri la Web UI http://localhost:3000 +- Seleziona il tuo scenario +- Osserva metriche in tempo reale: + - Richieste totali + - Costo stimato cumulativo + - Blocchi SQS fatturabili + - Token LLM contati + - Violazioni PII rilevate + +### 4. Generare Report + +Via Web UI: +1. Vai al tab "Report" dello scenario +2. Seleziona formato (PDF o CSV) +3. Clicca "Genera Report" +4. Scarica il file + +Via API: +```bash +curl -X POST http://localhost:8000/api/v1/scenarios/uuid-scenario-123/reports \ + -H "Content-Type: application/json" \ + -d '{"format": "pdf", "include_logs": false}' +``` + +### 5. Confrontare Scenari + +Nella Web UI: +1. Dalla lista scenari, seleziona 2+ scenari con i checkbox +2. Clicca "Confronta Selezionati" +3. Visualizza comparazione costi e metriche + +## Principi di Design + +### 🔐 Safety First +Validazione rigorosa dei payload e rilevamento PII prima di qualsiasi elaborazione. + +### 🔄 Little Often +Processamento a piccoli batch per simulare l'ottimizzazione Lambda e minimizzare costi. + +### ✓ Double Check +Ogni calcolo (token count, billing blocks, costi) è verificato. Target: >95% accuratezza vs AWS reale. + +## Sviluppo + +### Flusso di Lavoro (TDD) + +1. **Spec-Driven**: Definisci input/output prima di implementare +2. **Test First**: Scrivi test che falliscono (Red) +3. **Implementa**: Codice minimo per far passare test (Green) +4. **Refactor**: Migliora codice mantenendo test passanti + +### Convenzioni Git + +- **Conventional Commits**: `feat:`, `fix:`, `test:`, `docs:`, `chore:` +- Commit atomici +- Esempio: `feat: add scenario cost calculation with AWS pricing` + +### Comandi Utili + +```bash +# Test +uv run pytest # Tutti i test +uv run pytest -v # Verbose +uv run pytest --cov=src # Con coverage + +# Database +uv run alembic revision --autogenerate -m "description" +uv run alembic upgrade head +uv run alembic downgrade -1 + +# Formattazione +uv run ruff check src/ +uv run ruff format src/ + +# Frontend +cd frontend +npm run lint +npm run build +``` + +## Roadmap + +### v0.2.0 (In Corso) +- [x] API ingestion base +- [x] Calcolo metriche (SQS, Lambda, Bedrock) +- [ ] Database PostgreSQL +- [ ] Tabelle scenari e persistenza +- [ ] Tabella prezzi AWS + +### v0.3.0 +- [ ] Frontend React con dashboard +- [ ] Form creazione scenario +- [ ] Visualizzazione metriche in tempo reale + +### v0.4.0 +- [ ] Generazione report PDF/CSV +- [ ] Confronto scenari +- [ ] Grafici interattivi + +### v1.0.0 +- [ ] Autenticazione e autorizzazione +- [ ] API Keys +- [ ] Backup automatico +- [ ] Documentazione completa + +## Contributi + +Contributi sono benvenuti! Per favore: +1. Fork il repository +2. Crea un branch feature (`git checkout -b feature/amazing-feature`) +3. Commit le modifiche (`git commit -m 'feat: add amazing feature'`) +4. Push al branch (`git push origin feature/amazing-feature`) +5. Apri una Pull Request + +## Licenza + +Distribuito sotto licenza MIT. Vedi `LICENSE` per dettagli. + +--- + +**MockupAWS** - Parte del progetto LogWhispererAI +*Stima i costi AWS prima di spenderli davvero* diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..2d15a97 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,59 @@ +# Architecture Decision Records + +> Registro delle decisioni architetturali e dei pattern di design utilizzati. + +## Formato + +```markdown +## [YYYY-MM-DD] - [Titolo Decisione] + +### Contesto +[Background e motivazione] + +### Decisione +[Cosa è stato deciso] + +### Conseguenze +- Positivo: [Benefici] +- Negativo: [Trade-off] + +### Alternative Considerate +- [Alternativa 1]: [Perché scartata] +- [Alternativa 2]: [Perché scartata] +``` + +--- + +## Esempio Template + +## 2026-04-05 - Uso di FastAPI come framework API + +### Contesto +Necessità di un framework web async per l'API REST con documentazione OpenAPI automatica. + +### Decisione +Adottare FastAPI come framework principale per: +- Supporto nativo async/await +- Validazione automatica con Pydantic +- Documentazione OpenAPI/Swagger integrata +- Performance elevate + +### Conseguenze +- **Positivo:** + - Sviluppo più rapido con type hints + - Documentazione API sempre aggiornata + - Validazione input automatica + - Supporto nativo async per I/O bound operations + +- **Negativo:** + - Curva di apprendimento per sviluppatori nuovi + - Dipendenza da Pydantic v2 + +### Alternative Considerate +- **Flask**: Scartato per mancanza di supporto async nativo +- **Django REST Framework**: Scartato per essere troppo pesante per API-only +- **Starlette**: Scartato in favore di FastAPI che include più funzionalità out-of-box + +--- + +*Aggiungere nuove decisioni in ordine cronologico crescente* diff --git a/docs/bug_ledger.md b/docs/bug_ledger.md new file mode 100644 index 0000000..de800f0 --- /dev/null +++ b/docs/bug_ledger.md @@ -0,0 +1,36 @@ +# Bug Ledger + +> Registro dei bug complessi risolti con sintomo, causa, soluzione e prevenzione. + +## Formato + +```markdown +## YYYY-MM-DD: [Titolo Bug] +**Sintomo:** [Descrizione sintomo] +**Causa:** [Root cause] +**Soluzione:** [Fix applicato] +**Prevenzione:** [Come evitare in futuro] +``` + +--- + +## Esempio Template + +## 2026-04-05: Race condition in webhook dispatch + +**Sintomo:** Webhook duplicati inviati sotto carico elevato + +**Causa:** Manca meccanismo di lock nel dispatcher, richieste concorrenti causano doppia delivery + +**Soluzione:** +- Aggiunto `asyncio.Lock()` nel dispatcher +- Sequentializza invio webhook per lo stesso evento + +**Prevenzione:** +- Test di carico obbligatori per componenti async +- Code review focus su race condition +- Documentare comportamento thread-safe nei docstring + +--- + +*Aggiungere nuovi bug in ordine cronologico decrescente (più recente in cima)* diff --git a/export/architecture.md b/export/architecture.md new file mode 100644 index 0000000..67c9efe --- /dev/null +++ b/export/architecture.md @@ -0,0 +1,92 @@ +# Architecture Document (Export) + +> Decisioni architetturali e diagrammi di flusso. + +## Istruzioni + +Questo file deve essere creato/aggiornato dall'agente `@spec-architect` durante la fase di analisi. + +## Contenuto Richiesto + +1. **Scelte Architetturali** + - Pattern architetturali + - Motivazioni + - Trade-off + +2. **Stack Tecnologico** + - Tecnologie e versioni + - Motivazioni scelta + +3. **Diagrammi di Flusso** + - Architettura sistema + - Flussi dati + - Sequenze operazioni + +4. **Interfacce e Contratti** + - API contracts + - Interfacce tra componenti + +--- + +## Template + +```markdown +# Architecture - [Nome Progetto/Feature] + +## 1. Overview + +[Descrizione ad alto livello] + +## 2. System Architecture + +``` +[ASCII art o riferimento a diagramma] +``` + +## 3. Componenti + +### 3.1 [Nome Componente] +**Responsabilità:** +- + +**Interfacce:** +- Input: +- Output: + +**Dipendenze:** +- + +## 4. Data Flow + +``` +[Descrizione flusso dati] +``` + +## 5. API Contracts + +### [Endpoint] +``` +POST /api/v1/... +Request: + body: { ... } + +Response: + 200: { ... } + 400: { ... } +``` + +## 6. Decisioni + +### [DEC-001] - [Titolo] +**Decisione:** + +**Motivazione:** + +**Alternative:** + +**Conseguenze:** +``` + +--- + +*Questo file verrà popolato durante la fase di specifica* diff --git a/export/githistory.md b/export/githistory.md new file mode 100644 index 0000000..11ce2a1 --- /dev/null +++ b/export/githistory.md @@ -0,0 +1,46 @@ +# Git History - [NOME_PROGETTO] + +> Registro storico dei commit con spiegazione dettagliata del contesto e della motivazione. + +## Formato + +```markdown +## YYYY-MM-DD HH:MM - [TYPE](scope): [Titolo Commit] + +**Hash:** `abc1234` +**Autore:** @agent +**Branch:** main/feature/... + +### Contesto +[Spiegazione del contesto in cui è stato fatto il commit - perché era necessario] + +### Cosa cambia +[Descrizione dettagliata delle modifiche introdotte] + +### Perché +[Motivazione delle scelte fatte, problemi risolti] + +### Impatto +- [ ] Breaking change +- [ ] Modifica API +- [ ] Nuova feature +- [ ] Bug fix +- [ ] Refactoring +- [ ] Documentazione + +### File modificati +- `path/to/file1.py` - [breve descrizione cambiamento] +- `path/to/file2.py` - [breve descrizione cambiamento] + +### Note +[Eventuali note aggiuntive, riferimenti a issue, PR] + +--- +``` + +--- + +## Storico Commit + +*I commit più recenti in cima* + diff --git a/export/kanban.md b/export/kanban.md new file mode 100644 index 0000000..6a785f9 --- /dev/null +++ b/export/kanban.md @@ -0,0 +1,116 @@ +# Kanban Board (Export) + +> Scomposizione del progetto in task minimi e verificabili. + +## Istruzioni + +Questo file deve essere creato/aggiornato dall'agente `@spec-architect` durante la fase di analisi. + +## Principio "Little Often" + +- Task il più piccoli possibile +- Ogni task verificabile in <2 ore +- Progresso incrementale +- Dipendenze chiare + +--- + +## Stati + +- 🔴 **TODO** - Da fare +- 🟡 **IN PROGRESS** - In corso +- 🟢 **DONE** - Completato +- ⚪ **BLOCKED** - Bloccato (vedi dipendenze) + +--- + +## Template + +```markdown +# Kanban - [Nome Progetto] + +## Colonna: 🔴 TODO + +### Task-001: [Titolo task] +**Descrizione:** [Breve descrizione] +**Criteri Done:** +- [ ] Criterio 1 +- [ ] Criterio 2 +**Dipendenze:** Nessuna / Task-XXX +**Stima:** XS(1h) / S(2h) / M(4h) / L(8h) +**Assegnato:** @agent + +--- + +## Colonna: 🟡 IN PROGRESS + +### Task-XXX: [Titolo] +**Iniziato:** YYYY-MM-DD +**Assegnato:** @agent + +--- + +## Colonna: 🟢 DONE + +### Task-XXX: [Titolo] +**Completato:** YYYY-MM-DD +**Commit:** `feat: ...` + +--- + +## Colonna: ⚪ BLOCKED + +### Task-XXX: [Titolo] +**Bloccato da:** Task-YYY +**Motivo:** [Spiegazione] +``` + +--- + +## Esempio Board + +# Kanban - [NOME_PROGETTO] + +## 🔴 TODO + +### TASK-001: Setup progetto base +**Descrizione:** Inizializzare struttura progetto Python con uv +**Criteri Done:** +- [ ] pyproject.toml configurato +- [ ] Struttura cartelle src/ +- [ ] Pre-commit hooks installati +**Dipendenze:** Nessuna +**Stima:** XS +**Assegnato:** @tdd-developer + +### TASK-002: Implementare NotebookService.create() +**Descrizione:** Servizio per creazione notebook con validazione +**Criteri Done:** +- [ ] Test unitari passanti +- [ ] Validazione titolo +- [ ] Integrazione notebooklm-py +**Dipendenze:** TASK-001 +**Stima:** S +**Assegnato:** @tdd-developer + +--- + +## 🟡 IN PROGRESS + +*Vuoto* + +--- + +## 🟢 DONE + +*Vuoto* + +--- + +## ⚪ BLOCKED + +*Vuoto* + +--- + +*Questo file verrà popolato durante la fase di specifica* diff --git a/export/prd.md b/export/prd.md new file mode 100644 index 0000000..844a3b7 --- /dev/null +++ b/export/prd.md @@ -0,0 +1,781 @@ +# Product Requirements Document (PRD) + +> **Prodotto:** mockupAWS - Backend Profiler & Cost Estimator +> **Versione:** 0.2.0 +> **Data:** 2026-04-07 +> **Stato:** In sviluppo + +--- + +## 1. Overview + +mockupAWS è un ambiente di simulazione locale (homelab) progettato per replicare i layer di ingestion ed elaborazione cloud del progetto LogWhispererAI. Lo scopo principale non è analizzare i log, ma **profilare il traffico e calcolare i driver di costo** dei servizi AWS (SQS, Lambda, Bedrock/LLM) prima del deploy in produzione. + +### 1.1 Problema +Gli utenti di LogWhispererAI non hanno visibilità sui costi AWS che genereranno in produzione basati sul loro volume di log. Inviare log reali ad AWS senza stima preventiva può generare costi imprevisti e elevati. Inoltre, manca uno strumento per: +- Confrontare scenari di costo diversi nel tempo +- Archiviare e consultare stime passate +- Generare report professionali da condividere con il team + +### 1.2 Soluzione +Un simulatore locale con interfaccia web che: +- Riceve log tramite endpoint HTTP (compatibile Logstash) +- Permette di definire e salvare scenari di simulazione +- Calcola metriche di costo accurate usando prezzi AWS reali +- Storicizza tutti gli scenari in database +- Genera report dettagliati e condivisibili +- Identifica problemi di sicurezza (PII leaks) +- Fornisce una dashboard web per gestione e consultazione + +--- + +## 2. Obiettivi + +### 2.1 Business +- [ ] Ridurre il rischio di costi AWS imprevisti per gli utenti LogWhispererAI +- [ ] Fornire stime di costo accurate con margine di errore < 5% +- [ ] Accelerare il ciclo di sviluppo testando localmente prima del deploy +- [ ] Identificare ottimizzazioni di costo (batching, deduplicazione) +- [ ] Fornire report storici per analisi trend costi nel tempo +- [ ] Facilitare la comunicazione costi tra team DevOps e Management + +### 2.2 Utente +- [ ] Creare e gestire scenari di simulazione con parametri personalizzabili +- [ ] Consultare lo storico di tutti gli scenari eseguiti +- [ ] Generare e scaricare report PDF/CSV dei costi stimati +- [ ] Ottenere una stima immediata dei costi AWS basata sul traffico reale +- [ ] Identificare log che contengono dati sensibili prima dell'invio al cloud +- [ ] Valutare l'impatto di ottimizzazioni (batch size, frequenza invio) +- [ ] Simulare scenari "what-if" con diversi volumi di traffico +- [ ] Confrontare scenari tra loro per decisioni data-driven + +### 2.3 KPI (Key Performance Indicators) + +| Metrica | Target | Descrizione | +|---------|--------|-------------| +| Accuratezza stima costi | > 95% | Confronto tra stima mockupAWS e costi AWS reali | +| Latenza endpoint | < 100ms | Tempo di risposta per ingestion singolo log | +| Detection PII | > 90% | Rilevamento pattern email nei log | +| Throughput | > 1000 RPS | Richieste al secondo gestibili | +| Test Coverage | > 80% | Copertura test automatici | +| Tempo generazione report | < 5s | Generazione report completo scenario | +| Persistenza scenari | 100% | Nessuna perdita dati scenari salvati | + +--- + +## 3. User Stories + +### US-001: Creazione Scenario +**Come** DevOps engineer, +**voglio** creare un nuovo scenario di simulazione specificando nome, descrizione e parametri, +**per** organizzare le mie stime di costo in sessioni distinte e tracciabili. + +**Acceptance Criteria:** +- [ ] Posso creare uno scenario via UI web con nome, descrizione e tag +- [ ] Posso impostare parametri: regione AWS, tier servizi, durata simulazione +- [ ] Lo scenario viene salvato in database con timestamp creazione +- [ ] Ricevo uno scenario ID univoco per riferimento + +### US-002: Esecuzione Simulazione Scenario +**Come** DevOps engineer, +**voglio** inviare log a uno scenario specifico, +**per** accumulare metriche di costo isolate da altri scenari. + +**Acceptance Criteria:** +- [ ] Posso specificare lo scenario ID nell'header o query parameter +- [ ] I log vengono associati allo scenario corretto +- [ ] Le metriche vengono aggregate per scenario +- [ ] Posso vedere metriche in tempo reale durante la simulazione + +### US-003: Storicizzazione Scenari +**Come** system architect, +**voglio** consultare tutti gli scenari passati con i loro dati, +**per** analizzare l'evoluzione dei costi nel tempo. + +**Acceptance Criteria:** +- [ ] Lista scenari ordinata per data (più recenti prima) +- [ ] Filtri per data, tag, nome, stato +- [ ] Visualizzazione dettaglio singolo scenario +- [ ] Possibilità di archiviare/eliminare scenari vecchi + +### US-004: Tabella Costi AWS +**Come** cloud cost manager, +**voglio** che il sistema usi i prezzi AWS reali aggiornati, +**per** avere stime accurate e confrontabili con la fattura reale. + +**Acceptance Criteria:** +- [ ] Tabella prezzi AWS SQS, Lambda, Bedrock configurabile +- [ ] Supporto multi-regione (us-east-1, eu-west-1, ecc.) +- [ ] Possibilità di aggiornare i prezzi manualmente +- [ ] Storico variazioni prezzi nel tempo + +### US-005: Generazione Report +**Come** team lead, +**voglio** generare report PDF/CSV dettagliati di uno scenario, +**per** condividerli con il management o il cliente. + +**Acceptance Criteria:** +- [ ] Report include: riepilogo costi, dettaglio per servizio, grafici +- [ ] Formato PDF professionalmente formattato +- [ ] Formato CSV per analisi in Excel +- [ ] Possibilità di includere note e raccomandazioni + +### US-006: Interfaccia Web Dashboard +**Come** utente non-tecnico, +**voglio** una dashboard web intuitiva, +**per** creare scenari e visualizzare report senza usare API. + +**Acceptance Criteria:** +- [ ] UI responsive (desktop, tablet, mobile) +- [ ] Form creazione scenario guidato +- [ ] Dashboard metriche in tempo reale con grafici +- [ ] Lista scenari con azioni rapide (view, duplicate, delete) +- [ ] Dark/Light mode + +### US-007: Confronto Scenari +**Come** decision maker, +**voglio** confrontare due o più scenari side-by-side, +**per** valutare l'impatto di diverse configurazioni. + +**Acceptance Criteria:** +- [ ] Selezione multipla scenari da confrontare +- [ ] Vista comparativa costi totali e per servizio +- [ ] Indicatore delta (costo maggiore/minore) con percentuale +- [ ] Export confronto in PDF + +### US-008: Rilevamento Data Leak +**Come** security engineer, +**voglio** che il sistema rilevi automaticamente email nei log, +**per** prevenire l'esposizione di PII ai servizi cloud. + +**Acceptance Criteria:** +- [ ] Il sistema identifica pattern email (es. `user@example.com`) +- [ ] Incrementa un contatore di violazioni safety +- [ ] Il contatore è visibile nel report dello scenario +- [ ] Possibilità di configurare pattern PII custom + +### US-009: Simulazione Batching +**Come** system architect, +**voglio** simulare l'effetto della deduplicazione sui costi LLM, +**per** valutare se implementare dedup in produzione. + +**Acceptance Criteria:** +- [ ] Posso inviare log multipli a uno scenario +- [ ] Il sistema conta i token solo per messaggi unici (deduplicazione) +- [ ] Vedo nel report il numero di invocazioni Lambda simulate +- [ ] Posso confrontare costi con/senza deduplicazione + +--- + +## 4. Requisiti Funzionali + +### 4.1 Gestione Scenari + +#### REQ-001: Creazione Scenario +**Priorità:** Alta + +**Descrizione:** +Sistema per creare e gestire scenari di simulazione isolati. + +**Criteri di Accettazione:** +- [ ] Modello dati Scenario: id, nome, descrizione, tag[], stato, created_at, updated_at +- [ ] Parametri configurabili: regione AWS, tier servizi, durata stimata +- [ ] API POST `/scenarios` per creazione +- [ ] Validazione nome univoco per utente + +#### REQ-002: Associazione Log a Scenario +**Priorità:** Alta + +**Descrizione:** +I log ingestiti devono essere associati a uno scenario specifico. + +**Criteri di Accettazione:** +- [ ] Header `X-Scenario-ID` o query param `scenario_id` nell'endpoint `/ingest` +- [ ] Se non specificato, crea scenario "default" o rifiuta +- [ ] Metriche aggregate per scenario in database +- [ ] Isolamento completo dati tra scenari diversi + +#### REQ-003: Stati Scenario +**Priorità:** Media + +**Descrizione:** +Gestione del ciclo di vita di uno scenario. + +**Criteri di Accettazione:** +- [ ] Stati: `draft`, `running`, `completed`, `archived` +- [ ] Transizioni: draft→running→completed→archived +- [ ] Solo scenari `running` accettano nuovi log +- [ ] Scenari `archived` in sola lettura + +### 4.2 Database e Persistenza + +#### REQ-004: Modello Dati Scenari +**Priorità:** Alta + +**Descrizione:** +Tabella/Collection per salvare i metadati degli scenari. + +**Criteri di Accettazione:** +```sql +scenarios ( + id: UUID PK, + name: VARCHAR(255) NOT NULL, + description: TEXT, + tags: JSON ARRAY, + status: ENUM('draft', 'running', 'completed', 'archived'), + region: VARCHAR(50), -- es. 'us-east-1' + created_at: TIMESTAMP, + updated_at: TIMESTAMP, + completed_at: TIMESTAMP NULL, + total_requests: INTEGER DEFAULT 0, + total_cost_estimate: DECIMAL(10,4) +) +``` + +#### REQ-005: Modello Dati Metriche +**Priorità:** Alta + +**Descrizione:** +Tabella per salvare le metriche dettagliate per scenario. + +**Criteri di Accettazione:** +```sql +scenario_metrics ( + id: UUID PK, + scenario_id: UUID FK, + timestamp: TIMESTAMP, + metric_type: VARCHAR(50), -- 'sqs', 'lambda', 'bedrock', 'safety' + metric_name: VARCHAR(100), + value: DECIMAL(15,4), + unit: VARCHAR(20), -- 'count', 'bytes', 'tokens', 'usd' + metadata: JSON -- dettagli aggiuntivi +) +``` + +#### REQ-006: Modello Dati Log Ricevuti +**Priorità:** Media + +**Descrizione:** +Storage dei log ricevuti (opzionale, configurabile retention). + +**Criteri di Accettazione:** +```sql +scenario_logs ( + id: UUID PK, + scenario_id: UUID FK, + received_at: TIMESTAMP, + message_hash: VARCHAR(64), -- SHA256 per deduplicazione + message_preview: VARCHAR(500), -- troncato per privacy + source: VARCHAR(100), + size_bytes: INTEGER, + has_pii: BOOLEAN, + token_count: INTEGER, + sqs_blocks: INTEGER +) +``` + +### 4.3 Tabella Costi AWS + +#### REQ-007: Tabella Prezzi Servizi AWS +**Priorità:** Alta + +**Descrizione:** +Tabella configurabile con i prezzi reali dei servizi AWS per regione. + +**Criteri di Accettazione:** +```sql +aws_pricing ( + id: UUID PK, + service: VARCHAR(50), -- 'sqs', 'lambda', 'bedrock' + region: VARCHAR(50), -- 'us-east-1', 'eu-west-1' + tier: VARCHAR(50), -- 'standard', 'on-demand' + price_per_unit: DECIMAL(10,8), + unit: VARCHAR(20), -- 'request', 'token', 'gb-second' + effective_from: DATE, + effective_to: DATE NULL, + is_active: BOOLEAN DEFAULT true, + source_url: VARCHAR(500) -- link documentazione AWS +) +``` + +**Prezzi Iniziali (Esempio):** + +| Servizio | Regione | Tier | Prezzo | Unità | +|----------|---------|------|--------|-------| +| SQS | us-east-1 | Standard | $0.40 | per milione richieste | +| Lambda | us-east-1 | x86 | $0.0000166667 | per GB-second | +| Lambda | us-east-1 | Request | $0.20 | per milione richieste | +| Bedrock | us-east-1 | Claude 3 Sonnet | $0.003 | per 1K token input | +| Bedrock | us-east-1 | Claude 3 Sonnet | $0.015 | per 1K token output | + +#### REQ-008: Calcolo Costi con Prezzi Reali +**Priorità:** Alta + +**Descrizione:** +Calcolo automatico dei costi usando i prezzi dalla tabella aws_pricing. + +**Criteri di Accettazione:** +- [ ] Calcolo costo SQS: `blocks * price_per_million / 1000000` +- [ ] Calcolo costo Lambda: `invocations * price_per_request + gb_seconds * price_per_gb_second` +- [ ] Calcolo costo Bedrock: `input_tokens * price_per_1k_input / 1000 + output_tokens * price_per_1k_output / 1000` +- [ ] Supporto multi-regione (prezzi diversi per regione) +- [ ] Totale costo stima aggregato per scenario + +#### REQ-009: Aggiornamento Prezzi +**Priorità:** Media + +**Descrizione:** +Interfaccia per aggiornare i prezzi AWS manualmente o via API. + +**Criteri di Accettazione:** +- [ ] API CRUD per gestione prezzi (protetta da auth) +- [ ] Validazione prezzi > 0 +- [ ] Storico prezzi (effective_from/to) +- [ ] Possibilità di importare prezzi da CSV + +### 4.4 Interfaccia Web + +#### REQ-010: Dashboard Principale +**Priorità:** Alta + +**Descrizione:** +Pagina principale con overview di tutti gli scenari. + +**Criteri di Accettazione:** +- [ ] Lista scenari con: nome, stato, data creazione, costo totale stimato +- [ ] Filtri rapidi: tutti, running, completati, archiviati +- [ ] Grafico trend costi ultimi 30 giorni +- [ ] Card riepilogo: totale scenari, costo medio, scenario più costoso +- [ ] Bottone "Nuovo Scenario" prominente + +#### REQ-011: Form Creazione Scenario +**Priorità:** Alta + +**Descrizione:** +Form guidato per creare nuovo scenario. + +**Criteri di Accettazione:** +- [ ] Step 1: Info base (nome, descrizione, tag) +- [ ] Step 2: Configurazione AWS (regione, tier servizi) +- [ ] Step 3: Parametri simulazione (durata, volume atteso) +- [ ] Validazione campi obbligatori +- [ ] Preview costo base stimato +- [ ] Bottone "Crea e Avvia" + +#### REQ-012: Vista Dettaglio Scenario +**Priorità:** Alta + +**Descrizione:** +Pagina dettaglio con tutte le metriche di uno scenario. + +**Criteri di Accettazione:** +- [ ] Header: nome, stato, date, azioni (start, stop, archive, delete) +- [ ] Tab "Overview": metriche principali, grafici tempo reale +- [ ] Tab "Costi": breakdown per servizio, grafici a torta/barre +- [ ] Tab "Logs": lista log ricevuti (paginata, con filtri) +- [ ] Tab "PII": report violazioni sicurezza rilevate +- [ ] Tab "Report": generazione e download report + +#### REQ-013: Confronto Scenari +**Priorità:** Media + +**Descrizione:** +Vista comparativa di multipli scenari. + +**Criteri di Accettazione:** +- [ ] Selezione multipla scenari da lista (checkbox) +- [ ] Vista side-by-side o tabella comparativa +- [ ] Grafici comparativi: costo totale, costo per servizio +- [ ] Indicatori delta: +15% più costoso, -10% più economico +- [ ] Bottone "Export Confronto PDF" + +#### REQ-014: Pagina Report +**Priorità:** Media + +**Descrizione:** +Generazione e gestione report. + +**Criteri di Accettazione:** +- [ ] Configurazione report: formato (PDF/CSV), data range, dettaglio +- [ ] Preview report prima del download +- [ ] Download diretto +- [ ] Storico report generati +- [ ] Possibilità di aggiungere note personalizzate + +#### REQ-015: Impostazioni Sistema +**Priorità:** Bassa + +**Descrizione:** +Pagina configurazione applicazione. + +**Criteri di Accettazione:** +- [ ] Tab "Prezzi AWS": visualizzazione e modifica tabella prezzi +- [ ] Tab "Database": info storage usato, retention policy +- [ ] Tab "API": gestione API keys, webhook +- [ ] Tab "Tema": dark/light mode, accent color + +### 4.5 API Backend (Aggiunte) + +#### REQ-016: API Scenari +**Priorità:** Alta + +**Criteri di Accettazione:** +- [ ] `GET /api/v1/scenarios` - Lista scenari (con paginazione, filtri) +- [ ] `POST /api/v1/scenarios` - Crea nuovo scenario +- [ ] `GET /api/v1/scenarios/{id}` - Dettaglio scenario +- [ ] `PUT /api/v1/scenarios/{id}` - Aggiorna scenario +- [ ] `DELETE /api/v1/scenarios/{id}` - Elimina scenario +- [ ] `POST /api/v1/scenarios/{id}/start` - Avvia scenario +- [ ] `POST /api/v1/scenarios/{id}/stop` - Ferma scenario +- [ ] `POST /api/v1/scenarios/{id}/archive` - Archivia scenario + +#### REQ-017: API Report +**Priorità:** Media + +**Criteri di Accettazione:** +- [ ] `POST /api/v1/scenarios/{id}/reports` - Genera nuovo report +- [ ] `GET /api/v1/scenarios/{id}/reports` - Lista report generati +- [ ] `GET /api/v1/reports/{report_id}/download` - Download report +- [ ] `GET /api/v1/scenarios/compare?ids=id1,id2` - Confronto scenari + +#### REQ-018: API Costi +**Priorità:** Media + +**Criteri di Accettazione:** +- [ ] `GET /api/v1/pricing` - Lista prezzi AWS configurati +- [ ] `PUT /api/v1/pricing/{id}` - Aggiorna prezzo +- [ ] `POST /api/v1/pricing/bulk-update` - Aggiornamento multiplo +- [ ] `GET /api/v1/scenarios/{id}/cost-breakdown` - Dettaglio costi scenario + +### 4.6 Funzionalità Esistenti (v0.1) + +#### REQ-019: Endpoint Ingestion (Aggiornato) +**Priorità:** Alta + +**Descrizione:** +Endpoint HTTP POST `/ingest` aggiornato per supportare scenari. + +**Criteri di Accettazione:** +- [ ] Accetta header `X-Scenario-ID: ` +- [ ] Se scenario non trovato o non running → HTTP 400 +- [ ] Log salvato in database associato allo scenario +- [ ] Metriche aggiornate in tempo reale + +#### REQ-020: Calcolo Metriche (Persistenza) +**Priorità:** Alta + +**Descrizione:** +Le metriche calcolate devono essere salvate in database. + +**Criteri di Accettazione:** +- [ ] Ogni log salvato in `scenario_logs` +- [ ] Metriche aggregate salvate in `scenario_metrics` +- [ ] Aggiornamento costo totale in `scenarios.total_cost_estimate` +- [ ] Deduplicazione basata su hash messaggio + +--- + +## 5. Requisiti Non Funzionali + +### 5.1 Performance +- **Throughput:** Supporto per almeno 1000 richieste/secondo +- **Latenza:** P95 < 100ms per endpoint `/ingest` +- **Latenza UI:** P95 < 2s per caricamento pagine web +- **Memoria:** Uso memoria < 1GB per traffico normale +- **Generazione Report:** < 5s per scenario con 100k log +- **CPU:** Basso overhead rispetto a semplice pass-through + +### 5.2 Sicurezza +- **Validazione input:** Sanitizzazione payload JSON e parametri UI +- **PII Detection:** Rilevamento pattern email nei log +- **Persistenza:** Crittografia dati sensibili in database (hash messaggi) +- **Autenticazione:** JWT o session-based per UI web +- **Autorizzazione:** Ruoli (admin, user, readonly) +- **API Keys:** Per accesso programmatico protetto + +### 5.3 Affidabilità +- **Disponibilità:** 99.9% uptime +- **Backup:** Backup giornaliero database +- **Recovery:** Restore database < 1 ora +- **Test Coverage:** > 80% copertura test automatici + +### 5.4 Manutenibilità +- **Codice:** Type hints obbligatori, follow PEP8 +- **Documentazione:** Docstring per tutte le funzioni pubbliche +- **TDD:** Test scritti prima dell'implementazione +- **Git:** Conventional Commits (`feat:`, `fix:`, `test:`) +- **Migrazioni:** Alembic per schema database + +### 5.5 Usabilità +- **UI:** Responsive design (mobile, tablet, desktop) +- **Temi:** Dark/Light mode +- **Accessibilità:** WCAG 2.1 AA compliance +- **I18n:** Preparazione per multi-lingua (italiano/inglese) +- **Errori:** Messaggi chiari, log dettagliati + +--- + +## 6. Stack Tecnologico + +### 6.1 Backend + +| Componente | Tecnologia | Versione | Motivazione | +|------------|------------|----------|-------------| +| Framework Web | FastAPI | >=0.110.0 | Async nativo, validazione Pydantic, docs auto | +| Server ASGI | Uvicorn | >=0.29.0 | Performance, supporto async | +| Validazione | Pydantic | >=2.7.0 | Type safety, serializzazione | +| Tokenizer | tiktoken | >=0.6.0 | Compatibile OpenAI/Bedrock | +| Testing | pytest | >=8.1.1 | TDD, fixtures, coverage | +| HTTP Client | httpx | >=0.27.0 | Async testing | +| Package Manager | uv | latest | Veloce, gestione dipendenze moderna | +| Python | CPython | >=3.11 | Type hints avanzati, performance | + +### 6.2 Database + +| Componente | Tecnologia | Versione | Motivazione | +|------------|------------|----------|-------------| +| Database | PostgreSQL | >=15 | ACID, JSON support, performance | +| ORM | SQLAlchemy | >=2.0 | Standard de-facto, async support | +| Migrazioni | Alembic | latest | Gestione schema database | +| Driver | asyncpg | latest | Driver async per PostgreSQL | + +### 6.3 Frontend + +| Componente | Tecnologia | Versione | Motivazione | +|------------|------------|----------|-------------| +| Framework | React | >=18 | Component-based, ecosystem | +| Build Tool | Vite | latest | Veloce, HMR | +| UI Library | Tailwind CSS | >=3.4 | Utility-first, customizzabile | +| Componenti | shadcn/ui | latest | Accessible, customizable | +| Charts | Recharts | latest | React-native, D3-based | +| State | React Query | latest | Server state management | +| HTTP Client | Axios | latest | Standard, interceptors | + +### 6.4 DevOps + +| Componente | Tecnologia | Versione | Motivazione | +|------------|------------|----------|-------------| +| Container | Docker | latest | Isolamento, portabilità | +| Compose | Docker Compose | latest | Multi-container dev | +| Reverse Proxy | Nginx | latest | Static files, SSL | + +--- + +## 7. Principi di Design + +### 7.1 Safety First +Tutti i payload in ingresso vengono validati. I log contenenti possibili dati sensibili (PII) vengono identificati e tracciati senza bloccare il flusso. I dati persistenti sono minimizzati (hash al posto di contenuti completi). + +### 7.2 Little Often +Il processamento avviene in piccoli batch frequenti, simulando l'approccio ottimale per Lambda e minimizzando i costi. I report sono generati on-demand e non in background per risparmiare risorse. + +### 7.3 Double Check +Ogni calcolo critico (token count, billing blocks, costi) è verificato con test specifici. Le stime devono essere accurate al 95%+ rispetto ai costi AWS reali. I prezzi AWS sono verificabili e aggiornabili. + +### 7.4 Data-Driven Decisions +L'interfaccia web facilita il confronto tra scenari per supportare decisioni basate sui dati. Ogni scenari è tracciabile e confrontabile nel tempo. + +--- + +## 8. Architettura di Alto Livello + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ CLIENT LAYER │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────────┐ │ +│ │ Logstash │ │ React Web UI │ │ API Consumers │ │ +│ │ (o Client) │ │ (Browser) │ │ (CI/CD, Scripts) │ │ +│ └────────┬────────┘ └────────┬────────┘ └────────────┬────────────┘ │ +└───────────┼────────────────────┼────────────────────────┼──────────────┘ + │ │ │ + │ HTTP POST │ HTTPS │ API Key + │ /ingest │ /api/v1/* │ /api/v1/* + ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ API LAYER (FastAPI) │ +│ ┌────────────────────────────────────────────────────────────────────┐ │ +│ │ Authentication & Authorization (JWT / API Key) │ │ +│ └────────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌──────────────────┐ │ +│ │ /scenarios │ │ /ingest │ │ /reports │ │ /pricing │ │ +│ │ CRUD │ │ (scenario) │ │ generate │ │ CRUD │ │ +│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └────────┬─────────┘ │ +└─────────┼───────────────┼───────────────┼─────────────────┼────────────┘ + │ │ │ │ + ▼ ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ SERVICE LAYER │ +│ ┌────────────────┐ ┌────────────────┐ ┌──────────────────────────────┐ │ +│ │ Scenario │ │ Ingestion │ │ Cost Calculator │ │ +│ │ Service │ │ Service │ │ (AWS Pricing) │ │ +│ │ - CRUD │ │ - Queue │ │ - SQS cost │ │ +│ │ - Lifecycle │ │ - Batch │ │ - Lambda cost │ │ +│ │ - Validation │ │ - Deduplicate │ │ - Bedrock cost │ │ +│ └────────────────┘ └────────────────┘ └──────────────────────────────┘ │ +│ ┌────────────────┐ ┌────────────────┐ ┌──────────────────────────────┐ │ +│ │ Report │ │ PII Detector │ │ Tokenizer │ │ +│ │ Generator │ │ (Safety) │ │ (tiktoken) │ │ +│ │ - PDF │ │ - Email check │ │ - cl100k_base │ │ +│ │ - CSV │ │ - Pattern │ │ - Token count │ │ +│ └────────────────┘ └────────────────┘ └──────────────────────────────┘ │ +└─────────┬───────────────────────────────────────────────────┬───────────┘ + │ │ + ▼ ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ DATABASE LAYER (PostgreSQL) │ +│ ┌────────────────┐ ┌────────────────┐ ┌──────────────────────────────┐ │ +│ │ scenarios │ │ scenario_logs │ │ aws_pricing │ │ +│ │ - metadata │ │ - logs │ │ - prices │ │ +│ │ - config │ │ - metrics │ │ - history │ │ +│ └────────────────┘ └────────────────┘ └──────────────────────────────┘ │ +│ ┌────────────────┐ ┌────────────────┐ │ +│ │ scenario_ │ │ reports │ │ +│ │ metrics │ │ - generated │ │ │ +│ │ - aggregated │ │ - metadata │ │ │ +│ └────────────────┘ └────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 9. Criteri di Accettazione Release + +### v0.2.0 - Scenari e Persistenza + +- [ ] Database PostgreSQL configurato e funzionante +- [ ] Tabelle scenarios, scenario_logs, scenario_metrics create +- [ ] API CRUD scenari complete (/scenarios) +- [ ] Associazione log a scenario funzionante +- [ ] Tabella aws_pricing con dati reali popolata +- [ ] Calcolo costi usando prezzi dalla tabella +- [ ] Storicizzazione automatica metriche +- [ ] Test coverage > 80% +- [ ] Documentazione API (OpenAPI) + +### v0.3.0 - Interfaccia Web + +- [ ] Dashboard React funzionante +- [ ] Form creazione scenario +- [ ] Vista dettaglio scenario con metriche +- [ ] Lista scenari con filtri +- [ ] Dark/Light mode +- [ ] Responsive design +- [ ] Export report CSV + +### v0.4.0 - Report e Confronto + +- [ ] Generazione report PDF +- [ ] Pagina confronto scenari +- [ ] Grafici interattivi (Recharts) +- [ ] API confronto scenari +- [ ] Export confronto PDF + +### v1.0.0 - Produzione + +- [ ] Autenticazione e autorizzazione +- [ ] API Keys management +- [ ] Backup automatico database +- [ ] Monitoraggio e alerting +- [ ] Documentazione utente completa +- [ ] Performance test superati + +--- + +## 10. Roadmap + +### Q2 2026 +- **Maggio:** Database, API scenari, persistenza metriche +- **Giugno:** Tabella prezzi AWS, calcolo costi, test integrazione + +### Q3 2026 +- **Luglio:** Frontend React base, dashboard, lista scenari +- **Agosto:** Form creazione, vista dettaglio, grafici +- **Settembre:** Report PDF, confronto scenari, polish UI + +### Q4 2026 +- **Ottobre:** Auth, security, API keys +- **Novembre:** Performance optimization, caching +- **Dicembre:** Bug fixing, documentazione, release v1.0 + +### Future Releases (post-v1.0) +- [ ] Supporto multi-utente con workspace +- [ ] Integration test con vero account AWS (read-only) +- [ ] Machine learning per predizione costi +- [ ] Alerting automatico se costo supera soglia +- [ ] Supporto Azure e GCP oltre ad AWS + +--- + +## 11. Appendici + +### 11.1 Schema Database Completo (ER Diagram) + +``` +┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ scenarios │ │ scenario_metrics │ │ scenario_logs │ +├─────────────────┤ ├──────────────────┤ ├─────────────────┤ +│ PK id │◄──────┤ FK scenario_id │ │ FK scenario_id │ +│ name │ │ timestamp │ │ received_at │ +│ description │ │ metric_type │ │ message_hash │ +│ tags[] │ │ metric_name │ │ preview │ +│ status │ │ value │ │ source │ +│ region │ │ unit │ │ size_bytes │ +│ created_at │ │ metadata │ │ has_pii │ +│ updated_at │ └──────────────────┘ │ token_count │ +│ total_cost │ └─────────────────┘ +└─────────────────┘ + │ + │ + ▼ +┌─────────────────┐ ┌──────────────────┐ +│ aws_pricing │ │ reports │ +├─────────────────┤ ├──────────────────┤ +│ PK id │ │ PK id │ +│ service │ │ FK scenario_id │ +│ region │ │ format │ +│ tier │ │ file_path │ +│ price │ │ generated_at │ +│ unit │ │ metadata │ +│ effective_ │ └──────────────────┘ +│ from/to │ +│ is_active │ +└─────────────────┘ +``` + +### 11.2 Esempio Flusso Completo + +1. **Creazione Scenario:** + ```bash + POST /api/v1/scenarios + { + "name": "Produzione Q2", + "description": "Simulazione traffico produzione", + "region": "us-east-1", + "tags": ["production", "q2"] + } + → Response: { "id": "uuid-scenario-123" } + ``` + +2. **Invio Log:** + ```bash + POST /ingest + Header: X-Scenario-ID: uuid-scenario-123 + Body: { "message": "Error: connection timeout", "source": "api" } + ``` + +3. **Visualizzazione Web:** + - User apre dashboard + - Seleziona scenario "Produzione Q2" + - Vede metriche in tempo reale + - Grafici costi per servizio + +4. **Generazione Report:** + ```bash + POST /api/v1/scenarios/uuid-scenario-123/reports + { "format": "pdf", "include_logs": false } + ``` + → Download report completo + +--- + +*Documento mantenuto dal team LogWhispererAI. Ultimo aggiornamento: 2026-04-07* diff --git a/export/progress.md b/export/progress.md new file mode 100644 index 0000000..ae16370 --- /dev/null +++ b/export/progress.md @@ -0,0 +1,109 @@ +# Progress Tracking - [NOME_PROGETTO] + +> Tracciamento progresso sviluppo in tempo reale. + +## 🎯 Sprint/Feature Corrente + +**Feature:** `[Nome feature in sviluppo]` +**Iniziata:** `YYYY-MM-DD` +**Stato:** 🔴 Pianificazione / 🟡 In sviluppo / 🟢 Completata +**Assegnato:** `@agent` + +--- + +## 📊 Progresso Complessivo + +| Area | Progresso | Stato | +|------|-----------|-------| +| API Core | 0/10 task | 🔴 Non iniziato | +| Webhook System | 0/5 task | 🔴 Non iniziato | +| AI Skill | 0/3 task | 🔴 Non iniziato | +| Testing | 0/8 task | 🔴 Non iniziato | +| Documentazione | 0/4 task | 🔴 Non iniziato | + +**Completamento Totale:** 0% + +--- + +## 🔄 Attività in Corso + +### Task Corrente: `[ID-XXX] - Titolo` + +| Campo | Valore | +|-------|--------| +| **ID** | TASK-XXX | +| **Descrizione** | [Breve descrizione] | +| **Iniziata** | YYYY-MM-DD HH:MM | +| **Assegnato** | @agent | +| **Stato** | 🟡 In progress | +| **Bloccata da** | Nessuna / TASK-YYY | +| **Note** | [Eventuali ostacoli, decisioni] | + +**Passi completati:** +- [x] Passo 1 +- [ ] Passo 2 +- [ ] Passo 3 + +--- + +## ✅ Task Completate (Oggi) + +| ID | Task | Completata | Commit | Assegnato | +|----|------|------------|--------|-----------| +| | | | | | + +--- + +## 📅 Prossime Task + +| Priority | ID | Task | Stima | Dipendenze | +|----------|----|------|-------|------------| +| P1 | | | | | +| P2 | | | | | + +--- + +## 🚧 Blocchi/Issue + +| ID | Problema | Impatto | Soluzione Proposta | Stato | +|----|----------|---------|-------------------|-------| +| | | | | 🔴 Aperto | + +--- + +## 📝 Decisioni Prese Oggi + +| Data | Decisione | Motivazione | Impatto | +|------|-----------|-------------|---------| +| | | | | + +--- + +## 📈 Metriche + +### Sprint Corrente +- **Task pianificate:** 0 +- **Task completate:** 0 +- **Task in progress:** 0 +- **Task bloccate:** 0 + +### Qualità +- **Test Coverage:** 0% +- **Test passanti:** 0/0 +- **Linting:** ✅ / ❌ +- **Type Check:** ✅ / ❌ + +--- + +## 🔄 Aggiornamento + +> **Nota:** Questo file deve essere aggiornato: +> - All'inizio di ogni nuova task +> - Al completamento di ogni task +> - Quando si risolve un blocco +> - Quando si prende una decisione architetturale +> - A fine giornata lavorativa + +--- + +*Ultimo aggiornamento: YYYY-MM-DD HH:MM* diff --git a/mockupAWS/.python-version b/mockupAWS/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/mockupAWS/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/mockupAWS/README.md b/mockupAWS/README.md new file mode 100644 index 0000000..e69de29 diff --git a/mockupAWS/main.py b/mockupAWS/main.py new file mode 100644 index 0000000..e7b575f --- /dev/null +++ b/mockupAWS/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from mockupaws!") + + +if __name__ == "__main__": + main() diff --git a/mockupAWS/pyproject.toml b/mockupAWS/pyproject.toml new file mode 100644 index 0000000..33b9fa1 --- /dev/null +++ b/mockupAWS/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "mockupaws" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [] diff --git a/prompt/prompt-zero.md b/prompt/prompt-zero.md new file mode 100644 index 0000000..886c6c8 --- /dev/null +++ b/prompt/prompt-zero.md @@ -0,0 +1,224 @@ +# Prompt Zero: [NOME_PROGETTO] - Project Kickoff + +## 🎯 Missione + +Sviluppare **[NOME_PROGETTO]**, [descrizione breve del progetto - da personalizzare]. + +**Repository:** `[ROOT_PROGETTO]` +**PRD:** `[ROOT_PROGETTO]/export/prd.md` + +--- + +## 📊 Stato Attuale + +- ✅ **PRD Completo**: Requisiti funzionali e non funzionali definiti +- ✅ **Team Configurato**: Agenti specializzati pronti +- ❌ **Nessun Codice**: Progetto da zero +- ❌ **Nessuna Specifica Tecnica**: Da creare + +--- + +## 👥 Team di Sviluppo + +| Agente | Ruolo | File Config | +|--------|-------|-------------| +| `@spec-architect` | Definisce specifiche e architettura | `.opencode/agents/spec-architect.md` | +| `@tdd-developer` | Implementazione TDD | `.opencode/agents/tdd-developer.md` | +| `@git-manager` | Gestione commit Git | `.opencode/agents/git-manager.md` | + +--- + +## 🔄 Workflow Obbligatorio + +``` +┌─────────────────────────────────────────────────────────────┐ +│ FASE 1: SPECIFICA │ +│ @spec-architect │ +│ └── Legge PRD → Crea architecture.md, kanban.md │ +│ │ +│ ↓ │ +│ │ +│ FASE 2: IMPLEMENTAZIONE │ +│ @tdd-developer │ +│ └── RED → GREEN → REFACTOR per ogni task │ +│ │ +│ ↓ │ +│ │ +│ FASE 3: COMMIT │ +│ @git-manager │ +│ └── Commit atomico + Conventional Commits │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 🚀 Task Iniziale: Fase 1 - Specifica + +**AGENTE:** `@spec-architect` + +**OBIETTIVO:** Analizzare il PRD e creare le specifiche tecniche dettagliate. + +### Azioni Richieste + +1. **Leggere** `[ROOT_PROGETTO]/export/prd.md` + +2. **Creare** la struttura di output: + ``` + [ROOT_PROGETTO]/export/ + ├── prd.md # Requisiti prodotti (estratto/dettaglio) + ├── architecture.md # Architettura sistema + ├── kanban.md # Task breakdown + └── progress.md # Tracciamento progresso + ``` + +3. **Produrre** `architecture.md` con: + - Stack tecnologico dettagliato + - Struttura cartelle progetto + - Diagrammi flusso dati + - Schema database completo (DDL) + - Interfacce API (OpenAPI specs) + - Sicurezza (cifratura, autenticazione) + +4. **Produrre** `kanban.md` con: + - Task breakdown per Fase 1 (MVP) + - Stima complessità + - Dipendenze tra task + - Regola "little often": task < 2 ore + +5. **Inizializzare** `progress.md` con: + - Feature corrente: "Fase 1 - MVP" + - Stato: "🔴 Pianificazione" + - Percentuale: 0% + +### Criteri di Accettazione + +- [ ] Architecture.md completo con tutte le sezioni +- [ ] Kanban.md con task pronti per @tdd-developer +- [ ] Progress.md inizializzato +- [ ] Tutti i path usano `[ROOT_PROGETTO]/` + +--- + +## 📋 Requisiti Chiave (Dal PRD) + +### Funzionalità MVP (Fase 1) + +[DA PERSONALIZZARE - Inserire le funzionalità principali del progetto] + +1. **[Feature 1]** + - [Dettaglio 1] + - [Dettaglio 2] + +2. **[Feature 2]** + - [Dettaglio 1] + - [Dettaglio 2] + +### Stack Tecnologico (Esempio) + +- **Backend:** [Linguaggio/Framework] +- **Database:** [Tipo database] +- **Frontend:** [Tecnologia frontend] +- **Auth:** [Metodo autenticazione] +- **Task Background:** [Scheduler] + +--- + +## 🛡️ Vincoli e Best Practices + +### Sicurezza (Critico) +- [Requisito sicurezza 1] +- [Requisito sicurezza 2] +- SQL injection prevention +- XSS prevention +- CSRF protection +- Rate limiting + +### Qualità +- Test coverage ≥ 90% +- TDD obbligatorio +- Conventional commits +- Commit atomici + +### Organizzazione +- Task "little often" (< 2 ore) +- Documentazione in `/export/` +- Bug complessi in `/docs/bug_ledger.md` + +--- + +## 📁 Struttura Progetto Attesa + +``` +[ROOT_PROGETTO]/ +├── prd.md # Product Requirements +├── prompt/ +│ └── prompt-zero.md # Questo file +├── .opencode/ +│ ├── agents/ # Configurazioni agenti +│ └── skills/ # Skill condivise +├── export/ # Output spec-driven (da creare) +│ ├── prd.md +│ ├── architecture.md +│ ├── kanban.md +│ └── progress.md +├── docs/ # Documentazione (da creare) +│ ├── bug_ledger.md +│ └── architecture.md +├── src/ # Codice sorgente (da creare) +│ └── [nome_package]/ +│ ├── __init__.py +│ ├── main.py +│ ├── config.py +│ └── ... +├── tests/ # Test suite (da creare) +│ ├── unit/ +│ ├── integration/ +│ └── conftest.py +├── requirements.txt / package.json +└── README.md +``` + +--- + +## ✅ Checklist Pre-Sviluppo + +- [ ] @spec-architect ha letto questo prompt +- [ ] Cartella `export/` creata +- [ ] `architecture.md` creato con schema DB +- [ ] `kanban.md` creato con task Fase 1 +- [ ] `progress.md` inizializzato + +--- + +## 🎬 Prossima Azione + +**@spec-architect**: Inizia analizzando il PRD in `export/prd.md` e crea le specifiche tecniche in `export/`. + +**NON iniziare l'implementazione** finché le specifiche non sono approvate. + +--- + +## 📞 Note per il Team + +- **Domande sul PRD?** Leggi prima `export/prd.md` completamente +- **Ambiguità?** Chiedi prima di procedere +- **Vincoli tecnici?** Documentali in `architecture.md` +- **Task troppo grandi?** Spezza in task più piccoli + +--- + +**Data Creazione:** YYYY-MM-DD +**Versione:** 1.0 +**Stato:** Pronto per kickoff + +--- + +## 📝 Note per l'Utente + +Questo è un template. Per usarlo: + +1. **Sostituisci** `[NOME_PROGETTO]` con il nome reale +2. **Descrivi** il progetto nella sezione Missione +3. **Personalizza** la sezione "Requisiti Chiave" con i tuoi requisiti +4. **Aggiorna** lo stack tecnologico +5. **Definisci** gli scope dei commit pertinenti al tuo progetto diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d97c095 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[project] +name = "mockup-aws" +version = "0.1.0" +description = "Simulatore locale del backend AWS per LogWhispererAI." +readme = "README.md" +requires-python = ">=3.11" + +dependencies = [ + "fastapi>=0.110.0", + "pydantic>=2.7.0", + "tiktoken>=0.6.0", + "uvicorn>=0.29.0", +] + +[dependency-groups] +dev = [ + "httpx>=0.27.0", + "pytest>=8.1.1", +] + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["."] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1dcdb47 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,20 @@ +annotated-doc==0.0.4 +annotated-types==0.7.0 +anyio==4.13.0 +certifi==2026.2.25 +charset-normalizer==3.4.7 +click==8.3.2 +fastapi==0.135.3 +h11==0.16.0 +idna==3.11 +pydantic==2.12.5 +pydantic_core==2.41.5 +regex==2026.4.4 +requests==2.33.1 +starlette==1.0.0 +tiktoken==0.12.0 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +urllib3==2.6.3 +uv==0.11.3 +uvicorn==0.44.0 diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..b6be189 --- /dev/null +++ b/src/main.py @@ -0,0 +1,121 @@ +from fastapi import FastAPI +from pydantic import BaseModel +from src.profiler import count_tokens, calculate_sqs_blocks +import asyncio +from typing import Set + +app = FastAPI(title="LogWhispererAI Mockup AWS") + + +# Stato in memoria per le metriche (valido per la simulazione locale) +class Metrics(BaseModel): + total_requests: int = 0 + sqs_billing_blocks: int = 0 + safety_violations_detected: int = 0 + llm_estimated_input_tokens: int = 0 + lambda_simulated_invocations: int = 0 + + +state_metrics = Metrics() + +# Coda asincrona per simulare il batching Lambda +message_queue: list[dict] = [] +queue_lock = asyncio.Lock() +processed_messages: Set[str] = set() # Per deduplicazione + + +# Struttura attesa del log inviato da Logstash +class LogPayload(BaseModel): + message: str + source: str = "unknown" + + +async def process_batch(): + """ + Worker asincrono che simula l'invocazione Lambda. + Processa i messaggi in batch con deduplicazione. + """ + global message_queue, processed_messages + + async with queue_lock: + if not message_queue: + return + + # Prendiamo tutti i messaggi dalla coda + batch = message_queue.copy() + message_queue = [] + + # Deduplicazione: processiamo solo messaggi unici + unique_messages = {} + for msg in batch: + msg_key = msg["message"] + if msg_key not in unique_messages: + unique_messages[msg_key] = msg + + # Simuliamo l'invocazione Lambda + state_metrics.lambda_simulated_invocations += 1 + + # Calcoliamo i token solo per i messaggi unici (deduplicazione) + for msg in unique_messages.values(): + tokens = count_tokens(msg["message"]) + state_metrics.llm_estimated_input_tokens += tokens + + +async def schedule_batch_processing(): + """Schedula il processamento batch dopo un breve delay.""" + await asyncio.sleep(0.1) # Piccolo delay per accumulare messaggi + await process_batch() + + +@app.post("/ingest") +async def ingest_log(payload: LogPayload): + """ + Riceve i log da Logstash, esegue le validazioni di sicurezza + e calcola le metriche per la stima dei costi cloud. + """ + global message_queue + + # 1. Calcolo richieste in ingresso + state_metrics.total_requests += 1 + + # 2. Calcolo blocchi fatturabili SQS + payload_json_str = payload.model_dump_json() + state_metrics.sqs_billing_blocks += calculate_sqs_blocks(payload_json_str) + + # 3. Safety First: Controllo base per dati non offuscati (es. email) + if "@" in payload.message and ".com" in payload.message: + state_metrics.safety_violations_detected += 1 + + # 4. Aggiungiamo il messaggio alla coda per processamento batch (Little Often) + async with queue_lock: + message_queue.append({"message": payload.message, "source": payload.source}) + + # Scheduliamo il processamento asincrono + asyncio.create_task(schedule_batch_processing()) + + return {"status": "accepted", "message": "Log accodato per processamento"} + + +@app.get("/metrics") +async def get_metrics(): + """Restituisce le metriche correnti di profilazione.""" + return state_metrics.model_dump() + + +@app.post("/reset") +async def reset_metrics(): + """Resetta tutte le metriche (utile per i test).""" + global state_metrics, message_queue, processed_messages + + state_metrics = Metrics() + message_queue = [] + processed_messages = set() + + return {"status": "reset"} + + +@app.post("/flush") +async def flush_queue(): + """Forza il processamento immediato della coda (utile per i test).""" + await process_batch() + return {"status": "flushed"} diff --git a/src/profiler.py b/src/profiler.py new file mode 100644 index 0000000..76441b6 --- /dev/null +++ b/src/profiler.py @@ -0,0 +1,26 @@ +import sys +import tiktoken + +# Inizializziamo l'encoder globalmente. +# cl100k_base è lo standard di fatto per le misurazioni token moderne. +_encoder = tiktoken.get_encoding("cl100k_base") + +def count_tokens(text: str) -> int: + """ + Calcola il numero esatto di token in ingresso per una data stringa. + Fondamentale per il calcolo accurato dei costi LLM. + """ + if not text: + return 0 + return len(_encoder.encode(text)) + +def calculate_sqs_blocks(payload_json: str) -> int: + """ + Calcola i blocchi fatturabili per Amazon SQS. + AWS addebita 1 richiesta per ogni payload (o frammento) fino a 64 KB (65536 bytes). + """ + # sys.getsizeof restituisce la dimensione in byte della stringa in memoria + payload_size_bytes = sys.getsizeof(payload_json) + + # Calcolo dei blocchi (divisione intera + 1) + return (payload_size_bytes // 65536) + 1 diff --git a/test/test_ingest.py b/test/test_ingest.py new file mode 100644 index 0000000..e3ac773 --- /dev/null +++ b/test/test_ingest.py @@ -0,0 +1,97 @@ +import pytest +from fastapi.testclient import TestClient +from src.main import app + +# Inizializziamo il client di test di FastAPI +client = TestClient(app) + + +@pytest.fixture(autouse=True) +def reset_metrics(): + """Resetta i contatori prima di ogni test per garantire isolamento.""" + client.post("/reset") + + +def test_ingest_returns_200(): + """Verifica che il webhook accetti il payload e risponda velocemente.""" + payload = {"message": "Standard error log", "source": "nginx"} + response = client.post("/ingest", json=payload) + assert response.status_code == 200 + + +def test_sqs_billing_block_calculation(): + """ + Simula l'invio di un payload > 64KB. + AWS SQS fattura a blocchi di 64KB. 65KB = 2 blocchi. + """ + # 65 * 1024 bytes = 66560 caratteri (circa 65KB) + large_payload = {"message": "A" * 66560} + client.post("/ingest", json=large_payload) + + response = client.get("/metrics") + metrics = response.json() + + # Ci aspettiamo 2 blocchi fatturabili + assert metrics["sqs_billing_blocks"] == 2 + + +def test_safety_first_leak_detection(): + """ + Verifica che se Logstash fallisce la sanitizzazione, + il mock lo rilevi e incrementi il contatore delle violazioni (Safety First). + """ + leaky_payload = {"message": "User luca@example.com failed login"} + client.post("/ingest", json=leaky_payload) + + response = client.get("/metrics") + metrics = response.json() + + assert metrics["safety_violations_detected"] == 1 + + +def test_double_check_token_count(): + """ + Verifica che il tokenizer calcoli esattamente il numero di token. + La stringa 'Hello, world!' corrisponde a 4 token in cl100k_base. + (Double Check) + """ + payload = {"message": "Hello, world!"} + + client.post("/ingest", json=payload) + + # Forziamo il processamento della coda + client.post("/flush") + + response = client.get("/metrics") + metrics = response.json() + + assert metrics["llm_estimated_input_tokens"] == 4 + + +def test_little_often_batch_deduplication(): + """ + Simula l'invio di 10 log identici in rapida successione. + Verifica che vengano deduplicati e che i token LLM + siano contati per 1 solo log, simulando il batching (Little Often). + """ + payload = {"message": "Hello, world!"} + + # Inviamo 10 messaggi identici + for _ in range(10): + client.post("/ingest", json=payload) + + # Forziamo il processamento della coda + client.post("/flush") + + response = client.get("/metrics") + metrics = response.json() + + # 1. 10 richieste ricevute dal mock SQS + assert metrics["total_requests"] == 10 + + # 2. Almeno 1 invocazione Lambda simulata + assert metrics.get("lambda_simulated_invocations", 0) > 0 + + # 3. I token stimati devono essere solo 4 (quelli di 1 singolo messaggio), + # NON 40 (che avremmo senza deduplicazione). + assert metrics["llm_estimated_input_tokens"] == 4 diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..973a7b6 --- /dev/null +++ b/uv.lock @@ -0,0 +1,639 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "fastapi" +version = "0.135.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/e6/7adb4c5fa231e82c35b8f5741a9f2d055f520c29af5546fd70d3e8e1cd2e/fastapi-0.135.3.tar.gz", hash = "sha256:bd6d7caf1a2bdd8d676843cdcd2287729572a1ef524fc4d65c17ae002a1be654", size = 396524, upload-time = "2026-04-01T16:23:58.188Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/a4/5caa2de7f917a04ada20018eccf60d6cc6145b0199d55ca3711b0fc08312/fastapi-0.135.3-py3-none-any.whl", hash = "sha256:9b0f590c813acd13d0ab43dd8494138eb58e484bfac405db1f3187cfc5810d98", size = 117734, upload-time = "2026-04-01T16:23:59.328Z" }, +] + +[[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.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "mockup-aws" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "fastapi" }, + { name = "pydantic" }, + { name = "tiktoken" }, + { name = "uvicorn" }, +] + +[package.dev-dependencies] +dev = [ + { name = "httpx" }, + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.110.0" }, + { name = "pydantic", specifier = ">=2.7.0" }, + { name = "tiktoken", specifier = ">=0.6.0" }, + { name = "uvicorn", specifier = ">=0.29.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "httpx", specifier = ">=0.27.0" }, + { name = "pytest", specifier = ">=8.1.1" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "regex" +version = "2026.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/3a246dbf05666918bd3664d9d787f84a9108f6f43cc953a077e4a7dfdb7e/regex-2026.4.4.tar.gz", hash = "sha256:e08270659717f6973523ce3afbafa53515c4dc5dcad637dc215b6fd50f689423", size = 416000, upload-time = "2026-04-03T20:56:28.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/7a/617356cbecdb452812a5d42f720d6d5096b360d4a4c1073af700ea140ad2/regex-2026.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4c36a85b00fadb85db9d9e90144af0a980e1a3d2ef9cd0f8a5bef88054657c6", size = 489415, upload-time = "2026-04-03T20:53:11.645Z" }, + { url = "https://files.pythonhosted.org/packages/20/e6/bf057227144d02e3ba758b66649e87531d744dda5f3254f48660f18ae9d8/regex-2026.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dcb5453ecf9cd58b562967badd1edbf092b0588a3af9e32ee3d05c985077ce87", size = 291205, upload-time = "2026-04-03T20:53:13.289Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3b/637181b787dd1a820ba1c712cee2b4144cd84a32dc776ca067b12b2d70c8/regex-2026.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6aa809ed4dc3706cc38594d67e641601bd2f36d5555b2780ff074edfcb136cf8", size = 289225, upload-time = "2026-04-03T20:53:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/05/21/bac05d806ed02cd4b39d9c8e5b5f9a2998c94c3a351b7792e80671fa5315/regex-2026.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33424f5188a7db12958246a54f59a435b6cb62c5cf9c8d71f7cc49475a5fdada", size = 792434, upload-time = "2026-04-03T20:53:17.414Z" }, + { url = "https://files.pythonhosted.org/packages/d9/17/c65d1d8ae90b772d5758eb4014e1e011bb2db353fc4455432e6cc9100df7/regex-2026.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d346fccdde28abba117cc9edc696b9518c3307fbfcb689e549d9b5979018c6d", size = 861730, upload-time = "2026-04-03T20:53:18.903Z" }, + { url = "https://files.pythonhosted.org/packages/ad/64/933321aa082a2c6ee2785f22776143ba89840189c20d3b6b1d12b6aae16b/regex-2026.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:415a994b536440f5011aa77e50a4274d15da3245e876e5c7f19da349caaedd87", size = 906495, upload-time = "2026-04-03T20:53:20.561Z" }, + { url = "https://files.pythonhosted.org/packages/01/ea/4c8d306e9c36ac22417336b1e02e7b358152c34dc379673f2d331143725f/regex-2026.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21e5eb86179b4c67b5759d452ea7c48eb135cd93308e7a260aa489ed2eb423a4", size = 799810, upload-time = "2026-04-03T20:53:22.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/ce/7605048f00e1379eba89d610c7d644d8f695dc9b26d3b6ecfa3132b872ff/regex-2026.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:312ec9dd1ae7d96abd8c5a36a552b2139931914407d26fba723f9e53c8186f86", size = 774242, upload-time = "2026-04-03T20:53:25.015Z" }, + { url = "https://files.pythonhosted.org/packages/e9/77/283e0d5023fde22cd9e86190d6d9beb21590a452b195ffe00274de470691/regex-2026.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0d2b28aa1354c7cd7f71b7658c4326f7facac106edd7f40eda984424229fd59", size = 781257, upload-time = "2026-04-03T20:53:26.918Z" }, + { url = "https://files.pythonhosted.org/packages/8b/fb/7f3b772be101373c8626ed34c5d727dcbb8abd42a7b1219bc25fd9a3cc04/regex-2026.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:349d7310eddff40429a099c08d995c6d4a4bfaf3ff40bd3b5e5cb5a5a3c7d453", size = 854490, upload-time = "2026-04-03T20:53:29.065Z" }, + { url = "https://files.pythonhosted.org/packages/85/30/56547b80f34f4dd2986e1cdd63b1712932f63b6c4ce2f79c50a6cd79d1c2/regex-2026.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e7ab63e9fe45a9ec3417509e18116b367e89c9ceb6219222a3396fa30b147f80", size = 763544, upload-time = "2026-04-03T20:53:30.917Z" }, + { url = "https://files.pythonhosted.org/packages/ac/2f/ce060fdfea8eff34a8997603532e44cdb7d1f35e3bc253612a8707a90538/regex-2026.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fe896e07a5a2462308297e515c0054e9ec2dd18dfdc9427b19900b37dfe6f40b", size = 844442, upload-time = "2026-04-03T20:53:32.463Z" }, + { url = "https://files.pythonhosted.org/packages/e5/44/810cb113096a1dacbe82789fbfab2823f79d19b7f1271acecb7009ba9b88/regex-2026.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eb59c65069498dbae3c0ef07bbe224e1eaa079825a437fb47a479f0af11f774f", size = 789162, upload-time = "2026-04-03T20:53:34.039Z" }, + { url = "https://files.pythonhosted.org/packages/20/96/9647dd7f2ecf6d9ce1fb04dfdb66910d094e10d8fe53e9c15096d8aa0bd2/regex-2026.4.4-cp311-cp311-win32.whl", hash = "sha256:2a5d273181b560ef8397c8825f2b9d57013de744da9e8257b8467e5da8599351", size = 266227, upload-time = "2026-04-03T20:53:35.601Z" }, + { url = "https://files.pythonhosted.org/packages/33/80/74e13262460530c3097ff343a17de9a34d040a5dc4de9cf3a8241faab51c/regex-2026.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:9542ccc1e689e752594309444081582f7be2fdb2df75acafea8a075108566735", size = 278399, upload-time = "2026-04-03T20:53:37.021Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/39f19f47f19dcefa3403f09d13562ca1c0fd07ab54db2bc03148f3f6b46a/regex-2026.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:b5f9fb784824a042be3455b53d0b112655686fdb7a91f88f095f3fee1e2a2a54", size = 270473, upload-time = "2026-04-03T20:53:38.633Z" }, + { url = "https://files.pythonhosted.org/packages/e5/28/b972a4d3df61e1d7bcf1b59fdb3cddef22f88b6be43f161bb41ebc0e4081/regex-2026.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c07ab8794fa929e58d97a0e1796b8b76f70943fa39df225ac9964615cf1f9d52", size = 490434, upload-time = "2026-04-03T20:53:40.219Z" }, + { url = "https://files.pythonhosted.org/packages/84/20/30041446cf6dc3e0eab344fc62770e84c23b6b68a3b657821f9f80cb69b4/regex-2026.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c785939dc023a1ce4ec09599c032cc9933d258a998d16ca6f2b596c010940eb", size = 292061, upload-time = "2026-04-03T20:53:41.862Z" }, + { url = "https://files.pythonhosted.org/packages/62/c8/3baa06d75c98c46d4cc4262b71fd2edb9062b5665e868bca57859dadf93a/regex-2026.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b1ce5c81c9114f1ce2f9288a51a8fd3aeea33a0cc440c415bf02da323aa0a76", size = 289628, upload-time = "2026-04-03T20:53:43.701Z" }, + { url = "https://files.pythonhosted.org/packages/31/87/3accf55634caad8c0acab23f5135ef7d4a21c39f28c55c816ae012931408/regex-2026.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:760ef21c17d8e6a4fe8cf406a97cf2806a4df93416ccc82fc98d25b1c20425be", size = 796651, upload-time = "2026-04-03T20:53:45.379Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0c/aaa2c83f34efedbf06f61cb1942c25f6cf1ee3b200f832c4d05f28306c2e/regex-2026.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7088fcdcb604a4417c208e2169715800d28838fefd7455fbe40416231d1d47c1", size = 865916, upload-time = "2026-04-03T20:53:47.064Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f6/8c6924c865124643e8f37823eca845dc27ac509b2ee58123685e71cd0279/regex-2026.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:07edca1ba687998968f7db5bc355288d0c6505caa7374f013d27356d93976d13", size = 912287, upload-time = "2026-04-03T20:53:49.422Z" }, + { url = "https://files.pythonhosted.org/packages/11/0e/a9f6f81013e0deaf559b25711623864970fe6a098314e374ccb1540a4152/regex-2026.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f657a7c1c6ec51b5e0ba97c9817d06b84ea5fa8d82e43b9405de0defdc2b9", size = 801126, upload-time = "2026-04-03T20:53:51.096Z" }, + { url = "https://files.pythonhosted.org/packages/71/61/3a0cc8af2dc0c8deb48e644dd2521f173f7e6513c6e195aad9aa8dd77ac5/regex-2026.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b69102a743e7569ebee67e634a69c4cb7e59d6fa2e1aa7d3bdbf3f61435f62d", size = 776788, upload-time = "2026-04-03T20:53:52.889Z" }, + { url = "https://files.pythonhosted.org/packages/64/0b/8bb9cbf21ef7dee58e49b0fdb066a7aded146c823202e16494a36777594f/regex-2026.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dac006c8b6dda72d86ea3d1333d45147de79a3a3f26f10c1cf9287ca4ca0ac3", size = 785184, upload-time = "2026-04-03T20:53:55.627Z" }, + { url = "https://files.pythonhosted.org/packages/99/c2/d3e80e8137b25ee06c92627de4e4d98b94830e02b3e6f81f3d2e3f504cf5/regex-2026.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:50a766ee2010d504554bfb5f578ed2e066898aa26411d57e6296230627cdefa0", size = 859913, upload-time = "2026-04-03T20:53:57.249Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/9d5d876157d969c804622456ef250017ac7a8f83e0e14f903b9e6df5ce95/regex-2026.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9e2f5217648f68e3028c823df58663587c1507a5ba8419f4fdfc8a461be76043", size = 765732, upload-time = "2026-04-03T20:53:59.428Z" }, + { url = "https://files.pythonhosted.org/packages/82/80/b568935b4421388561c8ed42aff77247285d3ae3bb2a6ca22af63bae805e/regex-2026.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39d8de85a08e32632974151ba59c6e9140646dcc36c80423962b1c5c0a92e244", size = 852152, upload-time = "2026-04-03T20:54:01.505Z" }, + { url = "https://files.pythonhosted.org/packages/39/29/f0f81217e21cd998245da047405366385d5c6072048038a3d33b37a79dc0/regex-2026.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55d9304e0e7178dfb1e106c33edf834097ddf4a890e2f676f6c5118f84390f73", size = 789076, upload-time = "2026-04-03T20:54:03.323Z" }, + { url = "https://files.pythonhosted.org/packages/49/1d/1d957a61976ab9d4e767dd4f9d04b66cc0c41c5e36cf40e2d43688b5ae6f/regex-2026.4.4-cp312-cp312-win32.whl", hash = "sha256:04bb679bc0bde8a7bfb71e991493d47314e7b98380b083df2447cda4b6edb60f", size = 266700, upload-time = "2026-04-03T20:54:05.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/bf575d396aeb58ea13b06ef2adf624f65b70fafef6950a80fc3da9cae3bc/regex-2026.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:db0ac18435a40a2543dbb3d21e161a6c78e33e8159bd2e009343d224bb03bb1b", size = 277768, upload-time = "2026-04-03T20:54:07.312Z" }, + { url = "https://files.pythonhosted.org/packages/c9/27/049df16ec6a6828ccd72add3c7f54b4df029669bea8e9817df6fff58be90/regex-2026.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:4ce255cc05c1947a12989c6db801c96461947adb7a59990f1360b5983fab4983", size = 270568, upload-time = "2026-04-03T20:54:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/9d/83/c4373bc5f31f2cf4b66f9b7c31005bd87fe66f0dce17701f7db4ee79ee29/regex-2026.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:62f5519042c101762509b1d717b45a69c0139d60414b3c604b81328c01bd1943", size = 490273, upload-time = "2026-04-03T20:54:11.202Z" }, + { url = "https://files.pythonhosted.org/packages/46/f8/fe62afbcc3cf4ad4ac9adeaafd98aa747869ae12d3e8e2ac293d0593c435/regex-2026.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3790ba9fb5dd76715a7afe34dbe603ba03f8820764b1dc929dd08106214ed031", size = 291954, upload-time = "2026-04-03T20:54:13.412Z" }, + { url = "https://files.pythonhosted.org/packages/5a/92/4712b9fe6a33d232eeb1c189484b80c6c4b8422b90e766e1195d6e758207/regex-2026.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fae3c6e795d7678963f2170152b0d892cf6aee9ee8afc8c45e6be38d5107fe7", size = 289487, upload-time = "2026-04-03T20:54:15.824Z" }, + { url = "https://files.pythonhosted.org/packages/88/2c/f83b93f85e01168f1070f045a42d4c937b69fdb8dd7ae82d307253f7e36e/regex-2026.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:298c3ec2d53225b3bf91142eb9691025bab610e0c0c51592dde149db679b3d17", size = 796646, upload-time = "2026-04-03T20:54:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/df/55/61a2e17bf0c4dc57e11caf8dd11771280d8aaa361785f9e3bc40d653f4a7/regex-2026.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9638791082eaf5b3ac112c587518ee78e083a11c4b28012d8fe2a0f536dfb17", size = 865904, upload-time = "2026-04-03T20:54:20.019Z" }, + { url = "https://files.pythonhosted.org/packages/45/32/1ac8ed1b5a346b5993a3d256abe0a0f03b0b73c8cc88d928537368ac65b6/regex-2026.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae3e764bd4c5ff55035dc82a8d49acceb42a5298edf6eb2fc4d328ee5dd7afae", size = 912304, upload-time = "2026-04-03T20:54:22.403Z" }, + { url = "https://files.pythonhosted.org/packages/26/47/2ee5c613ab546f0eddebf9905d23e07beb933416b1246c2d8791d01979b4/regex-2026.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffa81f81b80047ba89a3c69ae6a0f78d06f4a42ce5126b0eb2a0a10ad44e0b2e", size = 801126, upload-time = "2026-04-03T20:54:24.308Z" }, + { url = "https://files.pythonhosted.org/packages/75/cd/41dacd129ca9fd20bd7d02f83e0fad83e034ac8a084ec369c90f55ef37e2/regex-2026.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f56ebf9d70305307a707911b88469213630aba821e77de7d603f9d2f0730687d", size = 776772, upload-time = "2026-04-03T20:54:26.319Z" }, + { url = "https://files.pythonhosted.org/packages/89/6d/5af0b588174cb5f46041fa7dd64d3fd5cd2fe51f18766703d1edc387f324/regex-2026.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:773d1dfd652bbffb09336abf890bfd64785c7463716bf766d0eb3bc19c8b7f27", size = 785228, upload-time = "2026-04-03T20:54:28.387Z" }, + { url = "https://files.pythonhosted.org/packages/b7/3b/f5a72b7045bd59575fc33bf1345f156fcfd5a8484aea6ad84b12c5a82114/regex-2026.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d51d20befd5275d092cdffba57ded05f3c436317ee56466c8928ac32d960edaf", size = 860032, upload-time = "2026-04-03T20:54:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/39/a4/72a317003d6fcd7a573584a85f59f525dfe8f67e355ca74eb6b53d66a5e2/regex-2026.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0a51cdb3c1e9161154f976cb2bef9894bc063ac82f31b733087ffb8e880137d0", size = 765714, upload-time = "2026-04-03T20:54:32.789Z" }, + { url = "https://files.pythonhosted.org/packages/25/1e/5672e16f34dbbcb2560cc7e6a2fbb26dfa8b270711e730101da4423d3973/regex-2026.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ae5266a82596114e41fb5302140e9630204c1b5f325c770bec654b95dd54b0aa", size = 852078, upload-time = "2026-04-03T20:54:34.546Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0d/c813f0af7c6cc7ed7b9558bac2e5120b60ad0fa48f813e4d4bd55446f214/regex-2026.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c882cd92ec68585e9c1cf36c447ec846c0d94edd706fe59e0c198e65822fd23b", size = 789181, upload-time = "2026-04-03T20:54:36.642Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6d/a344608d1adbd2a95090ddd906cec09a11be0e6517e878d02a5123e0917f/regex-2026.4.4-cp313-cp313-win32.whl", hash = "sha256:05568c4fbf3cb4fa9e28e3af198c40d3237cf6041608a9022285fe567ec3ad62", size = 266690, upload-time = "2026-04-03T20:54:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/31/07/54049f89b46235ca6f45cd6c88668a7050e77d4a15555e47dd40fde75263/regex-2026.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:3384df51ed52db0bea967e21458ab0a414f67cdddfd94401688274e55147bb81", size = 277733, upload-time = "2026-04-03T20:54:40.11Z" }, + { url = "https://files.pythonhosted.org/packages/0e/21/61366a8e20f4d43fb597708cac7f0e2baadb491ecc9549b4980b2be27d16/regex-2026.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:acd38177bd2c8e69a411d6521760806042e244d0ef94e2dd03ecdaa8a3c99427", size = 270565, upload-time = "2026-04-03T20:54:41.883Z" }, + { url = "https://files.pythonhosted.org/packages/f1/1e/3a2b9672433bef02f5d39aa1143ca2c08f311c1d041c464a42be9ae648dc/regex-2026.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f94a11a9d05afcfcfa640e096319720a19cc0c9f7768e1a61fceee6a3afc6c7c", size = 494126, upload-time = "2026-04-03T20:54:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/4e/4b/c132a4f4fe18ad3340d89fcb56235132b69559136036b845be3c073142ed/regex-2026.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:36bcb9d6d1307ab629edc553775baada2aefa5c50ccc0215fbfd2afcfff43141", size = 293882, upload-time = "2026-04-03T20:54:45.41Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5f/eaa38092ce7a023656280f2341dbbd4ad5f05d780a70abba7bb4f4bea54c/regex-2026.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261c015b3e2ed0919157046d768774ecde57f03d8fa4ba78d29793447f70e717", size = 292334, upload-time = "2026-04-03T20:54:47.051Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f6/dd38146af1392dac33db7074ab331cec23cced3759167735c42c5460a243/regex-2026.4.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c228cf65b4a54583763645dcd73819b3b381ca8b4bb1b349dee1c135f4112c07", size = 811691, upload-time = "2026-04-03T20:54:49.074Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f0/dc54c2e69f5eeec50601054998ec3690d5344277e782bd717e49867c1d29/regex-2026.4.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd2630faeb6876fb0c287f664d93ddce4d50cd46c6e88e60378c05c9047e08ca", size = 871227, upload-time = "2026-04-03T20:54:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/a1/af/cb16bd5dc61621e27df919a4449bbb7e5a1034c34d307e0a706e9cc0f3e3/regex-2026.4.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a50ab11b7779b849472337191f3a043e27e17f71555f98d0092fa6d73364520", size = 917435, upload-time = "2026-04-03T20:54:52.994Z" }, + { url = "https://files.pythonhosted.org/packages/5c/71/8b260897f22996b666edd9402861668f45a2ca259f665ac029e6104a2d7d/regex-2026.4.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0734f63afe785138549fbe822a8cfeaccd1bae814c5057cc0ed5b9f2de4fc883", size = 816358, upload-time = "2026-04-03T20:54:54.884Z" }, + { url = "https://files.pythonhosted.org/packages/1c/60/775f7f72a510ef238254906c2f3d737fc80b16ca85f07d20e318d2eea894/regex-2026.4.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4ee50606cb1967db7e523224e05f32089101945f859928e65657a2cbb3d278b", size = 785549, upload-time = "2026-04-03T20:54:57.01Z" }, + { url = "https://files.pythonhosted.org/packages/58/42/34d289b3627c03cf381e44da534a0021664188fa49ba41513da0b4ec6776/regex-2026.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6c1818f37be3ca02dcb76d63f2c7aaba4b0dc171b579796c6fbe00148dfec6b1", size = 801364, upload-time = "2026-04-03T20:54:58.981Z" }, + { url = "https://files.pythonhosted.org/packages/fc/20/f6ecf319b382a8f1ab529e898b222c3f30600fcede7834733c26279e7465/regex-2026.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f5bfc2741d150d0be3e4a0401a5c22b06e60acb9aa4daa46d9e79a6dcd0f135b", size = 866221, upload-time = "2026-04-03T20:55:00.88Z" }, + { url = "https://files.pythonhosted.org/packages/92/6a/9f16d3609d549bd96d7a0b2aee1625d7512ba6a03efc01652149ef88e74d/regex-2026.4.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:504ffa8a03609a087cad81277a629b6ce884b51a24bd388a7980ad61748618ff", size = 772530, upload-time = "2026-04-03T20:55:03.213Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f6/aa9768bc96a4c361ac96419fbaf2dcdc33970bb813df3ba9b09d5d7b6d96/regex-2026.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70aadc6ff12e4b444586e57fc30771f86253f9f0045b29016b9605b4be5f7dfb", size = 856989, upload-time = "2026-04-03T20:55:05.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b4/c671db3556be2473ae3e4bb7a297c518d281452871501221251ea4ecba57/regex-2026.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f4f83781191007b6ef43b03debc35435f10cad9b96e16d147efe84a1d48bdde4", size = 803241, upload-time = "2026-04-03T20:55:07.162Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5c/83e3b1d89fa4f6e5a1bc97b4abd4a9a97b3c1ac7854164f694f5f0ba98a0/regex-2026.4.4-cp313-cp313t-win32.whl", hash = "sha256:e014a797de43d1847df957c0a2a8e861d1c17547ee08467d1db2c370b7568baa", size = 269921, upload-time = "2026-04-03T20:55:09.62Z" }, + { url = "https://files.pythonhosted.org/packages/28/07/077c387121f42cdb4d92b1301133c0d93b5709d096d1669ab847dda9fe2e/regex-2026.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:b15b88b0d52b179712632832c1d6e58e5774f93717849a41096880442da41ab0", size = 281240, upload-time = "2026-04-03T20:55:11.521Z" }, + { url = "https://files.pythonhosted.org/packages/9d/22/ead4a4abc7c59a4d882662aa292ca02c8b617f30b6e163bc1728879e9353/regex-2026.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:586b89cdadf7d67bf86ae3342a4dcd2b8d70a832d90c18a0ae955105caf34dbe", size = 272440, upload-time = "2026-04-03T20:55:13.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f5/ed97c2dc47b5fbd4b73c0d7d75f9ebc8eca139f2bbef476bba35f28c0a77/regex-2026.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2da82d643fa698e5e5210e54af90181603d5853cf469f5eedf9bfc8f59b4b8c7", size = 490343, upload-time = "2026-04-03T20:55:15.241Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/de4828a7385ec166d673a5790ad06ac48cdaa98bc0960108dd4b9cc1aef7/regex-2026.4.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:54a1189ad9d9357760557c91103d5e421f0a2dabe68a5cdf9103d0dcf4e00752", size = 291909, upload-time = "2026-04-03T20:55:17.558Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d6/5cfbfc97f3201a4d24b596a77957e092030dcc4205894bc035cedcfce62f/regex-2026.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:76d67d5afb1fe402d10a6403bae668d000441e2ab115191a804287d53b772951", size = 289692, upload-time = "2026-04-03T20:55:20.561Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/f2212d9fd56fe897e36d0110ba30ba2d247bd6410c5bd98499c7e5a1e1f2/regex-2026.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7cd3e4ee8d80447a83bbc9ab0c8459781fa77087f856c3e740d7763be0df27f", size = 796979, upload-time = "2026-04-03T20:55:22.56Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e3/a016c12675fbac988a60c7e1c16e67823ff0bc016beb27bd7a001dbdabc6/regex-2026.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e19e18c568d2866d8b6a6dfad823db86193503f90823a8f66689315ba28fbe8", size = 866744, upload-time = "2026-04-03T20:55:24.646Z" }, + { url = "https://files.pythonhosted.org/packages/af/a4/0b90ca4cf17adc3cb43de80ec71018c37c88ad64987e8d0d481a95ca60b5/regex-2026.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7698a6f38730fd1385d390d1ed07bb13dce39aa616aca6a6d89bea178464b9a4", size = 911613, upload-time = "2026-04-03T20:55:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3b/2b3dac0b82d41ab43aa87c6ecde63d71189d03fe8854b8ca455a315edac3/regex-2026.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:173a66f3651cdb761018078e2d9487f4cf971232c990035ec0eb1cdc6bf929a9", size = 800551, upload-time = "2026-04-03T20:55:29.532Z" }, + { url = "https://files.pythonhosted.org/packages/25/fe/5365eb7aa0e753c4b5957815c321519ecab033c279c60e1b1ae2367fa810/regex-2026.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa7922bbb2cc84fa062d37723f199d4c0cd200245ce269c05db82d904db66b83", size = 776911, upload-time = "2026-04-03T20:55:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b3/7fb0072156bba065e3b778a7bc7b0a6328212be5dd6a86fd207e0c4f2dab/regex-2026.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:59f67cd0a0acaf0e564c20bbd7f767286f23e91e2572c5703bf3e56ea7557edb", size = 785751, upload-time = "2026-04-03T20:55:33.797Z" }, + { url = "https://files.pythonhosted.org/packages/02/1a/9f83677eb699273e56e858f7bd95acdbee376d42f59e8bfca2fd80d79df3/regex-2026.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:475e50f3f73f73614f7cba5524d6de49dee269df00272a1b85e3d19f6d498465", size = 860484, upload-time = "2026-04-03T20:55:35.745Z" }, + { url = "https://files.pythonhosted.org/packages/3b/7a/93937507b61cfcff8b4c5857f1b452852b09f741daa9acae15c971d8554e/regex-2026.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a1c0c7d67b64d85ac2e1879923bad2f08a08f3004055f2f406ef73c850114bd4", size = 765939, upload-time = "2026-04-03T20:55:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/86/ea/81a7f968a351c6552b1670ead861e2a385be730ee28402233020c67f9e0f/regex-2026.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:1371c2ccbb744d66ee63631cc9ca12aa233d5749972626b68fe1a649dd98e566", size = 851417, upload-time = "2026-04-03T20:55:39.92Z" }, + { url = "https://files.pythonhosted.org/packages/4c/7e/323c18ce4b5b8f44517a36342961a0306e931e499febbd876bb149d900f0/regex-2026.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59968142787042db793348a3f5b918cf24ced1f23247328530e063f89c128a95", size = 789056, upload-time = "2026-04-03T20:55:42.303Z" }, + { url = "https://files.pythonhosted.org/packages/c0/af/e7510f9b11b1913b0cd44eddb784b2d650b2af6515bfce4cffcc5bfd1d38/regex-2026.4.4-cp314-cp314-win32.whl", hash = "sha256:59efe72d37fd5a91e373e5146f187f921f365f4abc1249a5ab446a60f30dd5f8", size = 272130, upload-time = "2026-04-03T20:55:44.995Z" }, + { url = "https://files.pythonhosted.org/packages/9a/51/57dae534c915e2d3a21490e88836fa2ae79dde3b66255ecc0c0a155d2c10/regex-2026.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:e0aab3ff447845049d676827d2ff714aab4f73f340e155b7de7458cf53baa5a4", size = 280992, upload-time = "2026-04-03T20:55:47.316Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5e/abaf9f4c3792e34edb1434f06717fae2b07888d85cb5cec29f9204931bf8/regex-2026.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:a7a5bb6aa0cf62208bb4fa079b0c756734f8ad0e333b425732e8609bd51ee22f", size = 273563, upload-time = "2026-04-03T20:55:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/ff/06/35da85f9f217b9538b99cbb170738993bcc3b23784322decb77619f11502/regex-2026.4.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:97850d0638391bdc7d35dc1c1039974dcb921eaafa8cc935ae4d7f272b1d60b3", size = 494191, upload-time = "2026-04-03T20:55:51.258Z" }, + { url = "https://files.pythonhosted.org/packages/54/5b/1bc35f479eef8285c4baf88d8c002023efdeebb7b44a8735b36195486ae7/regex-2026.4.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ee7337f88f2a580679f7bbfe69dc86c043954f9f9c541012f49abc554a962f2e", size = 293877, upload-time = "2026-04-03T20:55:53.214Z" }, + { url = "https://files.pythonhosted.org/packages/39/5b/f53b9ad17480b3ddd14c90da04bfb55ac6894b129e5dea87bcaf7d00e336/regex-2026.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7429f4e6192c11d659900c0648ba8776243bf396ab95558b8c51a345afeddde6", size = 292410, upload-time = "2026-04-03T20:55:55.736Z" }, + { url = "https://files.pythonhosted.org/packages/bb/56/52377f59f60a7c51aa4161eecf0b6032c20b461805aca051250da435ffc9/regex-2026.4.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4f10fbd5dd13dcf4265b4cc07d69ca70280742870c97ae10093e3d66000359", size = 811831, upload-time = "2026-04-03T20:55:57.802Z" }, + { url = "https://files.pythonhosted.org/packages/dd/63/8026310bf066f702a9c361f83a8c9658f3fe4edb349f9c1e5d5273b7c40c/regex-2026.4.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a152560af4f9742b96f3827090f866eeec5becd4765c8e0d3473d9d280e76a5a", size = 871199, upload-time = "2026-04-03T20:56:00.333Z" }, + { url = "https://files.pythonhosted.org/packages/20/9f/a514bbb00a466dbb506d43f187a04047f7be1505f10a9a15615ead5080ee/regex-2026.4.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54170b3e95339f415d54651f97df3bff7434a663912f9358237941bbf9143f55", size = 917649, upload-time = "2026-04-03T20:56:02.445Z" }, + { url = "https://files.pythonhosted.org/packages/cb/6b/8399f68dd41a2030218839b9b18360d79b86d22b9fab5ef477c7f23ca67c/regex-2026.4.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:07f190d65f5a72dcb9cf7106bfc3d21e7a49dd2879eda2207b683f32165e4d99", size = 816388, upload-time = "2026-04-03T20:56:04.595Z" }, + { url = "https://files.pythonhosted.org/packages/1e/9c/103963f47c24339a483b05edd568594c2be486188f688c0170fd504b2948/regex-2026.4.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9a2741ce5a29d3c84b0b94261ba630ab459a1b847a0d6beca7d62d188175c790", size = 785746, upload-time = "2026-04-03T20:56:07.13Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ee/7f6054c0dec0cee3463c304405e4ff42e27cff05bf36fcb34be549ab17bd/regex-2026.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b26c30df3a28fd9793113dac7385a4deb7294a06c0f760dd2b008bd49a9139bc", size = 801483, upload-time = "2026-04-03T20:56:09.365Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/51d3d941cf6070dc00c3338ecf138615fc3cce0421c3df6abe97a08af61a/regex-2026.4.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:421439d1bee44b19f4583ccf42670ca464ffb90e9fdc38d37f39d1ddd1e44f1f", size = 866331, upload-time = "2026-04-03T20:56:12.039Z" }, + { url = "https://files.pythonhosted.org/packages/16/e8/76d50dcc122ac33927d939f350eebcfe3dbcbda96913e03433fc36de5e63/regex-2026.4.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b40379b53ecbc747fd9bdf4a0ea14eb8188ca1bd0f54f78893a39024b28f4863", size = 772673, upload-time = "2026-04-03T20:56:14.558Z" }, + { url = "https://files.pythonhosted.org/packages/a5/6e/5f6bf75e20ea6873d05ba4ec78378c375cbe08cdec571c83fbb01606e563/regex-2026.4.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:08c55c13d2eef54f73eeadc33146fb0baaa49e7335eb1aff6ae1324bf0ddbe4a", size = 857146, upload-time = "2026-04-03T20:56:16.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/33/3c76d9962949e487ebba353a18e89399f292287204ac8f2f4cfc3a51c233/regex-2026.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9776b85f510062f5a75ef112afe5f494ef1635607bf1cc220c1391e9ac2f5e81", size = 803463, upload-time = "2026-04-03T20:56:18.923Z" }, + { url = "https://files.pythonhosted.org/packages/19/eb/ef32dcd2cb69b69bc0c3e55205bce94a7def48d495358946bc42186dcccc/regex-2026.4.4-cp314-cp314t-win32.whl", hash = "sha256:385edaebde5db5be103577afc8699fea73a0e36a734ba24870be7ffa61119d74", size = 275709, upload-time = "2026-04-03T20:56:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/a0/86/c291bf740945acbf35ed7dbebf8e2eea2f3f78041f6bd7cdab80cb274dc0/regex-2026.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:5d354b18839328927832e2fa5f7c95b7a3ccc39e7a681529e1685898e6436d45", size = 285622, upload-time = "2026-04-03T20:56:23.641Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e7/ec846d560ae6a597115153c02ca6138a7877a1748b2072d9521c10a93e58/regex-2026.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:af0384cb01a33600c49505c27c6c57ab0b27bf84a74e28524c92ca897ebdac9d", size = 275773, upload-time = "2026-04-03T20:56:26.07Z" }, +] + +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + +[[package]] +name = "starlette" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, + { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, + { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" }, +]