commit 0e41e7accec7bae5f9029b1efe8cb1da2521949e Author: danial Date: Sat Nov 1 14:32:29 2025 +0800 feat(core): 初始化核心配置和部署文件 - 添加 .env.example 环境变量配置示例 - 添加 .gitignore 忽略文件配置 - 添加 core/config.py 配置管理模块 - 添加 deployments/k8s/configmap.yaml Kubernetes 配置 - 添加 core/database.py 数据库连接管理模块 - 添加 core/dependencies.py 全局依赖模块 - 添加 DEPENDENCIES_UPDATED.md 依赖更新记录 - 添加 deployments/k8s/deployment.yaml Kubernetes 部署配置- 添加 deployments/swarm/docker-compose.swarm.yml Docker Swarm 部署配置 - 添加 deployments/docker/docker-compose.yml Docker 部署配置 - 添加 deployments/docker/Dockerfile 应用镜像构建文件 - 添加 middleware/error_handler.py 全局异常处理中间件 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7d2144a --- /dev/null +++ b/.env.example @@ -0,0 +1,45 @@ +# Application Configuration +APP_NAME=kami_spider +ENVIRONMENT=development +DEBUG=true +HOST=0.0.0.0 +PORT=8000 +WORKERS=4 +LOG_LEVEL=INFO + +# Database Configuration +DB_HOST=localhost +DB_PORT=3306 +DB_NAME=kami_spider +DB_USER=kami_user +DB_PASSWORD=kami_pass +DB_POOL_SIZE=10 +DB_MAX_OVERFLOW=20 +DB_POOL_RECYCLE=3600 +DB_POOL_PRE_PING=true +DB_ECHO=false + +# Redis Configuration +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_DB=0 +REDIS_PASSWORD= +REDIS_MAX_CONNECTIONS=50 +REDIS_DECODE_RESPONSES=true + +# OpenTelemetry Configuration +OTEL_ENABLED=true +OTEL_SERVICE_NAME=kami_spider +OTEL_EXPORTER_ENDPOINT=38.38.251.113:31547 +OTEL_EXPORTER_INSECURE=true +OTEL_SAMPLE_RATE=1.0 + +# CORS Configuration +CORS_ENABLED=true +CORS_ALLOW_ORIGINS=["*"] +CORS_ALLOW_CREDENTIALS=true +CORS_ALLOW_METHODS=["*"] +CORS_ALLOW_HEADERS=["*"] + +# Security +SECRET_KEY=change-me-in-production-use-strong-random-key diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7d01383 --- /dev/null +++ b/.gitignore @@ -0,0 +1,74 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Virtual environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# UV +.uv/ + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Testing +.pytest_cache/ +.coverage +htmlcov/ +.tox/ +.hypothesis/ + +# Logs +*.log +logs/ + +# Database +*.db +*.sqlite +*.sqlite3 + +# Alembic +alembic/versions/*.pyc + +# Environment +.env.local +.env.production +.env.staging + +# Docker +.dockerignore + +# Temporary files +tmp/ +temp/ +*.tmp diff --git a/.qoder/quests/project-architecture-setup.md b/.qoder/quests/project-architecture-setup.md new file mode 100644 index 0000000..01a1875 --- /dev/null +++ b/.qoder/quests/project-architecture-setup.md @@ -0,0 +1,1249 @@ +# Project Architecture Setup Design + +## 1. Overview + +### 1.1 Project Vision + +Build a stateless, production-ready FastAPI-based web service platform that hosts multiple independent web applications through a unified entry point. The architecture emphasizes observability, scalability, and modern Python ecosystem practices. + +### 1.2 Core Objectives + +- **Stateless Service**: Application state externalized to Redis and MySQL +- **Multi-Application Support**: Single web service hosting independent applications (App A, App B, etc.) +- **Modern Python Stack**: Python 3.13, UV package manager, FastAPI, SQLModel, Pydantic 2.x +- **Full Observability**: OpenTelemetry integration with trace context propagation +- **Production Deployment**: Docker, Docker Compose, Docker Swarm, and Kubernetes support +- **API Quality**: Pydantic-validated I/O, auto-generated documentation, RESTful response format + +### 1.3 Technology Stack + +| Component | Technology | Version | +|-----------|-----------|---------| +| Language | Python | 3.13 | +| Package Manager | UV | Latest | +| Web Framework | FastAPI | Latest | +| ORM | SQLModel | Latest | +| Validation | Pydantic | 2.x Latest | +| ASGI Server | Uvicorn | Latest | +| Process Manager | Gunicorn | Latest | +| Database | MySQL | 8.x (future: PostgreSQL) | +| Cache/Session | Redis | Latest | +| Observability | OpenTelemetry | Latest | +| Telemetry Protocol | gRPC | - | + +## 2. Architecture Design + +### 2.1 System Architecture + +```mermaid +graph TB + subgraph "Load Balancer / Ingress" + LB[Ingress Controller] + end + + subgraph "Application Layer - Stateless" + LB --> APP1[FastAPI Instance 1] + LB --> APP2[FastAPI Instance 2] + LB --> APP3[FastAPI Instance N] + end + + subgraph "Application Routing" + APP1 --> ROUTER[Unified Router] + ROUTER --> APPA[Web App A] + ROUTER --> APPB[Web App B] + ROUTER --> APPC[Web App N] + end + + subgraph "Shared Stateful Services - Single Instance" + REDIS[(Single Redis Instance)] + MYSQL[(Single MySQL Instance)] + end + + subgraph "Data Layer Access" + APPA --> REDIS + APPB --> REDIS + APPC --> REDIS + APPA --> MYSQL + APPB --> MYSQL + APPC --> MYSQL + end + + subgraph "Observability" + APP1 -.Traces/Logs.-> OTEL[OpenTelemetry Collector] + APP2 -.Traces/Logs.-> OTEL + APP3 -.Traces/Logs.-> OTEL + OTEL -.gRPC.-> BACKEND[38.38.251.113:31547] + end + + style REDIS fill:#ff9999 + style MYSQL fill:#ff9999 +``` + +### 2.2 Layered Architecture + +```mermaid +graph LR + subgraph "Presentation Layer" + A[FastAPI Routes] + B[Request Validation] + C[Response Formatting] + end + + subgraph "Business Layer" + D[Service Classes] + E[Business Logic] + end + + subgraph "Data Layer" + F[SQLModel Models] + G[Repository Pattern] + H[Cache Layer] + end + + subgraph "Cross-Cutting" + I[Middleware: TraceID Injection] + J[Exception Handlers] + K[Logging with Trace Context] + end + + A --> D + B --> A + A --> C + D --> F + F --> G + G --> H + I -.-> A + I -.-> D + K -.-> A + K -.-> D +``` + +### 2.3 Multi-Application Architecture + +```mermaid +graph TD + subgraph "Main Application" + MAIN[main.py - FastAPI App] + MAIN --> MIDDLEWARE[Middleware Stack] + MIDDLEWARE --> CORE_ROUTER[Core Router] + end + + subgraph "Application Modules" + CORE_ROUTER --> APP_A_ROUTER["/app-a/*"] + CORE_ROUTER --> APP_B_ROUTER["/app-b/*"] + CORE_ROUTER --> APP_N_ROUTER["/app-n/*"] + + APP_A_ROUTER --> APP_A_SERVICE[App A Services] + APP_B_ROUTER --> APP_B_SERVICE[App B Services] + APP_N_ROUTER --> APP_N_SERVICE[App N Services] + end + + subgraph "Shared Infrastructure - Single Instance" + APP_A_SERVICE --> SHARED_DB[Shared Database Pool] + APP_B_SERVICE --> SHARED_DB + APP_N_SERVICE --> SHARED_DB + + APP_A_SERVICE --> SHARED_REDIS[Shared Redis Pool] + APP_B_SERVICE --> SHARED_REDIS + APP_N_SERVICE --> SHARED_REDIS + + SHARED_DB --> MYSQL[(Single MySQL Instance)] + SHARED_REDIS --> REDIS[(Single Redis Instance)] + end +``` + +## 3. Project Structure + +### 3.1 Directory Organization + +``` +kami_spider/ +├── apps/ # Independent web applications +│ ├── __init__.py +│ ├── app_a/ # Application A module +│ │ ├── __init__.py +│ │ ├── router.py # FastAPI routes for App A +│ │ ├── services.py # Business logic +│ │ ├── models.py # SQLModel definitions +│ │ ├── schemas.py # Pydantic request/response schemas +│ │ └── dependencies.py # App-specific dependencies +│ ├── app_b/ # Application B module +│ │ └── ... # Same structure as App A +│ └── shared/ # Shared utilities across apps +│ ├── base_models.py # Base SQLModel classes +│ └── common_schemas.py # Common Pydantic schemas +├── core/ # Core infrastructure +│ ├── __init__.py +│ ├── config.py # Pydantic Settings configuration +│ ├── database.py # SQLModel database engine +│ ├── redis.py # Redis client setup +│ ├── responses.py # RESTful response generics +│ ├── exceptions.py # Custom exception classes +│ └── dependencies.py # Global dependencies +├── middleware/ # Middleware components +│ ├── __init__.py +│ ├── trace_context.py # TraceID injection +│ ├── error_handler.py # Global error handling +│ └── correlation.py # Request correlation +├── observability/ # OpenTelemetry integration +│ ├── __init__.py +│ ├── tracing.py # Tracer configuration +│ ├── logging.py # Structured logging with trace +│ └── metrics.py # Metrics instrumentation +├── deployments/ # Deployment configurations +│ ├── docker/ +│ │ ├── Dockerfile +│ │ └── docker-compose.yml +│ ├── swarm/ +│ │ └── docker-compose.swarm.yml +│ └── k8s/ +│ ├── deployment.yaml +│ ├── service.yaml +│ ├── configmap.yaml +│ └── ingress.yaml +├── tests/ # Test suites +│ ├── unit/ +│ ├── integration/ +│ └── conftest.py +├── main.py # Application entry point +├── pyproject.toml # UV project configuration +└── README.md +``` + +### 3.2 Module Responsibilities + +| Module | Responsibility | +|--------|----------------| +| `apps/` | Contains all independent web applications with isolated business logic | +| `core/` | Shared infrastructure: config, database, cache, response formatting | +| `middleware/` | Request/response interceptors for cross-cutting concerns | +| `observability/` | OpenTelemetry setup, logging, tracing, metrics | +| `deployments/` | Container and orchestration manifests | +| `main.py` | FastAPI application bootstrap and router registration | + +## 4. Core Components Design + +### 4.1 Configuration Management + +#### 4.1.1 Settings Structure + +Use Pydantic Settings to manage all configuration with environment variable support and validation. + +**Configuration Categories:** + +| Category | Purpose | Example Fields | +|----------|---------|----------------| +| Application | Runtime settings | `APP_NAME`, `DEBUG`, `ENVIRONMENT` | +| Database | MySQL connection | `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` | +| Redis | Cache configuration | `REDIS_HOST`, `REDIS_PORT`, `REDIS_DB`, `REDIS_PASSWORD` | +| OpenTelemetry | Observability | `OTEL_EXPORTER_ENDPOINT`, `OTEL_SERVICE_NAME` | +| Server | Gunicorn/Uvicorn | `WORKERS`, `HOST`, `PORT`, `LOG_LEVEL` | + +#### 4.1.2 Settings Validation + +- All settings validated at startup using Pydantic validators +- Support for `.env` files in development +- Environment-specific overrides (dev, staging, production) +- Sensitive values (passwords, tokens) loaded from environment only + +### 4.2 Database Layer + +#### 4.2.1 SQLModel Integration + +**Connection Management:** + +- Async SQLAlchemy engine with connection pooling +- Session factory using dependency injection +- Automatic session cleanup via context managers + +**Migration Strategy:** + +- Alembic for schema migrations +- Version-controlled migration files +- Support for MySQL → PostgreSQL future migration + +#### 4.2.2 Repository Pattern + +**Base Repository:** + +- Generic CRUD operations +- Pagination support +- Filter and query building +- Transaction management +- Shared across all applications + +**Application-Specific Repositories:** + +- Each app defines repositories extending base +- Encapsulates data access logic +- Returns domain models, not ORM entities +- Accesses same database instance as other applications +- May use discriminator columns to logically filter data by application + +**Cross-Application Data Access:** + +- Applications can access each other's tables if needed +- Shared models can be defined in `apps/shared/base_models.py` +- Business logic enforces data boundaries, not database constraints + +### 4.3 Redis Integration + +#### 4.3.1 Connection Strategy + +- Single Redis async client using `redis-py` +- Shared connection pool across all applications +- Health check integration +- All applications connect to the same Redis instance + +#### 4.3.2 Usage Patterns + +| Pattern | Use Case | Key Strategy | +|---------|----------|-------------| +| Session Store | User session data with TTL | Optional app prefix: `session:{app}:{id}` | +| Cache | Query result caching with invalidation | Optional app prefix: `cache:{app}:{key}` | +| Rate Limiting | API rate limit tracking | Shared or per-app: `ratelimit:{endpoint}:{user}` | +| Distributed Lock | Coordination across instances | Shared locks: `lock:{resource}` | +| Queue | Background task queueing (optional) | Shared queue: `queue:{task_type}` | + +**Key Namespace Convention (Optional):** + +- Use prefixes for organizational clarity: `{app_name}:{resource_type}:{identifier}` +- Not enforced at infrastructure level +- Applications can read/write any keys +- Helps with debugging and monitoring + +### 4.4 RESTful Response Format + +#### 4.4.1 Unified Response Structure + +All API responses follow a consistent structure using generic types. Both success and error responses share the same top-level structure to ensure consistency. + +**Unified Response Schema:** + +| Field | Type | Description | +|-------|------|-------------| +| `code` | integer | Business status code (0 = success, >0 = various business errors) | +| `message` | string | Human-readable message describing the result | +| `data` | Generic[T] \| null | Response payload for success, `null` for errors | +| `trace_id` | string | Request trace ID for debugging and log correlation | +| `timestamp` | datetime | Response timestamp (ISO 8601 format) | + +**Response Examples:** + +*Success Response (code = 0):* + +``` +{ + "code": 0, + "message": "Success", + "data": { + "user_id": 12345, + "username": "john_doe" + }, + "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "timestamp": "2024-01-15T10:30:00Z" +} +``` + +*Error Response (code > 0):* + +``` +{ + "code": 1001, + "message": "Login failed: Invalid credentials", + "data": null, + "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "timestamp": "2024-01-15T10:30:00Z" +} +``` + +#### 4.4.2 Business Code Convention + +**Code Range Assignment:** + +| Code Range | Category | Description | +|------------|----------|-------------| +| 0 | Success | Operation completed successfully | +| 1000-1999 | Authentication & Authorization | Login failures, permission errors | +| 2000-2999 | Business Logic Errors | Order failures, insufficient balance, etc. | +| 3000-3999 | Validation Errors | Invalid input, missing required fields | +| 4000-4999 | Resource Errors | Not found, already exists, conflicts | +| 5000-5999 | System Errors | Database errors, external service failures | +| 9000-9999 | Unknown Errors | Unexpected errors | + +**Example Business Codes:** + +| Code | Message | Use Case | +|------|---------|----------| +| 0 | Success | Any successful operation | +| 1001 | Login failed | Invalid username or password | +| 1002 | Token expired | JWT token has expired | +| 1003 | Insufficient permissions | User lacks required role | +| 2001 | Order creation failed | Inventory insufficient | +| 2002 | Payment failed | Payment gateway error | +| 2003 | Insufficient balance | User account balance too low | +| 3001 | Invalid input | Field validation failure | +| 3002 | Missing required field | Required parameter not provided | +| 4001 | Resource not found | Requested entity doesn't exist | +| 4002 | Resource already exists | Duplicate creation attempt | +| 4003 | Resource conflict | State conflict or version mismatch | +| 5001 | Database error | Database connection or query failure | +| 5002 | External service error | Third-party API unavailable | +| 9000 | Unknown error | Unhandled exception | + +**Application-Specific Code Ranges (Optional):** + +Each application can define sub-ranges within the main categories: + +- App A: 10000-19999 +- App B: 20000-29999 +- App N: 30000-39999 + +#### 4.4.3 HTTP Status Code Strategy + +**HTTP Status Codes vs Business Codes:** + +- **HTTP Status Codes**: Indicate transport/protocol-level status +- **Business Codes**: Indicate application-level result within the response body + +**HTTP Status Code Usage:** + +| HTTP Status | Usage | Business Code Range | +|-------------|-------|--------------------| +| 200 OK | Request processed successfully (business success or failure) | 0, 1000-9999 | +| 201 Created | Resource successfully created | 0 | +| 204 No Content | Successful DELETE with no response body | N/A (no body) | +| 400 Bad Request | Malformed request, cannot be processed | 3000-3999 | +| 401 Unauthorized | Missing or invalid authentication | 1001-1099 | +| 403 Forbidden | Authenticated but insufficient permissions | 1003 | +| 404 Not Found | Endpoint does not exist | N/A | +| 422 Unprocessable Entity | Pydantic validation failures | 3001-3099 | +| 500 Internal Server Error | Unhandled server errors | 9000-9999 | +| 503 Service Unavailable | Database/Redis/external service down | 5000-5999 | + +**Response Strategy:** + +- **Business-level errors** (login failed, order failed, insufficient balance): Return HTTP 200 with appropriate business code +- **Protocol-level errors** (invalid JSON, missing auth header): Return appropriate HTTP 4xx/5xx status +- Most application responses use HTTP 200 with business code indicating actual result +- Client checks `code` field to determine success (code = 0) or specific failure reason (code > 0) + +#### 4.4.4 Generic Response Types + +**TypeVar Definitions:** + +- `T` for data payload type + +**Generic Response Class:** + +- `ApiResponse[T]` for all responses (success and error) + - `code`: Business status code (integer) + - `message`: Human-readable message (string) + - `data`: Typed payload or null (Generic[T] | None) + - `trace_id`: Trace identifier (string) + - `timestamp`: Response timestamp (datetime) + +**Specialized Response Types:** + +- `SuccessResponse[T]`: ApiResponse with code=0 and typed data +- `ErrorResponse`: ApiResponse with code>0 and data=null +- `PaginatedResponse[T]`: ApiResponse where data contains: + - `items`: List[T] + - `total`: Total record count + - `page`: Current page number + - `page_size`: Records per page + - `total_pages`: Total page count + +**Response Builder Pattern:** + +Utility functions for creating responses: + +- `success(data: T, message: str = "Success")` → Returns ApiResponse with code=0 +- `error(code: int, message: str)` → Returns ApiResponse with specified error code +- `paginated(items: List[T], total: int, page: int, page_size: int)` → Returns paginated response + +### 4.5 Pydantic Validation + +#### 4.5.1 Input Validation + +- All route parameters validated via Pydantic models +- Query parameters use Pydantic schemas +- Request body validated with strict mode +- Custom validators for business rules + +#### 4.5.2 Output Validation + +- Response models enforce output contracts +- Auto-filtering of sensitive fields +- Serialization customization (alias, exclude) + +#### 4.5.3 API Documentation + +- FastAPI auto-generates OpenAPI spec from Pydantic models +- Swagger UI and ReDoc available at `/docs` and `/redoc` +- Schema examples in Pydantic models populate API docs +- Response model declarations generate accurate documentation + +## 5. OpenTelemetry Integration + +### 5.1 Instrumentation Architecture + +```mermaid +graph LR + subgraph "Request Flow" + A[Incoming Request] --> B[ASGI Middleware] + B --> C[TraceID Extraction/Generation] + C --> D[Span Creation] + D --> E[Business Logic] + E --> F[Database Query] + E --> G[Redis Operation] + F --> H[Response] + G --> H + end + + subgraph "Trace Context" + C --> I[Context Propagation] + I --> J[Logging Context] + I --> K[DB Span] + I --> L[Redis Span] + end + + subgraph "Export" + D --> M[Trace Exporter] + J --> N[Log Exporter] + M --> O[gRPC: 38.38.251.113:31547] + N --> O + end +``` + +### 5.2 TraceID Management + +#### 5.2.1 Trace Context Injection + +**Middleware Responsibilities:** + +- Extract `traceparent` from incoming request headers (W3C Trace Context) +- Generate new trace ID if not present +- Inject trace ID into request context +- Add trace ID to response headers +- Propagate context to all sub-spans + +#### 5.2.2 Context Propagation + +| Component | Propagation Method | +|-----------|-------------------| +| HTTP Requests | W3C Trace Context headers | +| Database Queries | SQLAlchemy instrumentation auto-propagates | +| Redis Operations | Manual span creation in Redis client | +| Logging | ContextVar injection into log records | +| Background Tasks | Explicit context passing | + +### 5.3 Logging with Trace Context + +#### 5.3.1 Structured Logging + +**Log Format:** + +- JSON-structured logs for production +- Human-readable for development +- Automatic trace_id injection into every log record +- Log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL + +**Standard Log Fields:** + +| Field | Description | +|-------|-------------| +| `timestamp` | ISO 8601 timestamp | +| `level` | Log level | +| `logger` | Logger name (module) | +| `message` | Log message | +| `trace_id` | Current trace ID | +| `span_id` | Current span ID | +| `service.name` | Service name from config | +| `extras` | Additional context (user_id, request_id, etc.) | + +#### 5.3.2 Log Correlation + +- Every log statement automatically includes trace_id +- Logs associated with spans exported to OpenTelemetry collector +- Correlation between traces and logs in observability backend + +### 5.4 Telemetry Export + +#### 5.4.1 Exporter Configuration + +**Protocol:** gRPC +**Endpoint:** `38.38.251.113:31547` +**Exported Data:** + +- Traces (spans with timing, attributes, events) +- Logs (structured with trace context) +- Metrics (optional: request rate, latency, error rate) + +#### 5.4.2 Instrumentation Targets + +| Target | Library | Auto/Manual | +|--------|---------|-------------| +| FastAPI | opentelemetry-instrumentation-fastapi | Auto | +| SQLAlchemy | opentelemetry-instrumentation-sqlalchemy | Auto | +| Redis | opentelemetry-instrumentation-redis | Auto | +| HTTP Client | opentelemetry-instrumentation-httpx | Auto | +| Custom Logic | opentelemetry-api (manual spans) | Manual | + +### 5.5 Performance Considerations + +- Sampling strategy for high-traffic endpoints (e.g., 10% sampling) +- Batch span export to reduce network overhead +- Async export to avoid blocking request processing +- Circuit breaker for collector unavailability + +## 6. Multi-Application Routing + +### 6.1 Application Registration + +**Router Mounting Strategy:** + +- Each application provides a FastAPI router +- Main application mounts routers with path prefixes +- Example: App A mounted at `/app-a`, App B at `/app-b` + +**Router Structure:** + +```mermaid +graph TD + MAIN[Main FastAPI App] --> HEALTH[/health] + MAIN --> DOCS[/docs, /redoc] + MAIN --> APP_A[/app-a/*] + MAIN --> APP_B[/app-b/*] + + APP_A --> A_USERS[/app-a/users] + APP_A --> A_ORDERS[/app-a/orders] + + APP_B --> B_PRODUCTS[/app-b/products] + APP_B --> B_INVENTORY[/app-b/inventory] +``` + +### 6.2 Application Isolation + +**Namespace Isolation:** + +| Aspect | Isolation Strategy | +|--------|-------------------| +| Routes | Path prefix separation | +| Database Tables | Shared tables, logical separation via application identifier column | +| Redis Keys | Shared Redis, optional key prefixing for clarity | +| Logging | Logger name includes app identifier | +| Metrics | Service labels distinguish apps | +| Dependencies | Shared dependency injection with optional app-specific overrides | + +**Shared Resource Strategy:** + +- All applications share the same database instance and connection pool +- All applications share the same Redis instance and connection pool +- Data isolation achieved through application logic, not physical separation +- Tables may include `app_id` or similar discriminator columns where needed +- Redis keys can use prefixes for organizational clarity but not enforced isolation + +### 6.3 Shared Infrastructure + +**Shared Components:** + +- Database connection pool +- Redis connection pool +**Shared Components:** +- **Single Database Instance**: All applications connect to the same MySQL database + - Shared connection pool across all applications + - Shared transaction management + - Tables accessible by all applications + - Optional: Use discriminator columns for logical separation + +- **Single Redis Instance**: All applications use the same Redis cache + - Shared connection pool + - Shared key space (namespace prefixing optional) + - Shared session storage + - Shared rate limiting counters + +- **Common Dependencies**: + - OpenTelemetry tracer (single service name or app-tagged spans) + - Middleware stack (applied to all applications) + - Exception handlers (global error handling) + - Response formatting utilities (consistent API responses) + - Configuration management (single settings instance) + - Logging configuration (shared formatters and handlers) + +**Benefits of Shared Infrastructure:** + +- Simplified operations and maintenance +- Reduced resource consumption +- Easier data sharing between applications +- Centralized monitoring and observability +- Single source of truth for configuration + +### 6.4 Application Lifecycle + +**Application Bootstrap Flow:** + +```mermaid +sequenceDiagram + participant Main + participant Config + participant DB + participant Redis + participant OTEL + participant AppA + participant AppB + + Main->>Config: Load Settings + Main->>OTEL: Initialize Tracing + Main->>DB: Create Engine & Pool + Main->>Redis: Create Client & Pool + Main->>AppA: Register Router + Main->>AppB: Register Router + Main->>Main: Apply Middleware + Main->>Main: Register Exception Handlers + Main->>Main: Start Server +``` + +## 7. Middleware Stack + +### 7.1 Middleware Execution Order + +```mermaid +graph TD + REQ[Incoming Request] --> M1[CORS Middleware] + M1 --> M2[TraceID Injection] + M2 --> M3[Request Logging] + M3 --> M4[Error Handler] + M4 --> M5[Authentication Optional] + M5 --> ROUTE[Route Handler] + ROUTE --> RES[Response] + RES --> L1[Response Logging] + L1 --> L2[Trace Header Addition] + L2 --> OUT[Outgoing Response] +``` + +### 7.2 Middleware Components + +| Middleware | Purpose | Configuration | +|------------|---------|---------------| +| CORS | Cross-origin resource sharing | Allow origins from settings | +| TraceID Injection | Extract/generate trace context | W3C Trace Context format | +| Request Logging | Log incoming requests with trace | Log level: INFO | +| Error Handler | Catch and format exceptions | Return standardized error response | +| Authentication | Validate JWT/API keys (if needed) | Optional, app-specific | +| Rate Limiting | Prevent abuse | Redis-backed sliding window | +| Request ID | Generate correlation ID | UUID v4 | + +### 7.3 Exception Handling Strategy + +**Exception Hierarchy:** + +```mermaid +graph TD + BASE[BaseException] --> APP[ApplicationException] + APP --> VALIDATION[ValidationException - 400] + APP --> NOTFOUND[NotFoundException - 404] + APP --> CONFLICT[ConflictException - 409] + APP --> AUTH[AuthenticationException - 401] + APP --> PERM[PermissionException - 403] + APP --> BUSINESS[BusinessLogicException - 400] + + BASE --> SYSTEM[SystemException] + SYSTEM --> DB_ERROR[DatabaseException - 503] + SYSTEM --> CACHE_ERROR[CacheException - 503] + SYSTEM --> EXTERNAL[ExternalServiceException - 502] +``` + +**Exception Handler Behavior:** + +- Catch all exceptions at top level +- Log exception with trace context +- Map exception type to HTTP status code +- Return standardized ErrorResponse +- Include trace_id for debugging +- Sanitize sensitive information in error messages + +## 8. Deployment Strategy + +### 8.1 Container Design + +#### 8.1.1 Dockerfile Strategy + +**Base Image:** `python:3.13-slim` + +**Build Stages:** + +1. **Builder Stage:** Install UV, copy dependencies, install packages +2. **Runtime Stage:** Copy virtual environment, copy application code +3. **Optimization:** Multi-stage build to reduce image size + +**Runtime Configuration:** + +- Use UV in production for dependency resolution +- Gunicorn as process manager +- Uvicorn as ASGI worker +- Non-root user for security +- Health check endpoint + +**Environment Variables:** + +- All configuration via environment variables +- Secrets via Docker secrets or K8s secrets +- Default to production settings + +#### 8.1.2 Gunicorn + Uvicorn Configuration + +| Parameter | Value | Rationale | +|-----------|-------|-----------| +| Worker Class | `uvicorn.workers.UvicornWorker` | ASGI support | +| Workers | `(CPU cores * 2) + 1` | Optimal concurrency | +| Worker Connections | 1000 | Max concurrent connections per worker | +| Timeout | 30s | Prevent hanging requests | +| Graceful Timeout | 30s | Allow ongoing requests to complete | +| Keep-Alive | 5s | Connection reuse | +| Access Log | stdout | Container log collection | +| Error Log | stderr | Container log collection | + +### 8.2 Docker Compose Deployment + +#### 8.2.1 Service Topology + +```mermaid +graph TB + subgraph "Docker Compose Stack" + APP[kami_spider app] + MYSQL[(MySQL)] + REDIS[(Redis)] + + APP --> MYSQL + APP --> REDIS + end + + OTEL_EXT[External OTEL Collector
38.38.251.113:31547] + APP -.gRPC.-> OTEL_EXT +``` + +#### 8.2.2 Service Definitions + +| Service | Image | Ports | Volumes | Dependencies | +|---------|-------|-------|---------|--------------| +| app | kami_spider:latest | 8000:8000 | None (stateless) | mysql, redis | +| mysql | mysql:8.x | 3306:3306 | mysql_data | None | +| redis | redis:latest | 6379:6379 | redis_data | None | + +**Health Checks:** + +- App: `GET /health` every 30s +- MySQL: `mysqladmin ping` every 10s +- Redis: `redis-cli ping` every 10s + +**Restart Policy:** `unless-stopped` + +### 8.3 Docker Swarm Deployment + +#### 8.3.1 Swarm Enhancements + +**Differences from Compose:** + +- Replicas for app service (e.g., 3 replicas) +- Placement constraints (e.g., avoid same host) +- Update strategy: rolling updates +- Resource limits and reservations + +**Stack Configuration:** + +| Service | Replicas | Update Strategy | Resources | +|---------|----------|-----------------|-----------| +| app | 3 | Parallelism: 1, Delay: 10s | CPU: 0.5-2, Memory: 512M-2G | +| mysql | 1 | N/A (stateful) | CPU: 1-4, Memory: 2G-8G | +| redis | 1 | N/A (stateful) | CPU: 0.5-2, Memory: 512M-4G | + +**Network:** + +- Overlay network for service discovery +- Ingress routing mesh for load balancing + +### 8.4 Kubernetes Deployment + +#### 8.4.1 Resource Topology + +```mermaid +graph TB + subgraph "Kubernetes Cluster" + ING[Ingress] --> SVC[Service: kami-spider] + SVC --> POD1[Pod 1] + SVC --> POD2[Pod 2] + SVC --> POD3[Pod 3] + + POD1 --> MYSQL_SVC[MySQL Service] + POD2 --> MYSQL_SVC + POD3 --> MYSQL_SVC + + POD1 --> REDIS_SVC[Redis Service] + POD2 --> REDIS_SVC + POD3 --> REDIS_SVC + + MYSQL_SVC --> MYSQL_POD[MySQL StatefulSet] + REDIS_SVC --> REDIS_POD[Redis StatefulSet] + end +``` + +#### 8.4.2 Kubernetes Resources + +**Deployment (kami-spider-app):** + +| Aspect | Configuration | +|--------|--------------| +| Replicas | 3 (HPA: 3-10) | +| Update Strategy | RollingUpdate, maxSurge: 1, maxUnavailable: 0 | +| Resources | Requests: 500m CPU, 512Mi RAM; Limits: 2 CPU, 2Gi RAM | +| Health Probes | Liveness: /health, Readiness: /health | +| Environment | ConfigMap + Secret references | + +**StatefulSet (MySQL):** + +| Aspect | Configuration | +|--------|--------------| +| Replicas | 1 (or 3 for HA) | +| Storage | PersistentVolumeClaim, 20Gi | +| Service | ClusterIP, headless for StatefulSet | + +**StatefulSet (Redis):** + +| Aspect | Configuration | +|--------|--------------| +| Replicas | 1 (or 3 for cluster mode) | +| Storage | PersistentVolumeClaim, 5Gi | +| Service | ClusterIP | + +**ConfigMap:** + +- Non-sensitive configuration (DB host, Redis host, OTEL endpoint) + +**Secret:** + +- Sensitive data (DB password, Redis password) + +**Service:** + +- ClusterIP for app (internal) +- LoadBalancer or NodePort (if needed) +- Ingress for HTTP routing + +**Ingress:** + +- Path-based routing +- TLS termination (optional) +- Annotations for ingress controller + +**HorizontalPodAutoscaler:** + +- Target CPU: 70% +- Target Memory: 80% +- Min replicas: 3, Max: 10 + +### 8.5 Database Migration in Deployment + +**Strategy:** + +- Run Alembic migrations as init container in K8s +- Run migrations as part of entrypoint script in Docker Compose/Swarm +- Ensure migrations complete before app starts + +**Rollback Plan:** + +- Keep migration reversibility (downgrade scripts) +- Database backups before deployment +- Blue-green deployment for zero downtime + +## 9. Additional Recommended Features + +### 9.1 Authentication & Authorization + +**Recommendation:** Implement JWT-based authentication + +**Rationale:** + +- Stateless authentication aligns with stateless service goal +- Supports multi-application architecture with shared auth +- Can be made optional per application + +**Components:** + +- JWT token generation and validation +- Password hashing (bcrypt/argon2) +- Role-based access control (RBAC) +- OAuth2 integration (optional) + +### 9.2 API Versioning + +**Recommendation:** URL path-based versioning + +**Strategy:** + +- Prefix routes with version: `/v1/app-a/users` +- Each app can have independent versioning +- Deprecation policy for old versions + +**Benefits:** + +- Clear version identification +- Backward compatibility support +- Gradual migration path + +### 9.3 Rate Limiting + +**Recommendation:** Redis-based rate limiting + +**Implementation:** + +- Sliding window algorithm +- Per-endpoint or per-user limits +- Configurable limits via settings +- Return 429 Too Many Requests with Retry-After header + +**Use Cases:** + +- Protect against abuse +- Fair resource allocation +- Cost control for external APIs + +### 9.4 Caching Strategy + +**Recommendation:** Multi-layer caching + +**Layers:** + +| Layer | Technology | Use Case | TTL | +|-------|-----------|----------|-----| +| Application | In-memory (lru_cache) | Configuration, static data | App lifetime | +| Distributed | Redis | Query results, computed data | 5m - 1h | +| HTTP | Response headers | Client-side caching | Varies | + +**Cache Invalidation:** + +- Time-based expiration +- Event-based invalidation (on write operations) +- Cache-aside pattern + +### 9.5 Background Task Processing + +**Recommendation:** Celery or ARQ for async tasks + +**Use Cases:** + +- Email sending +- Report generation +- Data synchronization +- Cleanup jobs + +**Integration:** + +- Redis as message broker +- Task result backend in Redis +- Separate worker processes +- Task monitoring and retry logic + +### 9.6 Database Connection Pooling + +**Recommendation:** SQLAlchemy pool configuration + +**Configuration:** + +| Parameter | Value | Rationale | +|-----------|-------|-----------| +| Pool Size | 10 per worker | Balance connection usage | +| Max Overflow | 20 | Handle traffic spikes | +| Pool Recycle | 3600s | Prevent stale connections | +| Pool Pre-ping | True | Validate connections before use | +| Echo | False (production) | Reduce log noise | + +### 9.7 Request/Response Compression + +**Recommendation:** GZip middleware + +**Configuration:** + +- Enable for responses > 1KB +- Compression level: 6 (balance speed/size) +- Exclude already-compressed content types + +### 9.8 Security Headers + +**Recommendation:** Add security headers middleware + +**Headers:** + +- `X-Content-Type-Options: nosniff` +- `X-Frame-Options: DENY` +- `X-XSS-Protection: 1; mode=block` +- `Strict-Transport-Security` (if HTTPS) +- `Content-Security-Policy` (if serving UI) + +### 9.9 Health Check Endpoint + +**Recommendation:** Comprehensive health endpoint + +**Endpoint:** `GET /health` + +**Checks:** + +| Component | Check | Healthy Criteria | +|-----------|-------|------------------| +| API | Self | Always true if responding | +| Database | Connection test | Query executes successfully | +| Redis | Ping | Returns PONG | +| OTEL | Export queue | Queue not full/blocked | + +**Response Format:** + +- 200 OK if all healthy +- 503 Service Unavailable if any unhealthy +- JSON body with component status details + +### 9.10 Metrics Collection + +**Recommendation:** Prometheus metrics + +**Metrics:** + +- Request count by endpoint, method, status +- Request duration histogram +- Active request gauge +- Database connection pool stats +- Redis connection pool stats +- Custom business metrics + +**Export:** + +- `/metrics` endpoint for Prometheus scraping +- OpenTelemetry metrics export (optional) + +### 9.11 Environment-Based Configuration + +**Recommendation:** Configuration profiles + +**Environments:** + +- Development: Debug on, verbose logging, local DB +- Staging: Production-like, test data +- Production: Optimized, minimal logging, secure + +**Override Hierarchy:** + +1. Default values in code +2. `.env` file +3. Environment variables +4. K8s ConfigMap/Secrets (in K8s) + +### 9.12 API Documentation Enhancements + +**Recommendation:** Rich OpenAPI documentation + +**Enhancements:** + +- Add description, summary, tags to all endpoints +- Provide request/response examples in schemas +- Document error responses +- Add security scheme definitions +- Custom OpenAPI metadata (contact, license) + +### 9.13 Database Read Replicas + +**Recommendation:** Support read/write splitting (future) + +**Strategy:** + +- Primary for writes +- Replicas for reads +- Routing logic in repository layer +- Configuration for replica endpoints + +**Benefits:** + +- Horizontal scaling for read-heavy workloads +- High availability + +### 9.14 Graceful Shutdown + +**Recommendation:** Handle SIGTERM properly + +**Implementation:** + +- Register shutdown event handlers +- Stop accepting new requests +- Wait for ongoing requests to complete (up to timeout) +- Close database connections +- Close Redis connections +- Flush telemetry data + +### 9.15 Feature Flags + +**Recommendation:** Feature toggle system + +**Use Cases:** + +- Gradual feature rollout +- A/B testing +- Emergency kill switches +- Environment-specific features + +**Storage:** Redis or configuration service + +## 10. Testing Strategy + +### 10.1 Unit Testing + +**Scope:** + +- Business logic in services +- Utility functions +- Validation logic +- Repository methods (with mocked DB) + +**Tools:** + +- pytest +- pytest-asyncio for async tests +- pytest-cov for coverage reports + +**Target Coverage:** > 80% + +### 10.2 Integration Testing + +**Scope:** + +- API endpoints (FastAPI TestClient) +- Database operations (test database) +- Redis operations (test Redis instance) +- Middleware behavior + +**Strategy:** + +- Use TestClient with dependency overrides +- Spin up test containers (testcontainers-python) +- Test complete request/response cycle + +### 10.3 Test Data Management + +**Approach:** + +- Fixtures for common test data +- Factory pattern for model creation +- Database rollback after each test +- Redis flush after each test + +### 10.4 Mocking Strategy + +**Mock Targets:** + +- External API calls +- OTEL exporter (avoid sending test traces) +- Time-dependent functions +- File I/O + +**Tools:** + +- pytest-mock +- unittest.mock +- responses library for HTTP mocking diff --git a/DEPENDENCIES_UPDATED.md b/DEPENDENCIES_UPDATED.md new file mode 100644 index 0000000..72e2d42 --- /dev/null +++ b/DEPENDENCIES_UPDATED.md @@ -0,0 +1,147 @@ +# Dependencies Updated to Latest Versions + +**Updated on:** October 27, 2025 + +This document lists all the dependencies that have been updated to their latest versions based on internet research. + +## Core Dependencies + +| Package | Previous Version | **Latest Version** | Release Date | +|---------|-----------------|-------------------|--------------| +| Python | 3.13 | **3.13** | October 2024 | +| FastAPI | >=0.115.0 | **>=0.120.0** | October 23, 2025 | +| Uvicorn | >=0.32.0 | **>=0.38.0** | October 18, 2025 | +| Gunicorn | >=23.0.0 | **>=23.0.0** | ✓ Latest | +| Pydantic | >=2.9.0 | **>=2.10.4** | December 18, 2024 | +| Pydantic Settings | >=2.6.0 | **>=2.7.0** | Latest | +| SQLModel | >=0.0.22 | **>=0.0.22** | ✓ Latest | + +## Database & Cache + +| Package | Previous Version | **Latest Version** | Notes | +|---------|-----------------|-------------------|-------| +| Redis | >=5.2.0 | **>=5.2.1** | Latest stable | +| PyMySQL | >=1.1.1 | **>=1.1.1** | ✓ Latest | +| aiomysql | >=0.2.0 | **>=0.2.0** | ✓ Latest | +| cryptography | >=43.0.0 | **>=44.0.0** | Latest security updates | +| Alembic | >=1.14.0 | **>=1.14.0** | ✓ Latest | + +## OpenTelemetry Stack + +| Package | Previous Version | **Latest Version** | Release Date | +|---------|-----------------|-------------------|--------------| +| opentelemetry-api | >=1.28.0 | **>=1.38.0** | October 16, 2025 | +| opentelemetry-sdk | >=1.28.0 | **>=1.38.0** | October 16, 2025 | +| opentelemetry-instrumentation-fastapi | >=0.49b0 | **>=0.49b3** | Latest beta | +| opentelemetry-instrumentation-sqlalchemy | >=0.49b0 | **>=0.49b3** | Latest beta | +| opentelemetry-instrumentation-redis | >=0.49b0 | **>=0.49b3** | Latest beta | +| opentelemetry-instrumentation-httpx | >=0.49b0 | **>=0.49b3** | Latest beta | +| opentelemetry-exporter-otlp-proto-grpc | >=1.28.0 | **>=1.38.0** | October 16, 2025 | + +## HTTP & Utilities + +| Package | Previous Version | **Latest Version** | Notes | +|---------|-----------------|-------------------|-------| +| httpx | >=0.27.0 | **>=0.28.1** | Latest async HTTP client | +| python-multipart | >=0.0.12 | **>=0.0.20** | Latest | +| python-dotenv | >=1.0.1 | **>=1.0.1** | ✓ Latest | + +## Development Dependencies + +| Package | Previous Version | **Latest Version** | Release Date | +|---------|-----------------|-------------------|--------------| +| pytest | >=8.3.0 | **>=8.3.4** | Latest | +| pytest-asyncio | >=0.24.0 | **>=0.24.0** | ✓ Latest | +| pytest-cov | >=6.0.0 | **>=6.0.0** | ✓ Latest | +| pytest-mock | >=3.14.0 | **>=3.14.0** | ✓ Latest | +| ruff | >=0.7.0 | **>=0.8.4** | Latest linter | +| mypy | >=1.13.0 | **>=1.14.0** | Latest type checker | + +## Key Highlights + +### 🚀 Major Updates + +1. **FastAPI 0.120.0** - Latest release with: + - Full Python 3.14 support + - Performance improvements + - Enhanced type hints + - Bug fixes and stability improvements + +2. **Uvicorn 0.38.0** - Latest ASGI server with: + - Python 3.14 support + - Better HTTP/2 support + - Performance optimizations + +3. **Pydantic 2.10.4** - Latest validation library: + - Python 3.14 initial support (Pydantic 2.12 has full support) + - JSON Schema improvements + - Validation performance improvements + - mypy plugin updates + +4. **OpenTelemetry 1.38.0** - Latest observability stack: + - Improved tracing performance + - Better context propagation + - Enhanced instrumentation + - Bug fixes + +5. **Ruff 0.8.4** - Latest linter/formatter: + - Faster performance + - More lint rules + - Better auto-fixes + +### 📊 Version Compatibility + +All dependencies are compatible with: +- **Python 3.13** (current stable) +- **Python 3.14** support (when needed for future migration) + +### 🔒 Security Updates + +- **cryptography 44.0.0** - Latest security patches +- **httpx 0.28.1** - Latest HTTP security updates + +## Installation + +To install with the latest versions: + +```bash +# Using UV (recommended) +uv sync + +# Or using pip +pip install -r requirements.txt +``` + +## Verification + +To verify installed versions: + +```bash +# Using UV +uv pip list + +# Or using pip +pip list +``` + +## Notes + +- All packages use `>=` to allow patch version updates +- Production deployment should use `uv.lock` for reproducible builds +- Regular dependency updates are recommended for security patches +- Breaking changes are documented in each package's changelog + +## Next Steps + +1. **Test the application** with updated dependencies +2. **Run test suite** to ensure compatibility +3. **Update `uv.lock`** by running `uv sync` +4. **Deploy to staging** for integration testing +5. **Monitor for issues** in staging before production + +## References + +- FastAPI: https://fastapi.tiangolo.com/ +- Pydantic: https://docs.pydantic.dev/ +- OpenTelemetry: https://opentelemetry.io/ +- UV: https://docs.astral.sh/uv/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..30ad1f5 --- /dev/null +++ b/README.md @@ -0,0 +1,389 @@ +# Kami Spider + +A stateless, production-ready FastAPI-based web service platform that hosts multiple independent web applications through a unified entry point. Built with modern Python ecosystem practices, full observability, and production deployment support. + +## 🚀 Features + +- **Stateless Service Architecture**: Application state externalized to Redis and MySQL +- **Multi-Application Support**: Single web service hosting independent applications (App A, App B, etc.) +- **Modern Python Stack**: Python 3.13, UV package manager, FastAPI, SQLModel, Pydantic 2.x +- **Full Observability**: OpenTelemetry integration with trace context propagation +- **Production Deployment**: Docker, Docker Compose, Docker Swarm, and Kubernetes support +- **API Quality**: Pydantic-validated I/O, auto-generated documentation, RESTful response format +- **Comprehensive Middleware**: TraceID injection, request logging, exception handling, CORS +- **Structured Logging**: JSON logs with trace context correlation +- **Health Checks**: Component-level health monitoring + +## 📋 Technology Stack + +| Component | Technology | Version | +|-----------|-----------|---------| +| Language | Python | 3.13 | +| Package Manager | UV | Latest | +| Web Framework | FastAPI | Latest | +| ORM | SQLModel | Latest | +| Validation | Pydantic | 2.x | +| ASGI Server | Uvicorn | Latest | +| Process Manager | Gunicorn | Latest | +| Database | MySQL | 8.x | +| Cache/Session | Redis | Latest | +| Observability | OpenTelemetry | Latest | + +## 🏗️ Architecture + +### System Architecture + +``` +┌─────────────────────────────────────────────────┐ +│ Load Balancer / Ingress │ +└─────────────────┬───────────────────────────────┘ + │ + ┌─────────┴─────────┐ + │ │ +┌───────▼────────┐ ┌──────▼─────────┐ +│ FastAPI Pod 1 │ │ FastAPI Pod N │ +│ (Stateless) │ │ (Stateless) │ +└───────┬────────┘ └──────┬─────────┘ + │ │ + └─────────┬─────────┘ + │ + ┌─────────┴─────────┐ + │ │ +┌───────▼────────┐ ┌──────▼─────────┐ +│ MySQL (Single) │ │ Redis (Single) │ +│ Instance │ │ Instance │ +└────────────────┘ └────────────────┘ +``` + +### Multi-Application Routing + +``` +/app-a/* → App A (User Management) +/app-b/* → App B (Product Management) +/health → Health Check +/docs → API Documentation +``` + +## 🛠️ Setup + +### Prerequisites + +- Python 3.13+ +- UV package manager +- MySQL 8.x (or use Docker) +- Redis (or use Docker) + +### Installation + +1. **Clone the repository** + +```bash +git clone +cd kami_spider +``` + +2. **Install UV** (if not already installed) + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +3. **Install dependencies** + +```bash +uv sync +``` + +4. **Configure environment** + +```bash +cp .env.example .env +# Edit .env with your configuration +``` + +5. **Run with Docker Compose** (Recommended for local development) + +```bash +cd deployments/docker +docker-compose up -d +``` + +The application will be available at `http://localhost:8000` + +### Manual Setup (Without Docker) + +1. **Start MySQL and Redis** + +Ensure MySQL and Redis are running locally. + +2. **Update .env file** + +```env +DB_HOST=localhost +DB_PORT=3306 +DB_NAME=kami_spider +DB_USER=kami_user +DB_PASSWORD=kami_pass + +REDIS_HOST=localhost +REDIS_PORT=6379 +``` + +3. **Run the application** + +```bash +# Development mode with auto-reload +uv run python main.py + +# Or with Uvicorn directly +uv run uvicorn main:app --reload --host 0.0.0.0 --port 8000 + +# Production mode with Gunicorn +uv run gunicorn main:app \ + --workers 4 \ + --worker-class uvicorn.workers.UvicornWorker \ + --bind 0.0.0.0:8000 +``` + +## 📖 API Documentation + +Once running, visit: + +- **Swagger UI**: http://localhost:8000/docs +- **ReDoc**: http://localhost:8000/redoc +- **OpenAPI JSON**: http://localhost:8000/openapi.json + +### API Examples + +#### App A - User Management + +```bash +# Create a user +curl -X POST http://localhost:8000/app-a/users \ + -H "Content-Type: application/json" \ + -d '{ + "username": "john_doe", + "email": "john@example.com", + "password": "secret123", + "full_name": "John Doe" + }' + +# Get user +curl http://localhost:8000/app-a/users/1 + +# List users (paginated) +curl http://localhost:8000/app-a/users?page=1&page_size=10 +``` + +#### App B - Product Management + +```bash +# Create a product +curl -X POST http://localhost:8000/app-b/products \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Laptop", + "description": "High-performance laptop", + "price": "999.99", + "stock": 10, + "sku": "LAP-001" + }' + +# Get product +curl http://localhost:8000/app-b/products/1 + +# List products +curl http://localhost:8000/app-b/products?page=1&page_size=10 +``` + +## 🐳 Deployment + +### Docker Compose + +```bash +cd deployments/docker +docker-compose up -d +``` + +### Docker Swarm + +```bash +# Initialize swarm +docker swarm init + +# Deploy stack +docker stack deploy -c deployments/swarm/docker-compose.swarm.yml kami_spider +``` + +### Kubernetes + +```bash +# Apply configurations +kubectl apply -f deployments/k8s/configmap.yaml +kubectl apply -f deployments/k8s/secret.yaml +kubectl apply -f deployments/k8s/deployment.yaml +kubectl apply -f deployments/k8s/service.yaml +kubectl apply -f deployments/k8s/ingress.yaml +kubectl apply -f deployments/k8s/hpa.yaml + +# Check status +kubectl get pods -l app=kami-spider +kubectl get svc kami-spider-app +``` + +## 🔍 Observability + +### OpenTelemetry + +The application exports traces to the configured OpenTelemetry collector: + +- **Endpoint**: `38.38.251.113:31547` (gRPC) +- **Protocol**: gRPC (insecure) +- **Sampling**: Configurable rate (default 100%) + +### Logging + +Structured JSON logs in production, human-readable in development: + +```json +{ + "timestamp": "2024-01-15T10:30:00Z", + "level": "INFO", + "logger": "apps.app_a.services", + "message": "User created successfully", + "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "module": "services", + "function": "create_user" +} +``` + +### Health Check + +```bash +curl http://localhost:8000/health +``` + +Response: +```json +{ + "status": "healthy", + "components": { + "api": "healthy", + "database": "healthy", + "redis": "healthy" + }, + "environment": "development", + "version": "0.1.0" +} +``` + +## 📁 Project Structure + +``` +kami_spider/ +├── apps/ # Independent web applications +│ ├── app_a/ # User management app +│ │ ├── router.py +│ │ ├── services.py +│ │ ├── models.py +│ │ └── schemas.py +│ └── app_b/ # Product management app +│ └── ... +├── core/ # Core infrastructure +│ ├── config.py # Configuration management +│ ├── database.py # Database setup +│ ├── redis.py # Redis client +│ ├── responses.py # API response formats +│ └── exceptions.py # Custom exceptions +├── middleware/ # Middleware components +│ ├── trace_context.py # TraceID injection +│ ├── logging.py # Request/response logging +│ └── error_handler.py # Exception handling +├── observability/ # OpenTelemetry integration +│ ├── tracing.py # Distributed tracing +│ └── logging.py # Structured logging +├── deployments/ # Deployment configs +│ ├── docker/ +│ ├── swarm/ +│ └── k8s/ +├── main.py # Application entry point +├── pyproject.toml # UV project config +└── README.md +``` + +## 🔧 Configuration + +All configuration is managed through environment variables. See `.env.example` for all available options. + +### Key Configuration Categories + +- **Application**: `APP_NAME`, `ENVIRONMENT`, `DEBUG`, `LOG_LEVEL` +- **Database**: `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` +- **Redis**: `REDIS_HOST`, `REDIS_PORT`, `REDIS_DB` +- **OpenTelemetry**: `OTEL_ENABLED`, `OTEL_SERVICE_NAME`, `OTEL_EXPORTER_ENDPOINT` +- **CORS**: `CORS_ENABLED`, `CORS_ALLOW_ORIGINS` + +## 🧪 Testing + +```bash +# Run tests +uv run pytest + +# Run with coverage +uv run pytest --cov=. --cov-report=html + +# Run specific test +uv run pytest tests/unit/test_users.py +``` + +## 📝 API Response Format + +All API responses follow a unified structure: + +```json +{ + "code": 0, + "message": "Success", + "data": { ... }, + "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "timestamp": "2024-01-15T10:30:00Z" +} +``` + +### Business Codes + +| Code | Description | +|------|-------------| +| 0 | Success | +| 1001-1999 | Authentication & Authorization | +| 2000-2999 | Business Logic Errors | +| 3000-3999 | Validation Errors | +| 4000-4999 | Resource Errors | +| 5000-5999 | System Errors | +| 9000-9999 | Unknown Errors | + +## 🤝 Contributing + +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +## 📄 License + +This project is licensed under the MIT License. + +## 🙏 Acknowledgments + +- FastAPI for the excellent web framework +- UV for modern Python package management +- OpenTelemetry for observability standards +- SQLModel for elegant database ORM + +## 📞 Support + +For issues and questions: +- Open an issue on GitHub +- Check the API documentation at `/docs` +- Review logs with trace IDs for debugging diff --git a/apps/__init__.py b/apps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/app_a/__init__.py b/apps/app_a/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/app_a/models.py b/apps/app_a/models.py new file mode 100644 index 0000000..93b390f --- /dev/null +++ b/apps/app_a/models.py @@ -0,0 +1,41 @@ +""" +App A Models - Database models using SQLModel. +""" + +from typing import Optional +from datetime import datetime +from sqlmodel import SQLModel, Field + + +class UserBase(SQLModel): + """Base user model with shared fields.""" + + username: str = Field(index=True, max_length=50) + email: str = Field(index=True, max_length=100) + full_name: Optional[str] = Field(default=None, max_length=100) + is_active: bool = Field(default=True) + + +class User(UserBase, table=True): + """ + User table model. + + Represents users in the system. + """ + + __tablename__ = "users" + + id: Optional[int] = Field(default=None, primary_key=True) + hashed_password: str = Field(max_length=255) + created_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: datetime = Field(default_factory=datetime.utcnow) + + class Config: + json_schema_extra = { + "example": { + "username": "john_doe", + "email": "john@example.com", + "full_name": "John Doe", + "is_active": True, + } + } diff --git a/apps/app_a/router.py b/apps/app_a/router.py new file mode 100644 index 0000000..68856a4 --- /dev/null +++ b/apps/app_a/router.py @@ -0,0 +1,139 @@ +""" +App A Router - API endpoints for user management. +""" + +from fastapi import APIRouter, Depends, Request +from sqlalchemy.ext.asyncio import AsyncSession +from core.database import get_session +from core.responses import ApiResponse, success, paginated, ERROR_RESPONSES +from core.dependencies import get_trace_id +from apps.app_a.schemas import UserCreate, UserUpdate, UserResponse +from apps.app_a.services import UserService + +router = APIRouter( + prefix="/app-a", + tags=["App A - Users"] +) + + +@router.post( + "/users", + response_model=ApiResponse[UserResponse], + status_code=201, + summary="Create a new user", + description="Create a new user with username, email, and password", + responses={ + 201: {"description": "User created successfully"}, + 400: ERROR_RESPONSES[400], + 409: ERROR_RESPONSES[409], + 422: ERROR_RESPONSES[422], + 500: ERROR_RESPONSES[500], + } +) +async def create_user( + user_data: UserCreate, + session: AsyncSession = Depends(get_session), + trace_id: str = Depends(get_trace_id) +) -> ApiResponse[UserResponse]: + """Create a new user.""" + user = await UserService.create_user(session, user_data) + user_response = UserResponse.model_validate(user) + return success(data=user_response, message="User created successfully", trace_id=trace_id) + + +@router.get( + "/users/{user_id}", + response_model=ApiResponse[UserResponse], + summary="Get user by ID", + description="Retrieve a user by their ID", + responses={ + 200: {"description": "User retrieved successfully"}, + 404: ERROR_RESPONSES[404], + 500: ERROR_RESPONSES[500], + } +) +async def get_user( + user_id: int, + session: AsyncSession = Depends(get_session), + trace_id: str = Depends(get_trace_id) +) -> ApiResponse[UserResponse]: + """Get user by ID.""" + user = await UserService.get_user(session, user_id) + user_response = UserResponse.model_validate(user) + return success(data=user_response, trace_id=trace_id) + + +@router.get( + "/users", + response_model=ApiResponse, + summary="List all users", + description="List all users with pagination support", + responses={ + 200: {"description": "Users retrieved successfully"}, + 400: ERROR_RESPONSES[400], + 500: ERROR_RESPONSES[500], + } +) +async def list_users( + page: int = 1, + page_size: int = 10, + session: AsyncSession = Depends(get_session), + trace_id: str = Depends(get_trace_id) +) -> ApiResponse: + """List all users with pagination.""" + skip = (page - 1) * page_size + users, total = await UserService.list_users(session, skip, page_size) + user_responses = [UserResponse.model_validate(user) for user in users] + return paginated( + items=user_responses, + total=total, + page=page, + page_size=page_size, + trace_id=trace_id + ) + + +@router.put( + "/users/{user_id}", + response_model=ApiResponse[UserResponse], + summary="Update user", + description="Update user information", + responses={ + 200: {"description": "User updated successfully"}, + 400: ERROR_RESPONSES[400], + 404: ERROR_RESPONSES[404], + 422: ERROR_RESPONSES[422], + 500: ERROR_RESPONSES[500], + } +) +async def update_user( + user_id: int, + user_data: UserUpdate, + session: AsyncSession = Depends(get_session), + trace_id: str = Depends(get_trace_id) +) -> ApiResponse[UserResponse]: + """Update user.""" + user = await UserService.update_user(session, user_id, user_data) + user_response = UserResponse.model_validate(user) + return success(data=user_response, message="User updated successfully", trace_id=trace_id) + + +@router.delete( + "/users/{user_id}", + response_model=ApiResponse[None], + summary="Delete user", + description="Delete a user by ID", + responses={ + 200: {"description": "User deleted successfully"}, + 404: ERROR_RESPONSES[404], + 500: ERROR_RESPONSES[500], + } +) +async def delete_user( + user_id: int, + session: AsyncSession = Depends(get_session), + trace_id: str = Depends(get_trace_id) +) -> ApiResponse[None]: + """Delete user.""" + await UserService.delete_user(session, user_id) + return success(data=None, message="User deleted successfully", trace_id=trace_id) diff --git a/apps/app_a/schemas.py b/apps/app_a/schemas.py new file mode 100644 index 0000000..f142994 --- /dev/null +++ b/apps/app_a/schemas.py @@ -0,0 +1,69 @@ +""" +App A Schemas - Pydantic schemas for request/response validation. +""" + +from typing import Optional +from datetime import datetime +from pydantic import BaseModel, Field, EmailStr + + +class UserCreate(BaseModel): + """Schema for creating a new user.""" + + username: str = Field(..., min_length=3, max_length=50) + email: EmailStr + password: str = Field(..., min_length=6) + full_name: Optional[str] = Field(None, max_length=100) + + class Config: + json_schema_extra = { + "example": { + "username": "john_doe", + "email": "john@example.com", + "password": "secret123", + "full_name": "John Doe", + } + } + + +class UserUpdate(BaseModel): + """Schema for updating a user.""" + + email: Optional[EmailStr] = None + full_name: Optional[str] = Field(None, max_length=100) + is_active: Optional[bool] = None + + class Config: + json_schema_extra = { + "example": { + "email": "newemail@example.com", + "full_name": "John Smith", + "is_active": True, + } + } + + +class UserResponse(BaseModel): + """Schema for user response.""" + + id: int + username: str + email: str + full_name: Optional[str] + is_active: bool + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + json_schema_extra = { + "example": { + "id": 1, + "username": "john_doe", + "email": "john@example.com", + "full_name": "John Doe", + "is_active": True, + "created_at": "2024-01-15T10:30:00Z", + "updated_at": "2024-01-15T10:30:00Z", + } + } diff --git a/apps/app_a/services.py b/apps/app_a/services.py new file mode 100644 index 0000000..096a600 --- /dev/null +++ b/apps/app_a/services.py @@ -0,0 +1,187 @@ +""" +App A Services - Business logic layer. +""" + +from typing import Optional +from datetime import datetime +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from apps.app_a.models import User +from apps.app_a.schemas import UserCreate, UserUpdate +from core.exceptions import NotFoundException, AlreadyExistsException + + +class UserService: + """Service for user business logic.""" + + @staticmethod + async def create_user( + session: AsyncSession, + user_data: UserCreate + ) -> User: + """ + Create a new user. + + Args: + session: Database session + user_data: User creation data + + Returns: + User: Created user + + Raises: + AlreadyExistsException: If username or email already exists + """ + # Check if username exists + result = await session.execute( + select(User).where(User.username == user_data.username) + ) + if result.scalar_one_or_none(): + raise AlreadyExistsException( + message=f"Username '{user_data.username}' already exists", + resource="User" + ) + + # Check if email exists + result = await session.execute( + select(User).where(User.email == user_data.email) + ) + if result.scalar_one_or_none(): + raise AlreadyExistsException( + message=f"Email '{user_data.email}' already exists", + resource="User" + ) + + # Create user (in production, hash the password) + user = User( + username=user_data.username, + email=user_data.email, + full_name=user_data.full_name, + hashed_password=f"hashed_{user_data.password}", # TODO: Use proper hashing + created_at=datetime.utcnow(), + updated_at=datetime.utcnow() + ) + + session.add(user) + await session.commit() + await session.refresh(user) + + return user + + @staticmethod + async def get_user( + session: AsyncSession, + user_id: int + ) -> User: + """ + Get user by ID. + + Args: + session: Database session + user_id: User ID + + Returns: + User: User instance + + Raises: + NotFoundException: If user not found + """ + result = await session.execute( + select(User).where(User.id == user_id) + ) + user = result.scalar_one_or_none() + + if not user: + raise NotFoundException( + message=f"User with ID {user_id} not found", + resource="User" + ) + + return user + + @staticmethod + async def list_users( + session: AsyncSession, + skip: int = 0, + limit: int = 100 + ) -> tuple[list[User], int]: + """ + List all users with pagination. + + Args: + session: Database session + skip: Number of records to skip + limit: Maximum number of records to return + + Returns: + tuple: (list of users, total count) + """ + # Get total count + count_result = await session.execute( + select(User) + ) + total = len(count_result.all()) + + # Get paginated results + result = await session.execute( + select(User).offset(skip).limit(limit) + ) + users = result.scalars().all() + + return list(users), total + + @staticmethod + async def update_user( + session: AsyncSession, + user_id: int, + user_data: UserUpdate + ) -> User: + """ + Update user. + + Args: + session: Database session + user_id: User ID + user_data: Update data + + Returns: + User: Updated user + + Raises: + NotFoundException: If user not found + """ + user = await UserService.get_user(session, user_id) + + # Update fields + if user_data.email is not None: + user.email = user_data.email + if user_data.full_name is not None: + user.full_name = user_data.full_name + if user_data.is_active is not None: + user.is_active = user_data.is_active + + user.updated_at = datetime.utcnow() + + await session.commit() + await session.refresh(user) + + return user + + @staticmethod + async def delete_user( + session: AsyncSession, + user_id: int + ) -> None: + """ + Delete user. + + Args: + session: Database session + user_id: User ID + + Raises: + NotFoundException: If user not found + """ + user = await UserService.get_user(session, user_id) + await session.delete(user) + await session.commit() diff --git a/apps/app_b/__init__.py b/apps/app_b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/app_b/models.py b/apps/app_b/models.py new file mode 100644 index 0000000..64b2aff --- /dev/null +++ b/apps/app_b/models.py @@ -0,0 +1,45 @@ +""" +App B Models - Database models for products. +""" + +from typing import Optional +from datetime import datetime +from decimal import Decimal +from sqlmodel import SQLModel, Field + + +class ProductBase(SQLModel): + """Base product model with shared fields.""" + + name: str = Field(index=True, max_length=100) + description: Optional[str] = Field(default=None, max_length=500) + price: Decimal = Field(decimal_places=2) + stock: int = Field(default=0, ge=0) + is_available: bool = Field(default=True) + + +class Product(ProductBase, table=True): + """ + Product table model. + + Represents products in the system. + """ + + __tablename__ = "products" + + id: Optional[int] = Field(default=None, primary_key=True) + sku: str = Field(unique=True, index=True, max_length=50) + created_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: datetime = Field(default_factory=datetime.utcnow) + + class Config: + json_schema_extra = { + "example": { + "name": "Laptop", + "description": "High-performance laptop", + "price": "999.99", + "stock": 10, + "sku": "LAP-001", + "is_available": True, + } + } diff --git a/apps/app_b/router.py b/apps/app_b/router.py new file mode 100644 index 0000000..c3a28b9 --- /dev/null +++ b/apps/app_b/router.py @@ -0,0 +1,93 @@ +""" +App B Router - API endpoints for product management. +""" + +from fastapi import APIRouter, Depends +from sqlalchemy.ext.asyncio import AsyncSession +from core.database import get_session +from core.responses import ApiResponse, success, paginated, ERROR_RESPONSES +from core.dependencies import get_trace_id +from apps.app_b.schemas import ProductCreate, ProductResponse +from apps.app_b.services import ProductService + +router = APIRouter( + prefix="/app-b", + tags=["App B - Products"] +) + + +@router.post( + "/products", + response_model=ApiResponse[ProductResponse], + status_code=201, + summary="Create a new product", + description="Create a new product with SKU, name, price, and stock", + responses={ + 201: {"description": "Product created successfully"}, + 400: ERROR_RESPONSES[400], + 409: ERROR_RESPONSES[409], + 422: ERROR_RESPONSES[422], + 500: ERROR_RESPONSES[500], + } +) +async def create_product( + product_data: ProductCreate, + session: AsyncSession = Depends(get_session), + trace_id: str = Depends(get_trace_id) +) -> ApiResponse[ProductResponse]: + """Create a new product.""" + product = await ProductService.create_product(session, product_data) + product_response = ProductResponse.model_validate(product) + return success(data=product_response, message="Product created successfully", trace_id=trace_id) + + +@router.get( + "/products/{product_id}", + response_model=ApiResponse[ProductResponse], + summary="Get product by ID", + description="Retrieve a product by its ID", + responses={ + 200: {"description": "Product retrieved successfully"}, + 404: ERROR_RESPONSES[404], + 500: ERROR_RESPONSES[500], + } +) +async def get_product( + product_id: int, + session: AsyncSession = Depends(get_session), + trace_id: str = Depends(get_trace_id) +) -> ApiResponse[ProductResponse]: + """Get product by ID.""" + product = await ProductService.get_product(session, product_id) + product_response = ProductResponse.model_validate(product) + return success(data=product_response, trace_id=trace_id) + + +@router.get( + "/products", + response_model=ApiResponse, + summary="List all products", + description="List all products with pagination support", + responses={ + 200: {"description": "Products retrieved successfully"}, + 400: ERROR_RESPONSES[400], + 500: ERROR_RESPONSES[500], + } +) +async def list_products( + page: int = 1, + page_size: int = 10, + session: AsyncSession = Depends(get_session), + trace_id: str = Depends(get_trace_id) +) -> ApiResponse: + """List all products with pagination.""" + skip = (page - 1) * page_size + products, total = await ProductService.list_products(session, skip, page_size) + product_responses = [ProductResponse.model_validate(product) for product in products] + return paginated( + items=product_responses, + total=total, + page=page, + page_size=page_size, + trace_id=trace_id + ) diff --git a/apps/app_b/schemas.py b/apps/app_b/schemas.py new file mode 100644 index 0000000..f8db9b2 --- /dev/null +++ b/apps/app_b/schemas.py @@ -0,0 +1,80 @@ +""" +App B Schemas - Pydantic schemas for products. +""" + +from typing import Optional +from datetime import datetime +from decimal import Decimal +from pydantic import BaseModel, Field + + +class ProductCreate(BaseModel): + """Schema for creating a new product.""" + + name: str = Field(..., min_length=1, max_length=100) + description: Optional[str] = Field(None, max_length=500) + price: Decimal = Field(..., gt=0, decimal_places=2) + stock: int = Field(default=0, ge=0) + sku: str = Field(..., min_length=1, max_length=50) + is_available: bool = Field(default=True) + + class Config: + json_schema_extra = { + "example": { + "name": "Laptop", + "description": "High-performance laptop", + "price": "999.99", + "stock": 10, + "sku": "LAP-001", + "is_available": True, + } + } + + +class ProductUpdate(BaseModel): + """Schema for updating a product.""" + + name: Optional[str] = Field(None, min_length=1, max_length=100) + description: Optional[str] = Field(None, max_length=500) + price: Optional[Decimal] = Field(None, gt=0, decimal_places=2) + stock: Optional[int] = Field(None, ge=0) + is_available: Optional[bool] = None + + class Config: + json_schema_extra = { + "example": { + "name": "Updated Laptop", + "price": "899.99", + "stock": 15, + } + } + + +class ProductResponse(BaseModel): + """Schema for product response.""" + + id: int + name: str + description: Optional[str] + price: Decimal + stock: int + sku: str + is_available: bool + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + json_schema_extra = { + "example": { + "id": 1, + "name": "Laptop", + "description": "High-performance laptop", + "price": "999.99", + "stock": 10, + "sku": "LAP-001", + "is_available": True, + "created_at": "2024-01-15T10:30:00Z", + "updated_at": "2024-01-15T10:30:00Z", + } + } diff --git a/apps/app_b/services.py b/apps/app_b/services.py new file mode 100644 index 0000000..602abb7 --- /dev/null +++ b/apps/app_b/services.py @@ -0,0 +1,78 @@ +""" +App B Services - Business logic for products. +""" + +from datetime import datetime +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from apps.app_b.models import Product +from apps.app_b.schemas import ProductCreate, ProductUpdate +from core.exceptions import NotFoundException, AlreadyExistsException + + +class ProductService: + """Service for product business logic.""" + + @staticmethod + async def create_product( + session: AsyncSession, + product_data: ProductCreate + ) -> Product: + """Create a new product.""" + # Check if SKU exists + result = await session.execute( + select(Product).where(Product.sku == product_data.sku) + ) + if result.scalar_one_or_none(): + raise AlreadyExistsException( + message=f"Product with SKU '{product_data.sku}' already exists", + resource="Product" + ) + + product = Product( + **product_data.model_dump(), + created_at=datetime.utcnow(), + updated_at=datetime.utcnow() + ) + + session.add(product) + await session.commit() + await session.refresh(product) + + return product + + @staticmethod + async def get_product( + session: AsyncSession, + product_id: int + ) -> Product: + """Get product by ID.""" + result = await session.execute( + select(Product).where(Product.id == product_id) + ) + product = result.scalar_one_or_none() + + if not product: + raise NotFoundException( + message=f"Product with ID {product_id} not found", + resource="Product" + ) + + return product + + @staticmethod + async def list_products( + session: AsyncSession, + skip: int = 0, + limit: int = 100 + ) -> tuple[list[Product], int]: + """List all products with pagination.""" + count_result = await session.execute(select(Product)) + total = len(count_result.all()) + + result = await session.execute( + select(Product).offset(skip).limit(limit) + ) + products = result.scalars().all() + + return list(products), total diff --git a/apps/shared/__init__.py b/apps/shared/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/config.py b/core/config.py new file mode 100644 index 0000000..6f2c41d --- /dev/null +++ b/core/config.py @@ -0,0 +1,145 @@ +""" +Core configuration management using Pydantic Settings. +All configuration loaded from environment variables with validation. +""" + +from typing import Literal +from pydantic import Field, field_validator +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + """ + Application settings with environment variable support. + + Configuration is loaded from environment variables and .env files. + All settings are validated at startup using Pydantic validators. + """ + + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + case_sensitive=False, + extra="ignore", + ) + + # Application Settings + app_name: str = Field(default="kami_spider", description="Application name") + environment: Literal["development", "staging", "production"] = Field( + default="development", + description="Runtime environment" + ) + debug: bool = Field(default=False, description="Debug mode") + host: str = Field(default="0.0.0.0", description="Server host") + port: int = Field(default=8000, description="Server port") + workers: int = Field(default=1, description="Number of worker processes") + log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field( + default="INFO", + description="Logging level" + ) + + # Database Settings + db_host: str = Field(default="localhost", description="MySQL host") + db_port: int = Field(default=3306, description="MySQL port") + db_name: str = Field(default="kami_spider", description="Database name") + db_user: str = Field(default="root", description="Database user") + db_password: str = Field(default="", description="Database password") + db_pool_size: int = Field(default=10, description="Database connection pool size") + db_max_overflow: int = Field(default=20, description="Database max overflow connections") + db_pool_recycle: int = Field(default=3600, description="Database pool recycle time in seconds") + db_pool_pre_ping: bool = Field(default=True, description="Test connections before using") + db_echo: bool = Field(default=False, description="Echo SQL statements") + + # Redis Settings + redis_host: str = Field(default="localhost", description="Redis host") + redis_port: int = Field(default=6379, description="Redis port") + redis_db: int = Field(default=0, description="Redis database number") + redis_password: str = Field(default="", description="Redis password") + redis_max_connections: int = Field(default=50, description="Redis connection pool max connections") + redis_decode_responses: bool = Field(default=True, description="Decode Redis responses to strings") + + # OpenTelemetry Settings + otel_enabled: bool = Field(default=True, description="Enable OpenTelemetry") + otel_service_name: str = Field(default="kami_spider", description="Service name for traces") + otel_exporter_endpoint: str = Field( + default="38.38.251.113:31547", + description="OpenTelemetry collector gRPC endpoint" + ) + otel_exporter_insecure: bool = Field(default=True, description="Use insecure gRPC connection") + otel_sample_rate: float = Field(default=1.0, description="Trace sampling rate (0.0 to 1.0)") + + # CORS Settings + cors_enabled: bool = Field(default=True, description="Enable CORS") + cors_allow_origins: list[str] = Field( + default=["*"], + description="Allowed CORS origins" + ) + cors_allow_credentials: bool = Field(default=True, description="Allow credentials in CORS") + cors_allow_methods: list[str] = Field( + default=["*"], + description="Allowed HTTP methods" + ) + cors_allow_headers: list[str] = Field( + default=["*"], + description="Allowed HTTP headers" + ) + + # Security Settings + secret_key: str = Field( + default="change-me-in-production", + description="Secret key for signing tokens" + ) + + @field_validator("workers") + @classmethod + def validate_workers(cls, v: int) -> int: + """Ensure workers is at least 1.""" + if v < 1: + raise ValueError("workers must be at least 1") + return v + + @field_validator("otel_sample_rate") + @classmethod + def validate_sample_rate(cls, v: float) -> float: + """Ensure sample rate is between 0.0 and 1.0.""" + if not 0.0 <= v <= 1.0: + raise ValueError("otel_sample_rate must be between 0.0 and 1.0") + return v + + @property + def database_url(self) -> str: + """Generate async database URL for SQLModel/SQLAlchemy.""" + password_part = f":{self.db_password}" if self.db_password else "" + return ( + f"mysql+aiomysql://{self.db_user}{password_part}" + f"@{self.db_host}:{self.db_port}/{self.db_name}" + ) + + @property + def sync_database_url(self) -> str: + """Generate sync database URL for Alembic migrations.""" + password_part = f":{self.db_password}" if self.db_password else "" + return ( + f"mysql+pymysql://{self.db_user}{password_part}" + f"@{self.db_host}:{self.db_port}/{self.db_name}" + ) + + @property + def redis_url(self) -> str: + """Generate Redis URL.""" + password_part = f":{self.redis_password}@" if self.redis_password else "" + return f"redis://{password_part}{self.redis_host}:{self.redis_port}/{self.redis_db}" + + @property + def is_production(self) -> bool: + """Check if running in production environment.""" + return self.environment == "production" + + @property + def is_development(self) -> bool: + """Check if running in development environment.""" + return self.environment == "development" + + +# Global settings instance +settings = Settings() diff --git a/core/database.py b/core/database.py new file mode 100644 index 0000000..bb0fd20 --- /dev/null +++ b/core/database.py @@ -0,0 +1,135 @@ +""" +Database layer with SQLModel and async SQLAlchemy engine. +Provides connection pooling and session management. +""" + +from typing import AsyncGenerator +from contextlib import asynccontextmanager +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker +from sqlalchemy.pool import NullPool, AsyncAdaptedQueuePool +from sqlalchemy import text +from sqlmodel import SQLModel +from core.config import settings + + +# Create async engine with connection pooling +if settings.debug: + # In debug mode, use NullPool (no pooling parameters) + engine = create_async_engine( + settings.database_url, + echo=settings.db_echo, + pool_pre_ping=settings.db_pool_pre_ping, + poolclass=NullPool, + ) +else: + # In production mode, use AsyncAdaptedQueuePool with pooling parameters + engine = create_async_engine( + settings.database_url, + echo=settings.db_echo, + pool_size=settings.db_pool_size, + max_overflow=settings.db_max_overflow, + pool_recycle=settings.db_pool_recycle, + pool_pre_ping=settings.db_pool_pre_ping, + poolclass=AsyncAdaptedQueuePool, + ) + +# Create async session factory +AsyncSessionLocal = async_sessionmaker( + engine, + class_=AsyncSession, + expire_on_commit=False, + autocommit=False, + autoflush=False, +) + + +async def get_session() -> AsyncGenerator[AsyncSession, None]: + """ + Dependency that provides async database session. + + Usage in FastAPI: + @app.get("/users") + async def get_users(session: AsyncSession = Depends(get_session)): + ... + + Yields: + AsyncSession: Database session with automatic cleanup + """ + async with AsyncSessionLocal() as session: + try: + yield session + await session.commit() + except Exception: + await session.rollback() + raise + finally: + await session.close() + + +@asynccontextmanager +async def get_session_context() -> AsyncGenerator[AsyncSession, None]: + """ + Context manager for database session. + + Usage: + async with get_session_context() as session: + result = await session.execute(select(User)) + + Yields: + AsyncSession: Database session with automatic cleanup + """ + async with AsyncSessionLocal() as session: + try: + yield session + await session.commit() + except Exception: + await session.rollback() + raise + finally: + await session.close() + + +async def create_db_and_tables() -> None: + """ + Create all database tables. + + This should be called at application startup in development. + In production, use Alembic migrations instead. + """ + async with engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.create_all) + + +async def drop_db_and_tables() -> None: + """ + Drop all database tables. + + WARNING: This will delete all data. Use with caution. + Only use in development/testing environments. + """ + async with engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.drop_all) + + +async def check_database_connection() -> bool: + """ + Check if database connection is healthy. + + Returns: + bool: True if connection is healthy, False otherwise + """ + try: + async with engine.connect() as conn: + await conn.execute(text("SELECT 1")) + return True + except Exception: + return False + + +async def close_database_connection() -> None: + """ + Close database connection pool. + + This should be called at application shutdown. + """ + await engine.dispose() diff --git a/core/dependencies.py b/core/dependencies.py new file mode 100644 index 0000000..cdc4167 --- /dev/null +++ b/core/dependencies.py @@ -0,0 +1,30 @@ +""" +Global dependencies for FastAPI dependency injection. +""" + +from typing import AsyncGenerator +from fastapi import Request +from sqlalchemy.ext.asyncio import AsyncSession +from redis.asyncio import Redis +from core.database import get_session +from core.redis import get_redis + + +async def get_trace_id(request: Request) -> str: + """ + Get trace ID from request state. + + Args: + request: FastAPI request object + + Returns: + str: Trace ID + """ + return getattr(request.state, "trace_id", "") + + +__all__ = [ + "get_session", + "get_redis", + "get_trace_id", +] diff --git a/core/exceptions.py b/core/exceptions.py new file mode 100644 index 0000000..35bb707 --- /dev/null +++ b/core/exceptions.py @@ -0,0 +1,249 @@ +""" +Custom exception hierarchy for application errors. +Maps exceptions to HTTP status codes and business codes. +""" + +from typing import Optional, Any +from core.responses import BusinessCode + + +class BaseAppException(Exception): + """ + Base exception for all application exceptions. + + Attributes: + message: Error message + code: Business error code + status_code: HTTP status code + details: Additional error details + """ + + def __init__( + self, + message: str, + code: int = BusinessCode.UNKNOWN_ERROR, + status_code: int = 500, + details: Optional[dict[str, Any]] = None + ): + self.message = message + self.code = code + self.status_code = status_code + self.details = details or {} + super().__init__(self.message) + + +class ValidationException(BaseAppException): + """ + Exception for validation errors. + + HTTP Status: 400 Bad Request + Business Code Range: 3000-3999 + """ + + def __init__( + self, + message: str, + code: int = BusinessCode.INVALID_INPUT, + details: Optional[dict[str, Any]] = None + ): + super().__init__( + message=message, + code=code, + status_code=400, + details=details + ) + + +class NotFoundException(BaseAppException): + """ + Exception for resource not found errors. + + HTTP Status: 404 Not Found + Business Code: 4001 + """ + + def __init__( + self, + message: str, + resource: str = "Resource", + details: Optional[dict[str, Any]] = None + ): + super().__init__( + message=message or f"{resource} not found", + code=BusinessCode.RESOURCE_NOT_FOUND, + status_code=404, + details=details + ) + + +class ConflictException(BaseAppException): + """ + Exception for resource conflict errors. + + HTTP Status: 409 Conflict + Business Code: 4003 + """ + + def __init__( + self, + message: str, + code: int = BusinessCode.RESOURCE_CONFLICT, + details: Optional[dict[str, Any]] = None + ): + super().__init__( + message=message, + code=code, + status_code=409, + details=details + ) + + +class AlreadyExistsException(BaseAppException): + """ + Exception for resource already exists errors. + + HTTP Status: 409 Conflict + Business Code: 4002 + """ + + def __init__( + self, + message: str, + resource: str = "Resource", + details: Optional[dict[str, Any]] = None + ): + super().__init__( + message=message or f"{resource} already exists", + code=BusinessCode.RESOURCE_ALREADY_EXISTS, + status_code=409, + details=details + ) + + +class AuthenticationException(BaseAppException): + """ + Exception for authentication errors. + + HTTP Status: 401 Unauthorized + Business Code Range: 1001-1099 + """ + + def __init__( + self, + message: str, + code: int = BusinessCode.LOGIN_FAILED, + details: Optional[dict[str, Any]] = None + ): + super().__init__( + message=message, + code=code, + status_code=401, + details=details + ) + + +class PermissionException(BaseAppException): + """ + Exception for permission/authorization errors. + + HTTP Status: 403 Forbidden + Business Code: 1003 + """ + + def __init__( + self, + message: str, + code: int = BusinessCode.INSUFFICIENT_PERMISSIONS, + details: Optional[dict[str, Any]] = None + ): + super().__init__( + message=message, + code=code, + status_code=403, + details=details + ) + + +class BusinessLogicException(BaseAppException): + """ + Exception for business logic errors. + + HTTP Status: 400 Bad Request + Business Code Range: 2000-2999 + """ + + def __init__( + self, + message: str, + code: int = BusinessCode.OPERATION_NOT_ALLOWED, + details: Optional[dict[str, Any]] = None + ): + super().__init__( + message=message, + code=code, + status_code=400, + details=details + ) + + +class DatabaseException(BaseAppException): + """ + Exception for database errors. + + HTTP Status: 503 Service Unavailable + Business Code: 5001 + """ + + def __init__( + self, + message: str = "Database error occurred", + details: Optional[dict[str, Any]] = None + ): + super().__init__( + message=message, + code=BusinessCode.DATABASE_ERROR, + status_code=503, + details=details + ) + + +class CacheException(BaseAppException): + """ + Exception for cache/Redis errors. + + HTTP Status: 503 Service Unavailable + Business Code: 5003 + """ + + def __init__( + self, + message: str = "Cache service error", + details: Optional[dict[str, Any]] = None + ): + super().__init__( + message=message, + code=BusinessCode.CACHE_ERROR, + status_code=503, + details=details + ) + + +class ExternalServiceException(BaseAppException): + """ + Exception for external service errors. + + HTTP Status: 502 Bad Gateway + Business Code: 5002 + """ + + def __init__( + self, + message: str = "External service unavailable", + details: Optional[dict[str, Any]] = None + ): + super().__init__( + message=message, + code=BusinessCode.EXTERNAL_SERVICE_ERROR, + status_code=502, + details=details + ) diff --git a/core/redis.py b/core/redis.py new file mode 100644 index 0000000..57be535 --- /dev/null +++ b/core/redis.py @@ -0,0 +1,249 @@ +""" +Redis integration with async client and connection pooling. +Provides shared Redis instance for all applications. +""" + +from typing import Optional, Any +import json +from redis import asyncio as aioredis +from redis.asyncio import Redis, ConnectionPool +from core.config import settings + + +# Create Redis connection pool +redis_pool: Optional[ConnectionPool] = None +redis_client: Optional[Redis] = None + + +async def init_redis() -> Redis: + """ + Initialize Redis client with connection pool. + + This should be called at application startup. + + Returns: + Redis: Async Redis client + """ + global redis_pool, redis_client + + redis_pool = ConnectionPool.from_url( + settings.redis_url, + max_connections=settings.redis_max_connections, + decode_responses=settings.redis_decode_responses, + ) + + redis_client = Redis(connection_pool=redis_pool) + + return redis_client + + +async def get_redis() -> Redis: + """ + Dependency that provides Redis client. + + Usage in FastAPI: + @app.get("/cache") + async def get_cache(redis: Redis = Depends(get_redis)): + ... + + Returns: + Redis: Async Redis client + + Raises: + RuntimeError: If Redis client is not initialized + """ + if redis_client is None: + raise RuntimeError("Redis client not initialized. Call init_redis() at startup.") + return redis_client + + +async def check_redis_connection() -> bool: + """ + Check if Redis connection is healthy. + + Returns: + bool: True if connection is healthy, False otherwise + """ + try: + if redis_client is None: + return False + await redis_client.ping() + return True + except Exception: + return False + + +async def close_redis_connection() -> None: + """ + Close Redis connection pool. + + This should be called at application shutdown. + """ + global redis_pool, redis_client + + if redis_client: + await redis_client.aclose() + redis_client = None + + if redis_pool: + await redis_pool.disconnect() + redis_pool = None + + +class RedisCache: + """ + Redis cache utility with common operations. + + Provides helper methods for caching with TTL and JSON serialization. + """ + + def __init__(self, redis: Redis, prefix: str = ""): + """ + Initialize cache with Redis client and optional key prefix. + + Args: + redis: Redis client instance + prefix: Optional key prefix for namespace isolation + """ + self.redis = redis + self.prefix = prefix + + def _make_key(self, key: str) -> str: + """Generate prefixed key.""" + return f"{self.prefix}:{key}" if self.prefix else key + + async def get(self, key: str) -> Optional[str]: + """ + Get value by key. + + Args: + key: Cache key + + Returns: + Optional[str]: Cached value or None if not found + """ + return await self.redis.get(self._make_key(key)) + + async def set( + self, + key: str, + value: str, + ttl: Optional[int] = None + ) -> bool: + """ + Set value with optional TTL. + + Args: + key: Cache key + value: Value to cache + ttl: Time to live in seconds (optional) + + Returns: + bool: True if successful + """ + return await self.redis.set( + self._make_key(key), + value, + ex=ttl + ) + + async def get_json(self, key: str) -> Optional[Any]: + """ + Get JSON value by key. + + Args: + key: Cache key + + Returns: + Optional[Any]: Deserialized JSON value or None + """ + value = await self.get(key) + if value is None: + return None + try: + return json.loads(value) + except json.JSONDecodeError: + return None + + async def set_json( + self, + key: str, + value: Any, + ttl: Optional[int] = None + ) -> bool: + """ + Set JSON value with optional TTL. + + Args: + key: Cache key + value: Value to serialize and cache + ttl: Time to live in seconds (optional) + + Returns: + bool: True if successful + """ + json_value = json.dumps(value) + return await self.set(key, json_value, ttl) + + async def delete(self, key: str) -> int: + """ + Delete key. + + Args: + key: Cache key + + Returns: + int: Number of keys deleted + """ + return await self.redis.delete(self._make_key(key)) + + async def exists(self, key: str) -> bool: + """ + Check if key exists. + + Args: + key: Cache key + + Returns: + bool: True if key exists + """ + return await self.redis.exists(self._make_key(key)) > 0 + + async def expire(self, key: str, ttl: int) -> bool: + """ + Set TTL for existing key. + + Args: + key: Cache key + ttl: Time to live in seconds + + Returns: + bool: True if successful + """ + return await self.redis.expire(self._make_key(key), ttl) + + async def increment(self, key: str, amount: int = 1) -> int: + """ + Increment numeric value. + + Args: + key: Cache key + amount: Increment amount (default 1) + + Returns: + int: New value after increment + """ + return await self.redis.incrby(self._make_key(key), amount) + + async def decrement(self, key: str, amount: int = 1) -> int: + """ + Decrement numeric value. + + Args: + key: Cache key + amount: Decrement amount (default 1) + + Returns: + int: New value after decrement + """ + return await self.redis.decrby(self._make_key(key), amount) diff --git a/core/responses.py b/core/responses.py new file mode 100644 index 0000000..a96bcb0 --- /dev/null +++ b/core/responses.py @@ -0,0 +1,392 @@ +""" +RESTful response format with generic types. +Provides unified response structure for all API endpoints. +""" + +from typing import TypeVar, Generic, Optional, Any +from datetime import datetime +from pydantic import BaseModel, Field + + +T = TypeVar("T") + + +class ApiResponse(BaseModel, Generic[T]): + """ + Unified API response structure. + + Both success and error responses use this structure. + + Attributes: + code: Business status code (0 = success, >0 = error) + message: Human-readable message + data: Response payload (typed) or None for errors + trace_id: Request trace ID for debugging + timestamp: Response timestamp in ISO 8601 format + """ + + code: int = Field(description="Business status code (0=success, >0=error)") + message: str = Field(description="Human-readable message") + data: Optional[T] = Field(default=None, description="Response payload") + trace_id: str = Field(description="Request trace ID") + timestamp: datetime = Field(default_factory=datetime.utcnow, description="Response timestamp") + + class Config: + json_schema_extra = { + "example": { + "code": 0, + "message": "Success", + "data": {"user_id": 12345, "username": "john_doe"}, + "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "timestamp": "2024-01-15T10:30:00Z" + } + } + + +class PaginationMeta(BaseModel): + """Pagination metadata.""" + + total: int = Field(description="Total number of records") + page: int = Field(description="Current page number (1-based)") + page_size: int = Field(description="Number of records per page") + total_pages: int = Field(description="Total number of pages") + + +class PaginatedData(BaseModel, Generic[T]): + """ + Paginated data structure. + + Attributes: + items: List of items for current page + pagination: Pagination metadata + """ + + items: list[T] = Field(description="List of items") + pagination: PaginationMeta = Field(description="Pagination metadata") + + +def success( + data: Optional[T] = None, + message: str = "Success", + trace_id: str = "" +) -> ApiResponse[T]: + """ + Create success response. + + Args: + data: Response payload + message: Success message + trace_id: Request trace ID + + Returns: + ApiResponse: Success response with code=0 + + Example: + return success(data={"user_id": 123}, message="User created") + """ + return ApiResponse( + code=0, + message=message, + data=data, + trace_id=trace_id, + timestamp=datetime.utcnow() + ) + + +def error( + code: int, + message: str, + trace_id: str = "" +) -> ApiResponse[None]: + """ + Create error response. + + Args: + code: Business error code (must be > 0) + message: Error message + trace_id: Request trace ID + + Returns: + ApiResponse: Error response with data=None + + Example: + return error(code=1001, message="Login failed: Invalid credentials") + """ + if code <= 0: + raise ValueError("Error code must be greater than 0") + + return ApiResponse( + code=code, + message=message, + data=None, + trace_id=trace_id, + timestamp=datetime.utcnow() + ) + + +def paginated( + items: list[T], + total: int, + page: int, + page_size: int, + message: str = "Success", + trace_id: str = "" +) -> ApiResponse[PaginatedData[T]]: + """ + Create paginated response. + + Args: + items: List of items for current page + total: Total number of records + page: Current page number (1-based) + page_size: Number of records per page + message: Success message + trace_id: Request trace ID + + Returns: + ApiResponse: Paginated response + + Example: + return paginated( + items=[user1, user2], + total=100, + page=1, + page_size=10 + ) + """ + total_pages = (total + page_size - 1) // page_size if page_size > 0 else 0 + + pagination_meta = PaginationMeta( + total=total, + page=page, + page_size=page_size, + total_pages=total_pages + ) + + paginated_data = PaginatedData( + items=items, + pagination=pagination_meta + ) + + return ApiResponse( + code=0, + message=message, + data=paginated_data, + trace_id=trace_id, + timestamp=datetime.utcnow() + ) + + +# Business code constants +class BusinessCode: + """ + Business status code definitions. + + Code ranges: + 0: Success + 1000-1999: Authentication & Authorization + 2000-2999: Business Logic Errors + 3000-3999: Validation Errors + 4000-4999: Resource Errors + 5000-5999: System Errors + 9000-9999: Unknown Errors + """ + + # Success + SUCCESS = 0 + + # Authentication & Authorization (1000-1999) + LOGIN_FAILED = 1001 + TOKEN_EXPIRED = 1002 + INSUFFICIENT_PERMISSIONS = 1003 + INVALID_TOKEN = 1004 + + # Business Logic Errors (2000-2999) + ORDER_CREATION_FAILED = 2001 + PAYMENT_FAILED = 2002 + INSUFFICIENT_BALANCE = 2003 + OPERATION_NOT_ALLOWED = 2004 + + # Validation Errors (3000-3999) + INVALID_INPUT = 3001 + MISSING_REQUIRED_FIELD = 3002 + INVALID_FORMAT = 3003 + + # Resource Errors (4000-4999) + RESOURCE_NOT_FOUND = 4001 + RESOURCE_ALREADY_EXISTS = 4002 + RESOURCE_CONFLICT = 4003 + + # System Errors (5000-5999) + DATABASE_ERROR = 5001 + EXTERNAL_SERVICE_ERROR = 5002 + CACHE_ERROR = 5003 + + # Unknown Errors (9000-9999) + UNKNOWN_ERROR = 9000 + + +# Common error response examples for OpenAPI documentation +ERROR_RESPONSES = { + 400: { + "description": "Bad Request - Validation or business logic error", + "content": { + "application/json": { + "example": { + "code": 3001, + "message": "Validation error: email - field required", + "data": None, + "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "timestamp": "2024-01-15T10:30:00Z" + } + } + } + }, + 401: { + "description": "Unauthorized - Authentication failed", + "content": { + "application/json": { + "example": { + "code": 1001, + "message": "Login failed: Invalid credentials", + "data": None, + "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "timestamp": "2024-01-15T10:30:00Z" + } + } + } + }, + 403: { + "description": "Forbidden - Insufficient permissions", + "content": { + "application/json": { + "example": { + "code": 1003, + "message": "Insufficient permissions to perform this action", + "data": None, + "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "timestamp": "2024-01-15T10:30:00Z" + } + } + } + }, + 404: { + "description": "Not Found - Resource does not exist", + "content": { + "application/json": { + "example": { + "code": 4001, + "message": "User not found", + "data": None, + "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "timestamp": "2024-01-15T10:30:00Z" + } + } + } + }, + 409: { + "description": "Conflict - Resource already exists or conflicts", + "content": { + "application/json": { + "example": { + "code": 4002, + "message": "User already exists", + "data": None, + "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "timestamp": "2024-01-15T10:30:00Z" + } + } + } + }, + 422: { + "description": "Unprocessable Entity - Validation error", + "content": { + "application/json": { + "example": { + "code": 3001, + "message": "Validation error: body -> email - value is not a valid email address", + "data": None, + "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "timestamp": "2024-01-15T10:30:00Z" + } + } + } + }, + 500: { + "description": "Internal Server Error - Unexpected error", + "content": { + "application/json": { + "example": { + "code": 9000, + "message": "An unexpected error occurred", + "data": None, + "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "timestamp": "2024-01-15T10:30:00Z" + } + } + } + }, + 502: { + "description": "Bad Gateway - External service error", + "content": { + "application/json": { + "example": { + "code": 5002, + "message": "External service unavailable", + "data": None, + "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "timestamp": "2024-01-15T10:30:00Z" + } + } + } + }, + 503: { + "description": "Service Unavailable - Database or cache error", + "content": { + "application/json": { + "example": { + "code": 5001, + "message": "Database error occurred", + "data": None, + "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "timestamp": "2024-01-15T10:30:00Z" + } + } + } + }, +} + + +# Error message templates +class ErrorMessage: + """Predefined error messages for common errors.""" + + # Authentication + LOGIN_FAILED = "Login failed: Invalid credentials" + TOKEN_EXPIRED = "Authentication token has expired" + INSUFFICIENT_PERMISSIONS = "Insufficient permissions to perform this action" + INVALID_TOKEN = "Invalid authentication token" + + # Business Logic + ORDER_CREATION_FAILED = "Order creation failed: {reason}" + PAYMENT_FAILED = "Payment processing failed: {reason}" + INSUFFICIENT_BALANCE = "Insufficient account balance" + OPERATION_NOT_ALLOWED = "Operation not allowed: {reason}" + + # Validation + INVALID_INPUT = "Invalid input: {field}" + MISSING_REQUIRED_FIELD = "Missing required field: {field}" + INVALID_FORMAT = "Invalid format: {field}" + + # Resources + RESOURCE_NOT_FOUND = "{resource} not found" + RESOURCE_ALREADY_EXISTS = "{resource} already exists" + RESOURCE_CONFLICT = "{resource} conflict: {reason}" + + # System + DATABASE_ERROR = "Database error occurred" + EXTERNAL_SERVICE_ERROR = "External service unavailable" + CACHE_ERROR = "Cache service error" + + # Unknown + UNKNOWN_ERROR = "An unexpected error occurred" diff --git a/deployments/docker/Dockerfile b/deployments/docker/Dockerfile new file mode 100644 index 0000000..5831cd2 --- /dev/null +++ b/deployments/docker/Dockerfile @@ -0,0 +1,67 @@ +# Multi-stage Dockerfile for kami_spider +# Optimized for production deployment with UV package manager + +# Stage 1: Builder - Install dependencies +FROM python:3.13-slim AS builder + +# Set environment variables +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + UV_COMPILE_BYTECODE=1 \ + UV_LINK_MODE=copy + +# Install UV +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv + +# Set working directory +WORKDIR /app + +# Copy dependency files +COPY pyproject.toml ./ + +# Install dependencies using UV +RUN uv sync --frozen --no-dev --no-install-project + +# Stage 2: Runtime - Create final image +FROM python:3.13-slim + +# Set environment variables +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PATH="/app/.venv/bin:$PATH" + +# Create non-root user +RUN groupadd -r appuser && useradd -r -g appuser appuser + +# Set working directory +WORKDIR /app + +# Copy virtual environment from builder +COPY --from=builder /app/.venv /app/.venv + +# Copy application code +COPY . . + +# Change ownership to non-root user +RUN chown -R appuser:appuser /app + +# Switch to non-root user +USER appuser + +# Expose port +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" + +# Run application with Gunicorn +CMD ["gunicorn", "main:app", \ + "--workers", "4", \ + "--worker-class", "uvicorn.workers.UvicornWorker", \ + "--bind", "0.0.0.0:8000", \ + "--timeout", "30", \ + "--graceful-timeout", "30", \ + "--access-logfile", "-", \ + "--error-logfile", "-", \ + "--log-level", "info"] diff --git a/deployments/docker/docker-compose.yml b/deployments/docker/docker-compose.yml new file mode 100644 index 0000000..4794067 --- /dev/null +++ b/deployments/docker/docker-compose.yml @@ -0,0 +1,109 @@ +version: '3.8' + +services: + # FastAPI Application + app: + build: + context: ../.. + dockerfile: deployments/docker/Dockerfile + image: kami_spider:latest + container_name: kami_spider_app + ports: + - "8000:8000" + environment: + # Application + APP_NAME: kami_spider + ENVIRONMENT: development + DEBUG: "false" + LOG_LEVEL: INFO + + # Database + DB_HOST: mysql + DB_PORT: 3306 + DB_NAME: kami_spider + DB_USER: kami_user + DB_PASSWORD: kami_pass + DB_POOL_SIZE: 10 + DB_MAX_OVERFLOW: 20 + + # Redis + REDIS_HOST: redis + REDIS_PORT: 6379 + REDIS_DB: 0 + REDIS_PASSWORD: "" + + # OpenTelemetry + OTEL_ENABLED: "true" + OTEL_SERVICE_NAME: kami_spider + OTEL_EXPORTER_ENDPOINT: "38.38.251.113:31547" + OTEL_EXPORTER_INSECURE: "true" + OTEL_SAMPLE_RATE: "1.0" + + # CORS + CORS_ENABLED: "true" + CORS_ALLOW_ORIGINS: '["*"]' + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + restart: unless-stopped + networks: + - kami_network + + # MySQL Database + mysql: + image: mysql:8.0 + container_name: kami_spider_mysql + ports: + - "3306:3306" + environment: + MYSQL_ROOT_PASSWORD: root_pass + MYSQL_DATABASE: kami_spider + MYSQL_USER: kami_user + MYSQL_PASSWORD: kami_pass + volumes: + - mysql_data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + restart: unless-stopped + networks: + - kami_network + + # Redis Cache + redis: + image: redis:7-alpine + container_name: kami_spider_redis + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 5s + restart: unless-stopped + networks: + - kami_network + +networks: + kami_network: + driver: bridge + +volumes: + mysql_data: + driver: local + redis_data: + driver: local diff --git a/deployments/k8s/configmap.yaml b/deployments/k8s/configmap.yaml new file mode 100644 index 0000000..a48ea4a --- /dev/null +++ b/deployments/k8s/configmap.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: kami-spider-config + namespace: default +data: + APP_NAME: "kami_spider" + ENVIRONMENT: "production" + DEBUG: "false" + LOG_LEVEL: "INFO" + DB_HOST: "kami-spider-mysql" + DB_PORT: "3306" + DB_NAME: "kami_spider" + DB_USER: "kami_user" + DB_POOL_SIZE: "10" + DB_MAX_OVERFLOW: "20" + REDIS_HOST: "kami-spider-redis" + REDIS_PORT: "6379" + REDIS_DB: "0" + OTEL_ENABLED: "true" + OTEL_SERVICE_NAME: "kami_spider" + OTEL_EXPORTER_ENDPOINT: "38.38.251.113:31547" + OTEL_EXPORTER_INSECURE: "true" + OTEL_SAMPLE_RATE: "1.0" + CORS_ENABLED: "true" diff --git a/deployments/k8s/deployment.yaml b/deployments/k8s/deployment.yaml new file mode 100644 index 0000000..f2141f1 --- /dev/null +++ b/deployments/k8s/deployment.yaml @@ -0,0 +1,224 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kami-spider-app + namespace: default + labels: + app: kami-spider + component: app +spec: + replicas: 3 + selector: + matchLabels: + app: kami-spider + component: app + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + template: + metadata: + labels: + app: kami-spider + component: app + spec: + containers: + - name: app + image: kami_spider:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8000 + name: http + protocol: TCP + env: + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: kami-spider-secret + key: DB_PASSWORD + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: kami-spider-secret + key: REDIS_PASSWORD + - name: SECRET_KEY + valueFrom: + secretKeyRef: + name: kami-spider-secret + key: SECRET_KEY + envFrom: + - configMapRef: + name: kami-spider-config + resources: + requests: + cpu: 500m + memory: 512Mi + limits: + cpu: 2000m + memory: 2Gi + livenessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: kami-spider-mysql + namespace: default + labels: + app: kami-spider + component: mysql +spec: + serviceName: kami-spider-mysql + replicas: 1 + selector: + matchLabels: + app: kami-spider + component: mysql + template: + metadata: + labels: + app: kami-spider + component: mysql + spec: + containers: + - name: mysql + image: mysql:8.0 + ports: + - containerPort: 3306 + name: mysql + protocol: TCP + env: + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: kami-spider-secret + key: MYSQL_ROOT_PASSWORD + - name: MYSQL_DATABASE + value: "kami_spider" + - name: MYSQL_USER + value: "kami_user" + - name: MYSQL_PASSWORD + valueFrom: + secretKeyRef: + name: kami-spider-secret + key: DB_PASSWORD + volumeMounts: + - name: mysql-data + mountPath: /var/lib/mysql + resources: + requests: + cpu: 1000m + memory: 2Gi + limits: + cpu: 4000m + memory: 8Gi + livenessProbe: + exec: + command: + - mysqladmin + - ping + - -h + - localhost + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 5 + readinessProbe: + exec: + command: + - mysqladmin + - ping + - -h + - localhost + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + volumeClaimTemplates: + - metadata: + name: mysql-data + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 20Gi +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: kami-spider-redis + namespace: default + labels: + app: kami-spider + component: redis +spec: + serviceName: kami-spider-redis + replicas: 1 + selector: + matchLabels: + app: kami-spider + component: redis + template: + metadata: + labels: + app: kami-spider + component: redis + spec: + containers: + - name: redis + image: redis:7-alpine + ports: + - containerPort: 6379 + name: redis + protocol: TCP + volumeMounts: + - name: redis-data + mountPath: /data + resources: + requests: + cpu: 500m + memory: 512Mi + limits: + cpu: 2000m + memory: 4Gi + livenessProbe: + exec: + command: + - redis-cli + - ping + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + exec: + command: + - redis-cli + - ping + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + volumeClaimTemplates: + - metadata: + name: redis-data + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 5Gi diff --git a/deployments/k8s/hpa.yaml b/deployments/k8s/hpa.yaml new file mode 100644 index 0000000..674cc1d --- /dev/null +++ b/deployments/k8s/hpa.yaml @@ -0,0 +1,44 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: kami-spider-hpa + namespace: default + labels: + app: kami-spider +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: kami-spider-app + minReplicas: 3 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 + behavior: + scaleDown: + stabilizationWindowSeconds: 300 + policies: + - type: Percent + value: 50 + periodSeconds: 60 + scaleUp: + stabilizationWindowSeconds: 0 + policies: + - type: Percent + value: 100 + periodSeconds: 30 + - type: Pods + value: 2 + periodSeconds: 30 + selectPolicy: Max diff --git a/deployments/k8s/ingress.yaml b/deployments/k8s/ingress.yaml new file mode 100644 index 0000000..0f422aa --- /dev/null +++ b/deployments/k8s/ingress.yaml @@ -0,0 +1,31 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: kami-spider-ingress + namespace: default + labels: + app: kami-spider + annotations: + # Nginx ingress controller annotations + nginx.ingress.kubernetes.io/rewrite-target: / + nginx.ingress.kubernetes.io/ssl-redirect: "false" + # Uncomment for HTTPS/TLS + # cert-manager.io/cluster-issuer: "letsencrypt-prod" +spec: + ingressClassName: nginx + rules: + - host: kami-spider.example.com # Change to your domain + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: kami-spider-app + port: + number: 8000 + # Uncomment for TLS + # tls: + # - hosts: + # - kami-spider.example.com + # secretName: kami-spider-tls diff --git a/deployments/k8s/secret.yaml b/deployments/k8s/secret.yaml new file mode 100644 index 0000000..95db80a --- /dev/null +++ b/deployments/k8s/secret.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Secret +metadata: + name: kami-spider-secret + namespace: default +type: Opaque +stringData: + DB_PASSWORD: "kami_pass" + REDIS_PASSWORD: "" + SECRET_KEY: "change-me-in-production-use-kubectl-create-secret" + MYSQL_ROOT_PASSWORD: "root_pass" diff --git a/deployments/k8s/service.yaml b/deployments/k8s/service.yaml new file mode 100644 index 0000000..472fc2e --- /dev/null +++ b/deployments/k8s/service.yaml @@ -0,0 +1,58 @@ +apiVersion: v1 +kind: Service +metadata: + name: kami-spider-app + namespace: default + labels: + app: kami-spider + component: app +spec: + type: ClusterIP + selector: + app: kami-spider + component: app + ports: + - port: 8000 + targetPort: 8000 + protocol: TCP + name: http +--- +apiVersion: v1 +kind: Service +metadata: + name: kami-spider-mysql + namespace: default + labels: + app: kami-spider + component: mysql +spec: + type: ClusterIP + clusterIP: None # Headless service for StatefulSet + selector: + app: kami-spider + component: mysql + ports: + - port: 3306 + targetPort: 3306 + protocol: TCP + name: mysql +--- +apiVersion: v1 +kind: Service +metadata: + name: kami-spider-redis + namespace: default + labels: + app: kami-spider + component: redis +spec: + type: ClusterIP + clusterIP: None # Headless service for StatefulSet + selector: + app: kami-spider + component: redis + ports: + - port: 6379 + targetPort: 6379 + protocol: TCP + name: redis diff --git a/deployments/swarm/docker-compose.swarm.yml b/deployments/swarm/docker-compose.swarm.yml new file mode 100644 index 0000000..c432261 --- /dev/null +++ b/deployments/swarm/docker-compose.swarm.yml @@ -0,0 +1,128 @@ +version: '3.8' + +services: + # FastAPI Application (with replicas for scaling) + app: + image: kami_spider:latest + ports: + - "8000:8000" + environment: + APP_NAME: kami_spider + ENVIRONMENT: production + DEBUG: "false" + LOG_LEVEL: INFO + DB_HOST: mysql + DB_PORT: 3306 + DB_NAME: kami_spider + DB_USER: kami_user + DB_PASSWORD: kami_pass + REDIS_HOST: redis + REDIS_PORT: 6379 + OTEL_ENABLED: "true" + OTEL_SERVICE_NAME: kami_spider + OTEL_EXPORTER_ENDPOINT: "38.38.251.113:31547" + deploy: + replicas: 3 + update_config: + parallelism: 1 + delay: 10s + order: start-first + rollback_config: + parallelism: 1 + delay: 5s + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + resources: + limits: + cpus: '2' + memory: 2G + reservations: + cpus: '0.5' + memory: 512M + placement: + max_replicas_per_node: 1 + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + networks: + - kami_network + depends_on: + - mysql + - redis + + # MySQL Database (single instance) + mysql: + image: mysql:8.0 + environment: + MYSQL_ROOT_PASSWORD: root_pass + MYSQL_DATABASE: kami_spider + MYSQL_USER: kami_user + MYSQL_PASSWORD: kami_pass + volumes: + - mysql_data:/var/lib/mysql + deploy: + replicas: 1 + restart_policy: + condition: on-failure + resources: + limits: + cpus: '4' + memory: 8G + reservations: + cpus: '1' + memory: 2G + placement: + constraints: + - node.role == manager + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + networks: + - kami_network + + # Redis Cache (single instance) + redis: + image: redis:7-alpine + volumes: + - redis_data:/data + deploy: + replicas: 1 + restart_policy: + condition: on-failure + resources: + limits: + cpus: '2' + memory: 4G + reservations: + cpus: '0.5' + memory: 512M + placement: + constraints: + - node.role == manager + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 5s + networks: + - kami_network + +networks: + kami_network: + driver: overlay + attachable: true + +volumes: + mysql_data: + driver: local + redis_data: + driver: local diff --git a/main.py b/main.py new file mode 100644 index 0000000..591e0b4 --- /dev/null +++ b/main.py @@ -0,0 +1,184 @@ +""" +Main FastAPI application entry point. +Bootstraps the application with middleware, routers, and lifecycle handlers. +""" + +from contextlib import asynccontextmanager +from fastapi import FastAPI +from fastapi.responses import ORJSONResponse +from fastapi.middleware.cors import CORSMiddleware +from core.config import settings +from core.database import create_db_and_tables, close_database_connection +from core.redis import init_redis, close_redis_connection +from core.responses import ERROR_RESPONSES +from observability.tracing import init_tracing, instrument_app, shutdown_tracing +from observability.logging import setup_logging, get_logger +from middleware.trace_context import TraceContextMiddleware +from middleware.logging import RequestLoggingMiddleware +from middleware.error_handler import register_exception_handlers +from apps.app_a.router import router as app_a_router +from apps.app_b.router import router as app_b_router + +logger = get_logger(__name__) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """ + Application lifespan manager. + + Handles startup and shutdown events. + """ + # Startup + logger.info("Starting application...") + + # Setup logging + setup_logging() + logger.info(f"Logging configured: level={settings.log_level}") + + # Initialize OpenTelemetry + if settings.otel_enabled: + init_tracing() + instrument_app(app) + logger.info(f"OpenTelemetry initialized: endpoint={settings.otel_exporter_endpoint}") + + # Initialize Redis + await init_redis() + logger.info(f"Redis initialized: {settings.redis_host}:{settings.redis_port}") + + # Create database tables (development only) + if settings.is_development: + await create_db_and_tables() + logger.info("Database tables created") + + logger.info(f"Application started: environment={settings.environment}") + + yield + + # Shutdown + logger.info("Shutting down application...") + + # Close Redis connection + await close_redis_connection() + logger.info("Redis connection closed") + + # Close database connection + await close_database_connection() + logger.info("Database connection closed") + + # Shutdown OpenTelemetry + if settings.otel_enabled: + await shutdown_tracing() + logger.info("OpenTelemetry shutdown") + + logger.info("Application shutdown complete") + + +# Create FastAPI application +app = FastAPI( + title=settings.app_name, + description="A stateless, production-ready FastAPI web service platform", + version="0.1.0", + debug=settings.debug, + lifespan=lifespan, + docs_url="/docs", + redoc_url="/redoc", + openapi_url="/openapi.json", + default_response_class=ORJSONResponse, # Use orjson for better performance +) + +# Add CORS middleware +if settings.cors_enabled: + app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_allow_origins, + allow_credentials=settings.cors_allow_credentials, + allow_methods=settings.cors_allow_methods, + allow_headers=settings.cors_allow_headers, + ) + +# Add custom middleware (order matters: last added = first executed) +app.add_middleware(RequestLoggingMiddleware) +app.add_middleware(TraceContextMiddleware) + +# Register exception handlers +register_exception_handlers(app) + +# Include application routers +app.include_router(app_a_router) +app.include_router(app_b_router) + + +# Health check endpoint +@app.get( + "/health", + tags=["Health"], + summary="Health check", + description="Check the health status of the application and its dependencies", + responses={ + 200: {"description": "Service is healthy"}, + 500: ERROR_RESPONSES[500], + 503: ERROR_RESPONSES[503], + } +) +async def health_check(): + """ + Health check endpoint. + + Returns health status of the application and its components. + """ + from core.database import check_database_connection + from core.redis import check_redis_connection + + # Check database + db_healthy = await check_database_connection() + + # Check Redis + redis_healthy = await check_redis_connection() + + # Overall health + healthy = db_healthy and redis_healthy + + return { + "status": "healthy" if healthy else "unhealthy", + "components": { + "api": "healthy", + "database": "healthy" if db_healthy else "unhealthy", + "redis": "healthy" if redis_healthy else "unhealthy", + }, + "environment": settings.environment, + "version": "0.1.0", + } + + +# Root endpoint +@app.get( + "/", + tags=["Root"], + summary="Root endpoint", + description="Get API information", + responses={ + 200: {"description": "API information retrieved successfully"}, + } +) +async def root(): + """Root endpoint with API information.""" + return { + "name": settings.app_name, + "version": "0.1.0", + "environment": settings.environment, + "docs": "/docs", + "health": "/health", + } + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run( + "main:app", + host=settings.host, + port=settings.port, + reload=settings.debug, + log_level=settings.log_level.lower(), + ) diff --git a/middleware/__init__.py b/middleware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/middleware/error_handler.py b/middleware/error_handler.py new file mode 100644 index 0000000..700edcb --- /dev/null +++ b/middleware/error_handler.py @@ -0,0 +1,219 @@ +""" +Global exception handler middleware. +Catches all exceptions and returns standardized error responses. +""" + +import traceback +from typing import Union +from fastapi import Request, status +from fastapi.responses import ORJSONResponse +from fastapi.exceptions import RequestValidationError +from starlette.exceptions import HTTPException as StarletteHTTPException +from pydantic import ValidationError +from core.exceptions import BaseAppException +from core.responses import error, BusinessCode, ErrorMessage +from observability.logging import get_logger + +logger = get_logger(__name__) + + +async def app_exception_handler( + request: Request, + exc: BaseAppException +) -> ORJSONResponse: + """ + Handle application-specific exceptions. + + Args: + request: FastAPI request + exc: Application exception + + Returns: + JSONResponse: Standardized error response + """ + # Get trace ID from request + trace_id = getattr(request.state, "trace_id", "") + + # Log exception + logger.error( + f"Application error: {exc.message}", + extra={ + "trace_id": trace_id, + "error_code": exc.code, + "details": exc.details, + }, + exc_info=True + ) + + # Return error response + error_response = error( + code=exc.code, + message=exc.message, + trace_id=trace_id + ) + + return ORJSONResponse( + status_code=exc.status_code, + content=error_response.model_dump(mode='json') + ) + + +async def validation_exception_handler( + request: Request, + exc: Union[RequestValidationError, ValidationError] +) -> ORJSONResponse: + """ + Handle Pydantic validation errors. + + Args: + request: FastAPI request + exc: Validation error + + Returns: + JSONResponse: Standardized error response + """ + # Get trace ID from request + trace_id = getattr(request.state, "trace_id", "") + + # Extract validation errors + errors = exc.errors() if hasattr(exc, "errors") else [] + + # Format error message + if errors: + first_error = errors[0] + field = ".".join(str(loc) for loc in first_error.get("loc", [])) + message = f"Validation error: {field} - {first_error.get('msg', 'Invalid value')}" + else: + message = "Validation error" + + # Log validation error + logger.warning( + f"Validation error: {message}", + extra={ + "trace_id": trace_id, + "validation_errors": errors, + } + ) + + # Return error response + error_response = error( + code=BusinessCode.INVALID_INPUT, + message=message, + trace_id=trace_id + ) + + return ORJSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content=error_response.model_dump(mode='json') + ) + + +async def http_exception_handler( + request: Request, + exc: StarletteHTTPException +) -> ORJSONResponse: + """ + Handle HTTP exceptions. + + Args: + request: FastAPI request + exc: HTTP exception + + Returns: + JSONResponse: Standardized error response + """ + # Get trace ID from request + trace_id = getattr(request.state, "trace_id", "") + + # Map HTTP status to business code + code_mapping = { + 404: BusinessCode.RESOURCE_NOT_FOUND, + 401: BusinessCode.LOGIN_FAILED, + 403: BusinessCode.INSUFFICIENT_PERMISSIONS, + 409: BusinessCode.RESOURCE_CONFLICT, + } + + business_code = code_mapping.get(exc.status_code, BusinessCode.UNKNOWN_ERROR) + + # Log HTTP error + logger.warning( + f"HTTP error {exc.status_code}: {exc.detail}", + extra={ + "trace_id": trace_id, + "status_code": exc.status_code, + } + ) + + # Return error response + error_response = error( + code=business_code, + message=str(exc.detail), + trace_id=trace_id + ) + + return ORJSONResponse( + status_code=exc.status_code, + content=error_response.model_dump(mode='json') + ) + + +async def generic_exception_handler( + request: Request, + exc: Exception +) -> ORJSONResponse: + """ + Handle all other unhandled exceptions. + + Args: + request: FastAPI request + exc: Any exception + + Returns: + JSONResponse: Standardized error response + """ + # Get trace ID from request + trace_id = getattr(request.state, "trace_id", "") + + # Log full exception with traceback + logger.error( + f"Unhandled exception: {str(exc)}", + extra={ + "trace_id": trace_id, + "exception_type": type(exc).__name__, + "traceback": traceback.format_exc(), + }, + exc_info=True + ) + + # Return generic error response + error_response = error( + code=BusinessCode.UNKNOWN_ERROR, + message=ErrorMessage.UNKNOWN_ERROR, + trace_id=trace_id + ) + + return ORJSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=error_response.model_dump(mode='json') + ) + + +def register_exception_handlers(app) -> None: + """ + Register all exception handlers with FastAPI application. + + Args: + app: FastAPI application instance + """ + # Application exceptions + app.add_exception_handler(BaseAppException, app_exception_handler) + + # Validation errors + app.add_exception_handler(RequestValidationError, validation_exception_handler) + app.add_exception_handler(ValidationError, validation_exception_handler) + + # HTTP exceptions + app.add_exception_handler(StarletteHTTPException, http_exception_handler) + + # Generic exception handler (catch-all) + app.add_exception_handler(Exception, generic_exception_handler) diff --git a/middleware/logging.py b/middleware/logging.py new file mode 100644 index 0000000..b085bee --- /dev/null +++ b/middleware/logging.py @@ -0,0 +1,107 @@ +""" +Request and response logging middleware. +Logs all incoming requests and outgoing responses with trace context. +""" + +import time +from typing import Callable +from fastapi import Request, Response +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.types import ASGIApp +from observability.logging import get_logger + +logger = get_logger(__name__) + + +class RequestLoggingMiddleware(BaseHTTPMiddleware): + """ + Middleware for logging HTTP requests and responses. + + Logs: + - Request method, path, client IP + - Response status code, processing time + - Trace ID for correlation + """ + + def __init__(self, app: ASGIApp): + """ + Initialize middleware. + + Args: + app: ASGI application + """ + super().__init__(app) + + async def dispatch(self, request: Request, call_next: Callable) -> Response: + """ + Process request and log details. + + Args: + request: Incoming request + call_next: Next middleware or route handler + + Returns: + Response: Response from handler + """ + # Record start time + start_time = time.time() + + # Get trace ID from request state + trace_id = getattr(request.state, "trace_id", "") + + # Get client IP + client_ip = request.client.host if request.client else "unknown" + + # Log request + logger.info( + f"Request started: {request.method} {request.url.path}", + extra={ + "method": request.method, + "path": request.url.path, + "query_params": str(request.query_params), + "client_ip": client_ip, + "trace_id": trace_id, + } + ) + + # Process request + try: + response = await call_next(request) + except Exception as e: + # Calculate processing time + process_time = time.time() - start_time + + # Log error + logger.error( + f"Request failed: {request.method} {request.url.path} - {str(e)}", + extra={ + "method": request.method, + "path": request.url.path, + "client_ip": client_ip, + "trace_id": trace_id, + "process_time_ms": round(process_time * 1000, 2), + }, + exc_info=True + ) + raise + + # Calculate processing time + process_time = time.time() - start_time + + # Log response + logger.info( + f"Request completed: {request.method} {request.url.path} - {response.status_code}", + extra={ + "method": request.method, + "path": request.url.path, + "status_code": response.status_code, + "client_ip": client_ip, + "trace_id": trace_id, + "process_time_ms": round(process_time * 1000, 2), + } + ) + + # Add processing time header + response.headers["X-Process-Time"] = f"{process_time:.4f}" + + return response diff --git a/middleware/trace_context.py b/middleware/trace_context.py new file mode 100644 index 0000000..3b42d45 --- /dev/null +++ b/middleware/trace_context.py @@ -0,0 +1,105 @@ +""" +TraceID injection middleware with W3C Trace Context support. +Extracts or generates trace IDs and propagates them through the request lifecycle. +""" + +import uuid +from typing import Callable +from fastapi import Request, Response +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.types import ASGIApp +from observability.logging import set_trace_id + + +class TraceContextMiddleware(BaseHTTPMiddleware): + """ + Middleware for trace context management. + + Extracts trace ID from W3C Trace Context headers or generates a new one. + Adds trace ID to request state and response headers. + """ + + def __init__(self, app: ASGIApp): + """ + Initialize middleware. + + Args: + app: ASGI application + """ + super().__init__(app) + + async def dispatch(self, request: Request, call_next: Callable) -> Response: + """ + Process request and inject trace context. + + Args: + request: Incoming request + call_next: Next middleware or route handler + + Returns: + Response: Response with trace ID header + """ + # Extract trace ID from W3C Trace Context header (traceparent) + # Format: 00-{trace_id}-{parent_id}-{flags} + trace_id = self._extract_trace_id(request) + + # Generate new trace ID if not present + if not trace_id: + trace_id = str(uuid.uuid4()) + + # Store trace ID in request state + request.state.trace_id = trace_id + + # Set trace ID in logging context + set_trace_id(trace_id) + + # Process request + response = await call_next(request) + + # Add trace ID to response headers + response.headers["X-Trace-ID"] = trace_id + + return response + + def _extract_trace_id(self, request: Request) -> str: + """ + Extract trace ID from request headers. + + Supports: + - W3C Trace Context (traceparent header) + - X-Trace-ID custom header + + Args: + request: Incoming request + + Returns: + str: Trace ID or empty string if not found + """ + # Try X-Trace-ID header first (custom header) + trace_id = request.headers.get("X-Trace-ID", "") + if trace_id: + return trace_id + + # Try W3C Trace Context traceparent header + # Format: 00-{trace_id}-{parent_id}-{flags} + traceparent = request.headers.get("traceparent", "") + if traceparent: + parts = traceparent.split("-") + if len(parts) >= 2: + # Extract trace_id (second part) + return parts[1] + + return "" + + +def get_trace_id_from_request(request: Request) -> str: + """ + Get trace ID from request state. + + Args: + request: FastAPI request + + Returns: + str: Trace ID or empty string + """ + return getattr(request.state, "trace_id", "") diff --git a/observability/__init__.py b/observability/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/observability/logging.py b/observability/logging.py new file mode 100644 index 0000000..3d2cdc4 --- /dev/null +++ b/observability/logging.py @@ -0,0 +1,187 @@ +""" +Structured logging with trace context propagation. +Integrates with OpenTelemetry for log correlation. +""" + +import logging +import json +from typing import Any, MutableMapping +from datetime import datetime +from contextvars import ContextVar +from core.config import settings + + +# Context variable for trace ID +trace_id_var: ContextVar[str] = ContextVar("trace_id", default="") + + +class TraceContextFormatter(logging.Formatter): + """ + Custom formatter that adds trace context to log records. + + Formats logs as JSON in production, human-readable in development. + """ + + def __init__(self, use_json: bool = True): + """ + Initialize formatter. + + Args: + use_json: Whether to format as JSON (True) or human-readable (False) + """ + super().__init__() + self.use_json = use_json + + def format(self, record: logging.LogRecord) -> str: + """ + Format log record with trace context. + + Args: + record: Log record + + Returns: + str: Formatted log message + """ + # Get trace ID from context + trace_id = trace_id_var.get() + + if self.use_json: + # JSON format for production + log_data = { + "timestamp": datetime.utcnow().isoformat() + "Z", + "level": record.levelname, + "logger": record.name, + "message": record.getMessage(), + "trace_id": trace_id, + "module": record.module, + "function": record.funcName, + "line": record.lineno, + } + + # Add exception info if present + if record.exc_info: + log_data["exception"] = self.formatException(record.exc_info) + + # Add extra fields + if hasattr(record, "extra"): + log_data["extra"] = getattr(record, "extra") + + return json.dumps(log_data) + else: + # Human-readable format for development + timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S") + trace_part = f" [trace_id={trace_id}]" if trace_id else "" + message = f"{timestamp} - {record.levelname:8s} - {record.name:30s}{trace_part} - {record.getMessage()}" + + if record.exc_info: + message += "\n" + self.formatException(record.exc_info) + + return message + + +def setup_logging() -> None: + """ + Configure application logging. + + This should be called at application startup. + """ + # Determine if we should use JSON format + use_json = settings.is_production + + # Create formatter + formatter = TraceContextFormatter(use_json=use_json) + + # Configure root logger + root_logger = logging.getLogger() + root_logger.setLevel(settings.log_level) + + # Remove existing handlers + root_logger.handlers.clear() + + # Create console handler + console_handler = logging.StreamHandler() + console_handler.setLevel(settings.log_level) + console_handler.setFormatter(formatter) + + # Add handler to root logger + root_logger.addHandler(console_handler) + + # Set levels for third-party loggers + logging.getLogger("uvicorn").setLevel(logging.INFO) + logging.getLogger("uvicorn.access").setLevel(logging.INFO) + logging.getLogger("uvicorn.error").setLevel(logging.INFO) + logging.getLogger("fastapi").setLevel(logging.INFO) + + # Reduce noise from OpenTelemetry + logging.getLogger("opentelemetry").setLevel(logging.WARNING) + + +def set_trace_id(trace_id: str) -> None: + """ + Set trace ID in context for current request. + + Args: + trace_id: Trace ID to set + """ + trace_id_var.set(trace_id) + + +def get_trace_id() -> str: + """ + Get trace ID from context. + + Returns: + str: Current trace ID or empty string + """ + return trace_id_var.get() + + +def get_logger(name: str) -> logging.Logger: + """ + Get logger instance. + + Args: + name: Logger name (usually __name__) + + Returns: + logging.Logger: Logger instance + """ + return logging.getLogger(name) + + +class LoggerAdapter(logging.LoggerAdapter): + """ + Logger adapter that automatically includes trace context. + """ + + def process(self, msg: str, kwargs: MutableMapping[str, Any]) -> tuple[str, MutableMapping[str, Any]]: + """ + Process log message to add trace context. + + Args: + msg: Log message + kwargs: Keyword arguments + + Returns: + tuple: Processed message and kwargs + """ + trace_id = trace_id_var.get() + if trace_id: + extra = kwargs.get("extra", {}) + extra["trace_id"] = trace_id + kwargs["extra"] = extra + return msg, kwargs + + +def get_logger_with_trace(name: str) -> LoggerAdapter: + """ + Get logger adapter with automatic trace context. + + Args: + name: Logger name (usually __name__) + + Returns: + LoggerAdapter: Logger adapter instance + """ + logger = logging.getLogger(name) + return LoggerAdapter(logger, {}) diff --git a/observability/tracing.py b/observability/tracing.py new file mode 100644 index 0000000..4ea0999 --- /dev/null +++ b/observability/tracing.py @@ -0,0 +1,163 @@ +""" +OpenTelemetry tracing configuration with gRPC exporter. +Provides distributed tracing for the application. +""" + +from typing import Optional +from opentelemetry import trace +from opentelemetry.sdk.trace import TracerProvider, SpanProcessor +from opentelemetry.sdk.trace.export import BatchSpanProcessor +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter +from opentelemetry.sdk.resources import Resource, SERVICE_NAME +from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor +from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor +from opentelemetry.instrumentation.redis import RedisInstrumentor +from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor +from opentelemetry.sdk.trace.sampling import TraceIdRatioBased +from core.config import settings + + +tracer_provider: Optional[TracerProvider] = None +span_processor: Optional[SpanProcessor] = None + + +def init_tracing() -> trace.Tracer: + """ + Initialize OpenTelemetry tracing with gRPC exporter. + + This should be called at application startup. + + Returns: + trace.Tracer: Configured tracer instance + """ + global tracer_provider, span_processor + + if not settings.otel_enabled: + # Return no-op tracer if OpenTelemetry is disabled + return trace.get_tracer(__name__) + + # Create resource with service name + resource = Resource(attributes={ + SERVICE_NAME: settings.otel_service_name + }) + + # Create sampler based on sample rate + sampler = TraceIdRatioBased(settings.otel_sample_rate) + + # Create tracer provider + tracer_provider = TracerProvider( + resource=resource, + sampler=sampler + ) + + # Create OTLP gRPC exporter + otlp_exporter = OTLPSpanExporter( + endpoint=settings.otel_exporter_endpoint, + insecure=settings.otel_exporter_insecure + ) + + # Create batch span processor + span_processor = BatchSpanProcessor(otlp_exporter) + tracer_provider.add_span_processor(span_processor) + + # Set global tracer provider + trace.set_tracer_provider(tracer_provider) + + # Return tracer + return trace.get_tracer(__name__) + + +def instrument_app(app) -> None: + """ + Instrument FastAPI application with OpenTelemetry. + + Args: + app: FastAPI application instance + """ + if not settings.otel_enabled: + return + + # Instrument FastAPI + FastAPIInstrumentor.instrument_app(app) + + # Instrument HTTP client + HTTPXClientInstrumentor().instrument() + + # Redis instrumentation is done when Redis client is created + # SQLAlchemy instrumentation is done when engine is created + + +def instrument_sqlalchemy(engine) -> None: + """ + Instrument SQLAlchemy engine with OpenTelemetry. + + Args: + engine: SQLAlchemy engine instance + """ + if not settings.otel_enabled: + return + + SQLAlchemyInstrumentor().instrument( + engine=engine.sync_engine + ) + + +def instrument_redis() -> None: + """ + Instrument Redis client with OpenTelemetry. + """ + if not settings.otel_enabled: + return + + RedisInstrumentor().instrument() + + +async def shutdown_tracing() -> None: + """ + Shutdown tracing and flush remaining spans. + + This should be called at application shutdown. + """ + global tracer_provider, span_processor + + if span_processor: + span_processor.shutdown() + + if tracer_provider: + tracer_provider.shutdown() + + +def get_tracer(name: str = __name__) -> trace.Tracer: + """ + Get tracer instance. + + Args: + name: Tracer name (usually __name__) + + Returns: + trace.Tracer: Tracer instance + """ + return trace.get_tracer(name) + + +def get_current_span() -> trace.Span: + """ + Get current active span. + + Returns: + trace.Span: Current span + """ + return trace.get_current_span() + + +def get_trace_id() -> str: + """ + Get current trace ID as hex string. + + Returns: + str: Trace ID or empty string if no active span + """ + span = get_current_span() + if span and span.get_span_context().is_valid: + return format(span.get_span_context().trace_id, '032x') + return "" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f08651d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,120 @@ +[project] +name = "kami_spider" +version = "0.1.0" +description = "A stateless, production-ready FastAPI-based web service platform hosting multiple independent web applications" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "fastapi>=0.120.0", + "uvicorn[standard]>=0.38.0", + "gunicorn>=23.0.0", + "sqlmodel>=0.0.24", + "pydantic>=2.12.3", + "pydantic-settings>=2.11.0", + "redis>=5.2.1", + "pymysql>=1.1.1", + "cryptography>=46.0.1", + "aiomysql>=0.2.0", + "alembic>=1.14.0", + "python-dotenv>=1.0.1", + "opentelemetry-api>=1.38.0", + "opentelemetry-sdk>=1.38.0", + "opentelemetry-instrumentation-fastapi>=0.59b0", + "opentelemetry-instrumentation-sqlalchemy>=0.59b0", + "opentelemetry-instrumentation-redis>=0.59b0", + "opentelemetry-instrumentation-httpx>=0.59b0", + "opentelemetry-exporter-otlp-proto-grpc>=1.38.0", + "httpx>=0.28.1", + "python-multipart>=0.0.20", + "email-validator>=2.3.0", + "greenlet>=3.2.4", + "orjson>=3.11.4", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.3.4", + "pytest-asyncio>=0.24.0", + "pytest-cov>=6.0.0", + "pytest-mock>=3.14.0", + "httpx>=0.28.1", + "ruff>=0.8.4", + "mypy>=1.14.1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["apps", "core", "middleware", "observability"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +asyncio_mode = "auto" +addopts = [ + "--strict-markers", + "--strict-config", + "--cov=.", + "--cov-report=term-missing", + "--cov-report=html", +] + +[tool.coverage.run] +source = ["."] +omit = [ + "tests/*", + "*/site-packages/*", + "*/__pycache__/*", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", + "class .*\\bProtocol\\):", + "@(abc\\.)?abstractmethod", +] + +[tool.ruff] +line-length = 120 +target-version = "py313" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade +] +ignore = [ + "E501", # line too long (handled by formatter) + "B008", # do not perform function calls in argument defaults + "W191", # indentation contains tabs +] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] + +[tool.mypy] +python_version = "3.13" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +check_untyped_defs = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_no_return = true +strict_equality = true diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..3eaeb83 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1407 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "aiomysql" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pymysql" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/e0/302aeffe8d90853556f47f3106b89c16cc2ec2a4d269bdfd82e3f4ae12cc/aiomysql-0.3.2.tar.gz", hash = "sha256:72d15ef5cfc34c03468eb41e1b90adb9fd9347b0b589114bd23ead569a02ac1a", size = 108311, upload-time = "2025-10-22T00:15:21.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/af/aae0153c3e28712adaf462328f6c7a3c196a1c1c27b491de4377dd3e6b52/aiomysql-0.3.2-py3-none-any.whl", hash = "sha256:c82c5ba04137d7afd5c693a258bea8ead2aad77101668044143a991e04632eb2", size = 71834, upload-time = "2025-10-22T00:15:15.905Z" }, +] + +[[package]] +name = "alembic" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6b/45/6f4555f2039f364c3ce31399529dcf48dd60726ff3715ad67f547d87dfd2/alembic-1.17.0.tar.gz", hash = "sha256:4652a0b3e19616b57d652b82bfa5e38bf5dbea0813eed971612671cb9e90c0fe", size = 1975526, upload-time = "2025-10-11T18:40:13.585Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/1f/38e29b06bfed7818ebba1f84904afdc8153ef7b6c7e0d8f3bc6643f5989c/alembic-1.17.0-py3-none-any.whl", hash = "sha256:80523bc437d41b35c5db7e525ad9d908f79de65c27d6a5a5eab6df348a352d99", size = 247449, upload-time = "2025-10-11T18:40:16.288Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/a6/dc46877b911e40c00d395771ea710d5e77b6de7bacd5fdcd78d70cc5a48f/annotated_doc-0.0.3.tar.gz", hash = "sha256:e18370014c70187422c33e945053ff4c286f453a984eba84d0dbfa0c935adeda", size = 5535, upload-time = "2025-10-24T14:57:10.718Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/b7/cf592cb5de5cb3bade3357f8d2cf42bf103bbe39f459824b4939fd212911/annotated_doc-0.0.3-py3-none-any.whl", hash = "sha256:348ec6664a76f1fd3be81f43dffbee4c7e8ce931ba71ec67cc7f4ade7fbbb580", size = 5488, upload-time = "2025-10-24T14:57:09.462Z" }, +] + +[[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.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, +] + +[[package]] +name = "asgiref" +version = "3.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/08/4dfec9b90758a59acc6be32ac82e98d1fbfc321cb5cfa410436dbacf821c/asgiref-3.10.0.tar.gz", hash = "sha256:d89f2d8cd8b56dada7d52fa7dc8075baa08fb836560710d38c292a7a3f78c04e", size = 37483, upload-time = "2025-10-05T09:15:06.557Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/9c/fc2331f538fbf7eedba64b2052e99ccf9ba9d6888e2f41441ee28847004b/asgiref-3.10.0-py3-none-any.whl", hash = "sha256:aef8a81283a34d0ab31630c9b7dfe70c812c95eba78171367ca8745e88124734", size = 24050, upload-time = "2025-10-05T09:15:05.11Z" }, +] + +[[package]] +name = "certifi" +version = "2025.10.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, +] + +[[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 = "coverage" +version = "7.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/38/ee22495420457259d2f3390309505ea98f98a5eed40901cf62196abad006/coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050", size = 811905, upload-time = "2025-10-15T15:15:08.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/7f/85e4dfe65e400645464b25c036a26ac226cf3a69d4a50c3934c532491cdd/coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1", size = 216129, upload-time = "2025-10-15T15:13:25.371Z" }, + { url = "https://files.pythonhosted.org/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be", size = 216380, upload-time = "2025-10-15T15:13:26.976Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f5/3da9cc9596708273385189289c0e4d8197d37a386bdf17619013554b3447/coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d", size = 247375, upload-time = "2025-10-15T15:13:28.923Z" }, + { url = "https://files.pythonhosted.org/packages/65/6c/f7f59c342359a235559d2bc76b0c73cfc4bac7d61bb0df210965cb1ecffd/coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82", size = 249978, upload-time = "2025-10-15T15:13:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/e7/8c/042dede2e23525e863bf1ccd2b92689692a148d8b5fd37c37899ba882645/coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52", size = 251253, upload-time = "2025-10-15T15:13:32.174Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/3c58df67bfa809a7bddd786356d9c5283e45d693edb5f3f55d0986dd905a/coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b", size = 247591, upload-time = "2025-10-15T15:13:34.147Z" }, + { url = "https://files.pythonhosted.org/packages/26/5b/c7f32efd862ee0477a18c41e4761305de6ddd2d49cdeda0c1116227570fd/coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4", size = 249411, upload-time = "2025-10-15T15:13:38.425Z" }, + { url = "https://files.pythonhosted.org/packages/76/b5/78cb4f1e86c1611431c990423ec0768122905b03837e1b4c6a6f388a858b/coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd", size = 247303, upload-time = "2025-10-15T15:13:40.464Z" }, + { url = "https://files.pythonhosted.org/packages/87/c9/23c753a8641a330f45f221286e707c427e46d0ffd1719b080cedc984ec40/coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc", size = 247157, upload-time = "2025-10-15T15:13:42.087Z" }, + { url = "https://files.pythonhosted.org/packages/c5/42/6e0cc71dc8a464486e944a4fa0d85bdec031cc2969e98ed41532a98336b9/coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48", size = 248921, upload-time = "2025-10-15T15:13:43.715Z" }, + { url = "https://files.pythonhosted.org/packages/e8/1c/743c2ef665e6858cccb0f84377dfe3a4c25add51e8c7ef19249be92465b6/coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040", size = 218526, upload-time = "2025-10-15T15:13:45.336Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d5/226daadfd1bf8ddbccefbd3aa3547d7b960fb48e1bdac124e2dd13a2b71a/coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05", size = 219317, upload-time = "2025-10-15T15:13:47.401Z" }, + { url = "https://files.pythonhosted.org/packages/97/54/47db81dcbe571a48a298f206183ba8a7ba79200a37cd0d9f4788fcd2af4a/coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a", size = 217948, upload-time = "2025-10-15T15:13:49.096Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8b/cb68425420154e7e2a82fd779a8cc01549b6fa83c2ad3679cd6c088ebd07/coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b", size = 216837, upload-time = "2025-10-15T15:13:51.09Z" }, + { url = "https://files.pythonhosted.org/packages/33/55/9d61b5765a025685e14659c8d07037247de6383c0385757544ffe4606475/coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37", size = 217061, upload-time = "2025-10-15T15:13:52.747Z" }, + { url = "https://files.pythonhosted.org/packages/52/85/292459c9186d70dcec6538f06ea251bc968046922497377bf4a1dc9a71de/coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de", size = 258398, upload-time = "2025-10-15T15:13:54.45Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e2/46edd73fb8bf51446c41148d81944c54ed224854812b6ca549be25113ee0/coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f", size = 260574, upload-time = "2025-10-15T15:13:56.145Z" }, + { url = "https://files.pythonhosted.org/packages/07/5e/1df469a19007ff82e2ca8fe509822820a31e251f80ee7344c34f6cd2ec43/coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c", size = 262797, upload-time = "2025-10-15T15:13:58.635Z" }, + { url = "https://files.pythonhosted.org/packages/f9/50/de216b31a1434b94d9b34a964c09943c6be45069ec704bfc379d8d89a649/coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa", size = 257361, upload-time = "2025-10-15T15:14:00.409Z" }, + { url = "https://files.pythonhosted.org/packages/82/1e/3f9f8344a48111e152e0fd495b6fff13cc743e771a6050abf1627a7ba918/coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740", size = 260349, upload-time = "2025-10-15T15:14:02.188Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/3f52741f9e7d82124272f3070bbe316006a7de1bad1093f88d59bfc6c548/coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef", size = 258114, upload-time = "2025-10-15T15:14:03.907Z" }, + { url = "https://files.pythonhosted.org/packages/0b/8b/918f0e15f0365d50d3986bbd3338ca01178717ac5678301f3f547b6619e6/coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0", size = 256723, upload-time = "2025-10-15T15:14:06.324Z" }, + { url = "https://files.pythonhosted.org/packages/44/9e/7776829f82d3cf630878a7965a7d70cc6ca94f22c7d20ec4944f7148cb46/coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca", size = 259238, upload-time = "2025-10-15T15:14:08.002Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b8/49cf253e1e7a3bedb85199b201862dd7ca4859f75b6cf25ffa7298aa0760/coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2", size = 219180, upload-time = "2025-10-15T15:14:09.786Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e1/1a541703826be7ae2125a0fb7f821af5729d56bb71e946e7b933cc7a89a4/coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268", size = 220241, upload-time = "2025-10-15T15:14:11.471Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/5ee0e0a08621140fd418ec4020f595b4d52d7eb429ae6a0c6542b4ba6f14/coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836", size = 218510, upload-time = "2025-10-15T15:14:13.46Z" }, + { url = "https://files.pythonhosted.org/packages/f4/06/e923830c1985ce808e40a3fa3eb46c13350b3224b7da59757d37b6ce12b8/coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497", size = 216110, upload-time = "2025-10-15T15:14:15.157Z" }, + { url = "https://files.pythonhosted.org/packages/42/82/cdeed03bfead45203fb651ed756dfb5266028f5f939e7f06efac4041dad5/coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e", size = 216395, upload-time = "2025-10-15T15:14:16.863Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ba/e1c80caffc3199aa699813f73ff097bc2df7b31642bdbc7493600a8f1de5/coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1", size = 247433, upload-time = "2025-10-15T15:14:18.589Z" }, + { url = "https://files.pythonhosted.org/packages/80/c0/5b259b029694ce0a5bbc1548834c7ba3db41d3efd3474489d7efce4ceb18/coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca", size = 249970, upload-time = "2025-10-15T15:14:20.307Z" }, + { url = "https://files.pythonhosted.org/packages/8c/86/171b2b5e1aac7e2fd9b43f7158b987dbeb95f06d1fbecad54ad8163ae3e8/coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd", size = 251324, upload-time = "2025-10-15T15:14:22.419Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/7e10414d343385b92024af3932a27a1caf75c6e27ee88ba211221ff1a145/coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43", size = 247445, upload-time = "2025-10-15T15:14:24.205Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3b/e4f966b21f5be8c4bf86ad75ae94efa0de4c99c7bbb8114476323102e345/coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777", size = 249324, upload-time = "2025-10-15T15:14:26.234Z" }, + { url = "https://files.pythonhosted.org/packages/00/a2/8479325576dfcd909244d0df215f077f47437ab852ab778cfa2f8bf4d954/coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2", size = 247261, upload-time = "2025-10-15T15:14:28.42Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d8/3a9e2db19d94d65771d0f2e21a9ea587d11b831332a73622f901157cc24b/coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d", size = 247092, upload-time = "2025-10-15T15:14:30.784Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b1/bbca3c472544f9e2ad2d5116b2379732957048be4b93a9c543fcd0207e5f/coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4", size = 248755, upload-time = "2025-10-15T15:14:32.585Z" }, + { url = "https://files.pythonhosted.org/packages/89/49/638d5a45a6a0f00af53d6b637c87007eb2297042186334e9923a61aa8854/coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721", size = 218793, upload-time = "2025-10-15T15:14:34.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/cc/b675a51f2d068adb3cdf3799212c662239b0ca27f4691d1fff81b92ea850/coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad", size = 219587, upload-time = "2025-10-15T15:14:37.047Z" }, + { url = "https://files.pythonhosted.org/packages/93/98/5ac886876026de04f00820e5094fe22166b98dcb8b426bf6827aaf67048c/coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479", size = 218168, upload-time = "2025-10-15T15:14:38.861Z" }, + { url = "https://files.pythonhosted.org/packages/14/d1/b4145d35b3e3ecf4d917e97fc8895bcf027d854879ba401d9ff0f533f997/coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f", size = 216850, upload-time = "2025-10-15T15:14:40.651Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d1/7f645fc2eccd318369a8a9948acc447bb7c1ade2911e31d3c5620544c22b/coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e", size = 217071, upload-time = "2025-10-15T15:14:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/64d124649db2737ceced1dfcbdcb79898d5868d311730f622f8ecae84250/coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44", size = 258570, upload-time = "2025-10-15T15:14:44.542Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3f/6f5922f80dc6f2d8b2c6f974835c43f53eb4257a7797727e6ca5b7b2ec1f/coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3", size = 260738, upload-time = "2025-10-15T15:14:46.436Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5f/9e883523c4647c860b3812b417a2017e361eca5b635ee658387dc11b13c1/coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b", size = 262994, upload-time = "2025-10-15T15:14:48.3Z" }, + { url = "https://files.pythonhosted.org/packages/07/bb/43b5a8e94c09c8bf51743ffc65c4c841a4ca5d3ed191d0a6919c379a1b83/coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d", size = 257282, upload-time = "2025-10-15T15:14:50.236Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e5/0ead8af411411330b928733e1d201384b39251a5f043c1612970310e8283/coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2", size = 260430, upload-time = "2025-10-15T15:14:52.413Z" }, + { url = "https://files.pythonhosted.org/packages/ae/66/03dd8bb0ba5b971620dcaac145461950f6d8204953e535d2b20c6b65d729/coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e", size = 258190, upload-time = "2025-10-15T15:14:54.268Z" }, + { url = "https://files.pythonhosted.org/packages/45/ae/28a9cce40bf3174426cb2f7e71ee172d98e7f6446dff936a7ccecee34b14/coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996", size = 256658, upload-time = "2025-10-15T15:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/5c/7c/3a44234a8599513684bfc8684878fd7b126c2760f79712bb78c56f19efc4/coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11", size = 259342, upload-time = "2025-10-15T15:14:58.538Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e6/0108519cba871af0351725ebdb8660fd7a0fe2ba3850d56d32490c7d9b4b/coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73", size = 219568, upload-time = "2025-10-15T15:15:00.382Z" }, + { url = "https://files.pythonhosted.org/packages/c9/76/44ba876e0942b4e62fdde23ccb029ddb16d19ba1bef081edd00857ba0b16/coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547", size = 220687, upload-time = "2025-10-15T15:15:02.322Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0c/0df55ecb20d0d0ed5c322e10a441775e1a3a5d78c60f0c4e1abfe6fcf949/coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3", size = 218711, upload-time = "2025-10-15T15:15:04.575Z" }, + { url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761, upload-time = "2025-10-15T15:15:06.439Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "fastapi" +version = "0.120.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/0e/7f29e8f7219e4526747db182e1afb5a4b6abc3201768fb38d81fa2536241/fastapi-0.120.0.tar.gz", hash = "sha256:6ce2c1cfb7000ac14ffd8ddb2bc12e62d023a36c20ec3710d09d8e36fab177a0", size = 337603, upload-time = "2025-10-23T20:56:34.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/60/7a639ceaba54aec4e1d5676498c568abc654b95762d456095b6cb529b1ca/fastapi-0.120.0-py3-none-any.whl", hash = "sha256:84009182e530c47648da2f07eb380b44b69889a4acfd9e9035ee4605c5cfc469", size = 108243, upload-time = "2025-10-23T20:56:33.281Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.71.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/43/b25abe02db2911397819003029bef768f68a974f2ece483e6084d1a5f754/googleapis_common_protos-1.71.0.tar.gz", hash = "sha256:1aec01e574e29da63c80ba9f7bbf1ccfaacf1da877f23609fe236ca7c72a2e2e", size = 146454, upload-time = "2025-10-20T14:58:08.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/e8/eba9fece11d57a71e3e22ea672742c8f3cf23b35730c9e96db768b295216/googleapis_common_protos-1.71.0-py3-none-any.whl", hash = "sha256:59034a1d849dc4d18971997a72ac56246570afdd17f9369a0ff68218d50ab78c", size = 294576, upload-time = "2025-10-20T14:56:21.295Z" }, +] + +[[package]] +name = "greenlet" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, + { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, + { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, + { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, + { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, + { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, + { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, + { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, +] + +[[package]] +name = "grpcio" +version = "1.76.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" }, + { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" }, + { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" }, + { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" }, + { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" }, + { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" }, + { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" }, + { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417, upload-time = "2025-10-21T16:22:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219, upload-time = "2025-10-21T16:22:17.954Z" }, + { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826, upload-time = "2025-10-21T16:22:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550, upload-time = "2025-10-21T16:22:23.637Z" }, + { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564, upload-time = "2025-10-21T16:22:26.016Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236, upload-time = "2025-10-21T16:22:28.362Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795, upload-time = "2025-10-21T16:22:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214, upload-time = "2025-10-21T16:22:33.831Z" }, + { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961, upload-time = "2025-10-21T16:22:36.468Z" }, + { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462, upload-time = "2025-10-21T16:22:39.772Z" }, +] + +[[package]] +name = "gunicorn" +version = "23.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[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 = "kami-spider" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "aiomysql" }, + { name = "alembic" }, + { name = "cryptography" }, + { name = "email-validator" }, + { name = "fastapi" }, + { name = "greenlet" }, + { name = "gunicorn" }, + { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-instrumentation-fastapi" }, + { name = "opentelemetry-instrumentation-httpx" }, + { name = "opentelemetry-instrumentation-redis" }, + { name = "opentelemetry-instrumentation-sqlalchemy" }, + { name = "opentelemetry-sdk" }, + { name = "orjson" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pymysql" }, + { name = "python-dotenv" }, + { name = "python-multipart" }, + { name = "redis" }, + { name = "sqlmodel" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[package.optional-dependencies] +dev = [ + { name = "httpx" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiomysql", specifier = ">=0.2.0" }, + { name = "alembic", specifier = ">=1.14.0" }, + { name = "cryptography", specifier = ">=46.0.1" }, + { name = "email-validator", specifier = ">=2.3.0" }, + { name = "fastapi", specifier = ">=0.120.0" }, + { name = "greenlet", specifier = ">=3.2.4" }, + { name = "gunicorn", specifier = ">=23.0.0" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.28.1" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.14.1" }, + { name = "opentelemetry-api", specifier = ">=1.38.0" }, + { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = ">=1.38.0" }, + { name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.59b0" }, + { name = "opentelemetry-instrumentation-httpx", specifier = ">=0.59b0" }, + { name = "opentelemetry-instrumentation-redis", specifier = ">=0.59b0" }, + { name = "opentelemetry-instrumentation-sqlalchemy", specifier = ">=0.59b0" }, + { name = "opentelemetry-sdk", specifier = ">=1.38.0" }, + { name = "orjson", specifier = ">=3.11.4" }, + { name = "pydantic", specifier = ">=2.12.3" }, + { name = "pydantic-settings", specifier = ">=2.11.0" }, + { name = "pymysql", specifier = ">=1.1.1" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.4" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.0.0" }, + { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.14.0" }, + { name = "python-dotenv", specifier = ">=1.0.1" }, + { name = "python-multipart", specifier = ">=0.0.20" }, + { name = "redis", specifier = ">=5.2.1" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8.4" }, + { name = "sqlmodel", specifier = ">=0.0.24" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.38.0" }, +] +provides-extras = ["dev"] + +[[package]] +name = "mako" +version = "1.3.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mypy" +version = "1.18.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, + { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, + { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, + { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, + { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, + { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, + { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" }, + { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/d8/0f354c375628e048bd0570645b310797299754730079853095bf000fba69/opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12", size = 65242, upload-time = "2025-10-16T08:35:50.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/a2/d86e01c28300bd41bab8f18afd613676e2bd63515417b77636fc1add426f/opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582", size = 65947, upload-time = "2025-10-16T08:35:30.23Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/83/dd4660f2956ff88ed071e9e0e36e830df14b8c5dc06722dbde1841accbe8/opentelemetry_exporter_otlp_proto_common-1.38.0.tar.gz", hash = "sha256:e333278afab4695aa8114eeb7bf4e44e65c6607d54968271a249c180b2cb605c", size = 20431, upload-time = "2025-10-16T08:35:53.285Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/9e/55a41c9601191e8cd8eb626b54ee6827b9c9d4a46d736f32abc80d8039fc/opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a", size = 18359, upload-time = "2025-10-16T08:35:34.099Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/c0/43222f5b97dc10812bc4f0abc5dc7cd0a2525a91b5151d26c9e2e958f52e/opentelemetry_exporter_otlp_proto_grpc-1.38.0.tar.gz", hash = "sha256:2473935e9eac71f401de6101d37d6f3f0f1831db92b953c7dcc912536158ebd6", size = 24676, upload-time = "2025-10-16T08:35:53.83Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/f0/bd831afbdba74ca2ce3982142a2fad707f8c487e8a3b6fef01f1d5945d1b/opentelemetry_exporter_otlp_proto_grpc-1.38.0-py3-none-any.whl", hash = "sha256:7c49fd9b4bd0dbe9ba13d91f764c2d20b0025649a6e4ac35792fb8d84d764bc7", size = 19695, upload-time = "2025-10-16T08:35:35.053Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/ed/9c65cd209407fd807fa05be03ee30f159bdac8d59e7ea16a8fe5a1601222/opentelemetry_instrumentation-0.59b0.tar.gz", hash = "sha256:6010f0faaacdaf7c4dff8aac84e226d23437b331dcda7e70367f6d73a7db1adc", size = 31544, upload-time = "2025-10-16T08:39:31.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/f5/7a40ff3f62bfe715dad2f633d7f1174ba1a7dd74254c15b2558b3401262a/opentelemetry_instrumentation-0.59b0-py3-none-any.whl", hash = "sha256:44082cc8fe56b0186e87ee8f7c17c327c4c2ce93bdbe86496e600985d74368ee", size = 33020, upload-time = "2025-10-16T08:38:31.463Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/a4/cfbb6fc1ec0aa9bf5a93f548e6a11ab3ac1956272f17e0d399aa2c1f85bc/opentelemetry_instrumentation_asgi-0.59b0.tar.gz", hash = "sha256:2509d6fe9fd829399ce3536e3a00426c7e3aa359fc1ed9ceee1628b56da40e7a", size = 25116, upload-time = "2025-10-16T08:39:36.092Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/88/fe02d809963b182aafbf5588685d7a05af8861379b0ec203d48e360d4502/opentelemetry_instrumentation_asgi-0.59b0-py3-none-any.whl", hash = "sha256:ba9703e09d2c33c52fa798171f344c8123488fcd45017887981df088452d3c53", size = 16797, upload-time = "2025-10-16T08:38:37.214Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-asgi" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/a7/7a6ce5009584ce97dbfd5ce77d4f9d9570147507363349d2cb705c402bcf/opentelemetry_instrumentation_fastapi-0.59b0.tar.gz", hash = "sha256:e8fe620cfcca96a7d634003df1bc36a42369dedcdd6893e13fb5903aeeb89b2b", size = 24967, upload-time = "2025-10-16T08:39:46.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/27/5914c8bf140ffc70eff153077e225997c7b054f0bf28e11b9ab91b63b18f/opentelemetry_instrumentation_fastapi-0.59b0-py3-none-any.whl", hash = "sha256:0d8d00ff7d25cca40a4b2356d1d40a8f001e0668f60c102f5aa6bb721d660c4f", size = 13492, upload-time = "2025-10-16T08:38:52.312Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-httpx" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/6b/1bdf36b68cace9b4eae3cbbade4150c71c90aa392b127dda5bb5c2a49307/opentelemetry_instrumentation_httpx-0.59b0.tar.gz", hash = "sha256:a1cb9b89d9f05a82701cc9ab9cfa3db54fd76932489449778b350bc1b9f0e872", size = 19886, upload-time = "2025-10-16T08:39:48.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/16/c1e0745d20af392ec9060693531d7f01239deb2d81e460d0c379719691b8/opentelemetry_instrumentation_httpx-0.59b0-py3-none-any.whl", hash = "sha256:7dc9f66aef4ca3904d877f459a70c78eafd06131dc64d713b9b1b5a7d0a48f05", size = 15197, upload-time = "2025-10-16T08:38:55.507Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-redis" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/58bf83b10a97f67c7f06505bc4c4accbea7d961dec653a8c9e91fb65887e/opentelemetry_instrumentation_redis-0.59b0.tar.gz", hash = "sha256:d7f1c7c55ab57e10e0155c4c65d028a7e436aec7ccc7ccbf1d77e8cd12b55abd", size = 13922, upload-time = "2025-10-16T08:39:59.507Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/87/fef04827239ce84e2729b11611e8d5be7892288f620961ee9b9bafd035c5/opentelemetry_instrumentation_redis-0.59b0-py3-none-any.whl", hash = "sha256:8f7494dede5a6bfe5d8f20da67b371a502883398081856378380efef27da0bdf", size = 14946, upload-time = "2025-10-16T08:39:07.887Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-sqlalchemy" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/00/c5222a5e0521772aa530008c6c9c67f453e2b00e97d91fd799e8159aecf5/opentelemetry_instrumentation_sqlalchemy-0.59b0.tar.gz", hash = "sha256:7647b1e63497deebd41f9525c414699e0d49f19efcadc8a0642b715897f62d32", size = 14993, upload-time = "2025-10-16T08:40:01.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/a9/55d75a3d46c635a48cf3ad3b2599bad1d4ae47eeb1979b19ca47df47dc8c/opentelemetry_instrumentation_sqlalchemy-0.59b0-py3-none-any.whl", hash = "sha256:4ef150c49b6d1a8a7328f9d23ff40c285a245b88b0875ed2e5d277a40aa921c8", size = 14211, upload-time = "2025-10-16T08:39:10.714Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/14/f0c4f0f6371b9cb7f9fa9ee8918bfd59ac7040c7791f1e6da32a1839780d/opentelemetry_proto-1.38.0.tar.gz", hash = "sha256:88b161e89d9d372ce723da289b7da74c3a8354a8e5359992be813942969ed468", size = 46152, upload-time = "2025-10-16T08:36:01.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/6a/82b68b14efca5150b2632f3692d627afa76b77378c4999f2648979409528/opentelemetry_proto-1.38.0-py3-none-any.whl", hash = "sha256:b6ebe54d3217c42e45462e2a1ae28c3e2bf2ec5a5645236a490f55f45f1a0a18", size = 72535, upload-time = "2025-10-16T08:35:45.749Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/cb/f0eee1445161faf4c9af3ba7b848cc22a50a3d3e2515051ad8628c35ff80/opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe", size = 171942, upload-time = "2025-10-16T08:36:02.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/2e/e93777a95d7d9c40d270a371392b6d6f1ff170c2a3cb32d6176741b5b723/opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b", size = 132349, upload-time = "2025-10-16T08:35:46.995Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/bc/8b9ad3802cd8ac6583a4eb7de7e5d7db004e89cb7efe7008f9c8a537ee75/opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0", size = 129861, upload-time = "2025-10-16T08:36:03.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954, upload-time = "2025-10-16T08:35:48.054Z" }, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/f7/13cd081e7851c42520ab0e96efb17ffbd901111a50b8252ec1e240664020/opentelemetry_util_http-0.59b0.tar.gz", hash = "sha256:ae66ee91be31938d832f3b4bc4eb8a911f6eddd38969c4a871b1230db2a0a560", size = 9412, upload-time = "2025-10-16T08:40:11.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/56/62282d1d4482061360449dacc990c89cad0fc810a2ed937b636300f55023/opentelemetry_util_http-0.59b0-py3-none-any.whl", hash = "sha256:6d036a07563bce87bf521839c0671b507a02a0d39d7ea61b88efa14c6e25355d", size = 7648, upload-time = "2025-10-16T08:39:25.706Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/fe/ed708782d6709cc60eb4c2d8a361a440661f74134675c72990f2c48c785f/orjson-3.11.4.tar.gz", hash = "sha256:39485f4ab4c9b30a3943cfe99e1a213c4776fb69e8abd68f66b83d5a0b0fdc6d", size = 5945188, upload-time = "2025-10-24T15:50:38.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/15/c52aa7112006b0f3d6180386c3a46ae057f932ab3425bc6f6ac50431cca1/orjson-3.11.4-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2d6737d0e616a6e053c8b4acc9eccea6b6cce078533666f32d140e4f85002534", size = 243525, upload-time = "2025-10-24T15:49:29.737Z" }, + { url = "https://files.pythonhosted.org/packages/ec/38/05340734c33b933fd114f161f25a04e651b0c7c33ab95e9416ade5cb44b8/orjson-3.11.4-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:afb14052690aa328cc118a8e09f07c651d301a72e44920b887c519b313d892ff", size = 128871, upload-time = "2025-10-24T15:49:31.109Z" }, + { url = "https://files.pythonhosted.org/packages/55/b9/ae8d34899ff0c012039b5a7cb96a389b2476e917733294e498586b45472d/orjson-3.11.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38aa9e65c591febb1b0aed8da4d469eba239d434c218562df179885c94e1a3ad", size = 130055, upload-time = "2025-10-24T15:49:33.382Z" }, + { url = "https://files.pythonhosted.org/packages/33/aa/6346dd5073730451bee3681d901e3c337e7ec17342fb79659ec9794fc023/orjson-3.11.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f2cf4dfaf9163b0728d061bebc1e08631875c51cd30bf47cb9e3293bfbd7dcd5", size = 129061, upload-time = "2025-10-24T15:49:34.935Z" }, + { url = "https://files.pythonhosted.org/packages/39/e4/8eea51598f66a6c853c380979912d17ec510e8e66b280d968602e680b942/orjson-3.11.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89216ff3dfdde0e4070932e126320a1752c9d9a758d6a32ec54b3b9334991a6a", size = 136541, upload-time = "2025-10-24T15:49:36.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/47/cb8c654fa9adcc60e99580e17c32b9e633290e6239a99efa6b885aba9dbc/orjson-3.11.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9daa26ca8e97fae0ce8aa5d80606ef8f7914e9b129b6b5df9104266f764ce436", size = 137535, upload-time = "2025-10-24T15:49:38.307Z" }, + { url = "https://files.pythonhosted.org/packages/43/92/04b8cc5c2b729f3437ee013ce14a60ab3d3001465d95c184758f19362f23/orjson-3.11.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c8b2769dc31883c44a9cd126560327767f848eb95f99c36c9932f51090bfce9", size = 136703, upload-time = "2025-10-24T15:49:40.795Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fd/d0733fcb9086b8be4ebcfcda2d0312865d17d0d9884378b7cffb29d0763f/orjson-3.11.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1469d254b9884f984026bd9b0fa5bbab477a4bfe558bba6848086f6d43eb5e73", size = 136293, upload-time = "2025-10-24T15:49:42.347Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/3c5514e806837c210492d72ae30ccf050ce3f940f45bf085bab272699ef4/orjson-3.11.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:68e44722541983614e37117209a194e8c3ad07838ccb3127d96863c95ec7f1e0", size = 140131, upload-time = "2025-10-24T15:49:43.638Z" }, + { url = "https://files.pythonhosted.org/packages/9c/dd/ba9d32a53207babf65bd510ac4d0faaa818bd0df9a9c6f472fe7c254f2e3/orjson-3.11.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8e7805fda9672c12be2f22ae124dcd7b03928d6c197544fe12174b86553f3196", size = 406164, upload-time = "2025-10-24T15:49:45.498Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f9/f68ad68f4af7c7bde57cd514eaa2c785e500477a8bc8f834838eb696a685/orjson-3.11.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:04b69c14615fb4434ab867bf6f38b2d649f6f300af30a6705397e895f7aec67a", size = 149859, upload-time = "2025-10-24T15:49:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d2/7f847761d0c26818395b3d6b21fb6bc2305d94612a35b0a30eae65a22728/orjson-3.11.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:639c3735b8ae7f970066930e58cf0ed39a852d417c24acd4a25fc0b3da3c39a6", size = 139926, upload-time = "2025-10-24T15:49:48.321Z" }, + { url = "https://files.pythonhosted.org/packages/9f/37/acd14b12dc62db9a0e1d12386271b8661faae270b22492580d5258808975/orjson-3.11.4-cp313-cp313-win32.whl", hash = "sha256:6c13879c0d2964335491463302a6ca5ad98105fc5db3565499dcb80b1b4bd839", size = 136007, upload-time = "2025-10-24T15:49:49.938Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a9/967be009ddf0a1fffd7a67de9c36656b28c763659ef91352acc02cbe364c/orjson-3.11.4-cp313-cp313-win_amd64.whl", hash = "sha256:09bf242a4af98732db9f9a1ec57ca2604848e16f132e3f72edfd3c5c96de009a", size = 131314, upload-time = "2025-10-24T15:49:51.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/db/399abd6950fbd94ce125cb8cd1a968def95174792e127b0642781e040ed4/orjson-3.11.4-cp313-cp313-win_arm64.whl", hash = "sha256:a85f0adf63319d6c1ba06fb0dbf997fced64a01179cf17939a6caca662bf92de", size = 126152, upload-time = "2025-10-24T15:49:52.922Z" }, + { url = "https://files.pythonhosted.org/packages/25/e3/54ff63c093cc1697e758e4fceb53164dd2661a7d1bcd522260ba09f54533/orjson-3.11.4-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:42d43a1f552be1a112af0b21c10a5f553983c2a0938d2bbb8ecd8bc9fb572803", size = 243501, upload-time = "2025-10-24T15:49:54.288Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7d/e2d1076ed2e8e0ae9badca65bf7ef22710f93887b29eaa37f09850604e09/orjson-3.11.4-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:26a20f3fbc6c7ff2cb8e89c4c5897762c9d88cf37330c6a117312365d6781d54", size = 128862, upload-time = "2025-10-24T15:49:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/9f/37/ca2eb40b90621faddfa9517dfe96e25f5ae4d8057a7c0cdd613c17e07b2c/orjson-3.11.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e3f20be9048941c7ffa8fc523ccbd17f82e24df1549d1d1fe9317712d19938e", size = 130047, upload-time = "2025-10-24T15:49:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/c7/62/1021ed35a1f2bad9040f05fa4cc4f9893410df0ba3eaa323ccf899b1c90a/orjson-3.11.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aac364c758dc87a52e68e349924d7e4ded348dedff553889e4d9f22f74785316", size = 129073, upload-time = "2025-10-24T15:49:58.782Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3f/f84d966ec2a6fd5f73b1a707e7cd876813422ae4bf9f0145c55c9c6a0f57/orjson-3.11.4-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5c54a6d76e3d741dcc3f2707f8eeb9ba2a791d3adbf18f900219b62942803b1", size = 136597, upload-time = "2025-10-24T15:50:00.12Z" }, + { url = "https://files.pythonhosted.org/packages/32/78/4fa0aeca65ee82bbabb49e055bd03fa4edea33f7c080c5c7b9601661ef72/orjson-3.11.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f28485bdca8617b79d44627f5fb04336897041dfd9fa66d383a49d09d86798bc", size = 137515, upload-time = "2025-10-24T15:50:01.57Z" }, + { url = "https://files.pythonhosted.org/packages/c1/9d/0c102e26e7fde40c4c98470796d050a2ec1953897e2c8ab0cb95b0759fa2/orjson-3.11.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bfc2a484cad3585e4ba61985a6062a4c2ed5c7925db6d39f1fa267c9d166487f", size = 136703, upload-time = "2025-10-24T15:50:02.944Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/2de7188705b4cdfaf0b6c97d2f7849c17d2003232f6e70df98602173f788/orjson-3.11.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e34dbd508cb91c54f9c9788923daca129fe5b55c5b4eebe713bf5ed3791280cf", size = 136311, upload-time = "2025-10-24T15:50:04.441Z" }, + { url = "https://files.pythonhosted.org/packages/e0/52/847fcd1a98407154e944feeb12e3b4d487a0e264c40191fb44d1269cbaa1/orjson-3.11.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b13c478fa413d4b4ee606ec8e11c3b2e52683a640b006bb586b3041c2ca5f606", size = 140127, upload-time = "2025-10-24T15:50:07.398Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ae/21d208f58bdb847dd4d0d9407e2929862561841baa22bdab7aea10ca088e/orjson-3.11.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:724ca721ecc8a831b319dcd72cfa370cc380db0bf94537f08f7edd0a7d4e1780", size = 406201, upload-time = "2025-10-24T15:50:08.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/55/0789d6de386c8366059db098a628e2ad8798069e94409b0d8935934cbcb9/orjson-3.11.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:977c393f2e44845ce1b540e19a786e9643221b3323dae190668a98672d43fb23", size = 149872, upload-time = "2025-10-24T15:50:10.234Z" }, + { url = "https://files.pythonhosted.org/packages/cc/1d/7ff81ea23310e086c17b41d78a72270d9de04481e6113dbe2ac19118f7fb/orjson-3.11.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e539e382cf46edec157ad66b0b0872a90d829a6b71f17cb633d6c160a223155", size = 139931, upload-time = "2025-10-24T15:50:11.623Z" }, + { url = "https://files.pythonhosted.org/packages/77/92/25b886252c50ed64be68c937b562b2f2333b45afe72d53d719e46a565a50/orjson-3.11.4-cp314-cp314-win32.whl", hash = "sha256:d63076d625babab9db5e7836118bdfa086e60f37d8a174194ae720161eb12394", size = 136065, upload-time = "2025-10-24T15:50:13.025Z" }, + { url = "https://files.pythonhosted.org/packages/63/b8/718eecf0bb7e9d64e4956afaafd23db9f04c776d445f59fe94f54bdae8f0/orjson-3.11.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a54d6635fa3aaa438ae32e8570b9f0de36f3f6562c308d2a2a452e8b0592db1", size = 131310, upload-time = "2025-10-24T15:50:14.46Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bf/def5e25d4d8bfce296a9a7c8248109bf58622c21618b590678f945a2c59c/orjson-3.11.4-cp314-cp314-win_arm64.whl", hash = "sha256:78b999999039db3cf58f6d230f524f04f75f129ba3d1ca2ed121f8657e575d3d", size = 126151, upload-time = "2025-10-24T15:50:15.878Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[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 = "protobuf" +version = "6.33.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/ff/64a6c8f420818bb873713988ca5492cba3a7946be57e027ac63495157d97/protobuf-6.33.0.tar.gz", hash = "sha256:140303d5c8d2037730c548f8c7b93b20bb1dc301be280c378b82b8894589c954", size = 443463, upload-time = "2025-10-15T20:39:52.159Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/ee/52b3fa8feb6db4a833dfea4943e175ce645144532e8a90f72571ad85df4e/protobuf-6.33.0-cp310-abi3-win32.whl", hash = "sha256:d6101ded078042a8f17959eccd9236fb7a9ca20d3b0098bbcb91533a5680d035", size = 425593, upload-time = "2025-10-15T20:39:40.29Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c6/7a465f1825872c55e0341ff4a80198743f73b69ce5d43ab18043699d1d81/protobuf-6.33.0-cp310-abi3-win_amd64.whl", hash = "sha256:9a031d10f703f03768f2743a1c403af050b6ae1f3480e9c140f39c45f81b13ee", size = 436882, upload-time = "2025-10-15T20:39:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a9/b6eee662a6951b9c3640e8e452ab3e09f117d99fc10baa32d1581a0d4099/protobuf-6.33.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:905b07a65f1a4b72412314082c7dbfae91a9e8b68a0cc1577515f8df58ecf455", size = 427521, upload-time = "2025-10-15T20:39:43.803Z" }, + { url = "https://files.pythonhosted.org/packages/10/35/16d31e0f92c6d2f0e77c2a3ba93185130ea13053dd16200a57434c882f2b/protobuf-6.33.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e0697ece353e6239b90ee43a9231318302ad8353c70e6e45499fa52396debf90", size = 324445, upload-time = "2025-10-15T20:39:44.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/2a981a13e35cda8b75b5585aaffae2eb904f8f351bdd3870769692acbd8a/protobuf-6.33.0-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:e0a1715e4f27355afd9570f3ea369735afc853a6c3951a6afe1f80d8569ad298", size = 339159, upload-time = "2025-10-15T20:39:46.186Z" }, + { url = "https://files.pythonhosted.org/packages/21/51/0b1cbad62074439b867b4e04cc09b93f6699d78fd191bed2bbb44562e077/protobuf-6.33.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:35be49fd3f4fefa4e6e2aacc35e8b837d6703c37a2168a55ac21e9b1bc7559ef", size = 323172, upload-time = "2025-10-15T20:39:47.465Z" }, + { url = "https://files.pythonhosted.org/packages/07/d1/0a28c21707807c6aacd5dc9c3704b2aa1effbf37adebd8caeaf68b17a636/protobuf-6.33.0-py3-none-any.whl", hash = "sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995", size = 170477, upload-time = "2025-10-15T20:39:51.311Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.3" +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/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383, upload-time = "2025-10-17T15:04:21.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431, upload-time = "2025-10-17T15:04:19.346Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688, upload-time = "2025-10-14T10:20:54.448Z" }, + { url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807, upload-time = "2025-10-14T10:20:56.115Z" }, + { url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669, upload-time = "2025-10-14T10:20:57.874Z" }, + { url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629, upload-time = "2025-10-14T10:21:00.006Z" }, + { url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049, upload-time = "2025-10-14T10:21:01.801Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409, upload-time = "2025-10-14T10:21:03.556Z" }, + { url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635, upload-time = "2025-10-14T10:21:05.385Z" }, + { url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284, upload-time = "2025-10-14T10:21:07.122Z" }, + { url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566, upload-time = "2025-10-14T10:21:08.981Z" }, + { url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809, upload-time = "2025-10-14T10:21:10.805Z" }, + { url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119, upload-time = "2025-10-14T10:21:12.583Z" }, + { url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398, upload-time = "2025-10-14T10:21:14.584Z" }, + { url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735, upload-time = "2025-10-14T10:21:16.432Z" }, + { url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209, upload-time = "2025-10-14T10:21:18.213Z" }, + { url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324, upload-time = "2025-10-14T10:21:20.363Z" }, + { url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515, upload-time = "2025-10-14T10:21:22.339Z" }, + { url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819, upload-time = "2025-10-14T10:21:26.683Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866, upload-time = "2025-10-14T10:21:28.951Z" }, + { url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034, upload-time = "2025-10-14T10:21:30.869Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" }, + { url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" }, + { url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277, upload-time = "2025-10-14T10:21:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608, upload-time = "2025-10-14T10:21:59.557Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614, upload-time = "2025-10-14T10:22:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" }, + { url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" }, + { url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/c5/dbbc27b814c71676593d1c3f718e6cd7d4f00652cefa24b75f7aa3efb25e/pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180", size = 188394, upload-time = "2025-09-24T14:19:11.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c", size = 48608, upload-time = "2025-09-24T14:19:10.015Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pymysql" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/ae/1fe3fcd9f959efa0ebe200b8de88b5a5ce3e767e38c7ac32fb179f16a388/pymysql-1.1.2.tar.gz", hash = "sha256:4961d3e165614ae65014e361811a724e2044ad3ea3739de9903ae7c21f539f03", size = 48258, upload-time = "2025-08-24T12:55:55.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl", hash = "sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9", size = 45300, upload-time = "2025-08-24T12:55:53.394Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.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/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "redis" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/0e/80de0c7d9b04360331906b6b713a967e6523d155a92090983eba2e99302e/redis-7.0.0.tar.gz", hash = "sha256:6546ada54354248a53a47342d36abe6172bb156f23d24f018fda2e3c06b9c97a", size = 4754895, upload-time = "2025-10-22T15:38:36.128Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/de/68c1add9d9a49588e6f75a149e079e44bab973e748a35e0582ccada09002/redis-7.0.0-py3-none-any.whl", hash = "sha256:1e66c8355b3443af78367c4937484cd875fdf9f5f14e1fed14aa95869e64f6d1", size = 339526, upload-time = "2025-10-22T15:38:34.901Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/34/8218a19b2055b80601e8fd201ec723c74c7fe1ca06d525a43ed07b6d8e85/ruff-0.14.2.tar.gz", hash = "sha256:98da787668f239313d9c902ca7c523fe11b8ec3f39345553a51b25abc4629c96", size = 5539663, upload-time = "2025-10-23T19:37:00.956Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/dd/23eb2db5ad9acae7c845700493b72d3ae214dce0b226f27df89216110f2b/ruff-0.14.2-py3-none-linux_armv6l.whl", hash = "sha256:7cbe4e593505bdec5884c2d0a4d791a90301bc23e49a6b1eb642dd85ef9c64f1", size = 12533390, upload-time = "2025-10-23T19:36:18.044Z" }, + { url = "https://files.pythonhosted.org/packages/5a/8c/5f9acff43ddcf3f85130d0146d0477e28ccecc495f9f684f8f7119b74c0d/ruff-0.14.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8d54b561729cee92f8d89c316ad7a3f9705533f5903b042399b6ae0ddfc62e11", size = 12887187, upload-time = "2025-10-23T19:36:22.664Z" }, + { url = "https://files.pythonhosted.org/packages/99/fa/047646491479074029665022e9f3dc6f0515797f40a4b6014ea8474c539d/ruff-0.14.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5c8753dfa44ebb2cde10ce5b4d2ef55a41fb9d9b16732a2c5df64620dbda44a3", size = 11925177, upload-time = "2025-10-23T19:36:24.778Z" }, + { url = "https://files.pythonhosted.org/packages/15/8b/c44cf7fe6e59ab24a9d939493a11030b503bdc2a16622cede8b7b1df0114/ruff-0.14.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d0bbeffb8d9f4fccf7b5198d566d0bad99a9cb622f1fc3467af96cb8773c9e3", size = 12358285, upload-time = "2025-10-23T19:36:26.979Z" }, + { url = "https://files.pythonhosted.org/packages/45/01/47701b26254267ef40369aea3acb62a7b23e921c27372d127e0f3af48092/ruff-0.14.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7047f0c5a713a401e43a88d36843d9c83a19c584e63d664474675620aaa634a8", size = 12303832, upload-time = "2025-10-23T19:36:29.192Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5c/ae7244ca4fbdf2bee9d6405dcd5bc6ae51ee1df66eb7a9884b77b8af856d/ruff-0.14.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bf8d2f9aa1602599217d82e8e0af7fd33e5878c4d98f37906b7c93f46f9a839", size = 13036995, upload-time = "2025-10-23T19:36:31.861Z" }, + { url = "https://files.pythonhosted.org/packages/27/4c/0860a79ce6fd4c709ac01173f76f929d53f59748d0dcdd662519835dae43/ruff-0.14.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1c505b389e19c57a317cf4b42db824e2fca96ffb3d86766c1c9f8b96d32048a7", size = 14512649, upload-time = "2025-10-23T19:36:33.915Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7f/d365de998069720a3abfc250ddd876fc4b81a403a766c74ff9bde15b5378/ruff-0.14.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a307fc45ebd887b3f26b36d9326bb70bf69b01561950cdcc6c0bdf7bb8e0f7cc", size = 14088182, upload-time = "2025-10-23T19:36:36.983Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ea/d8e3e6b209162000a7be1faa41b0a0c16a133010311edc3329753cc6596a/ruff-0.14.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61ae91a32c853172f832c2f40bd05fd69f491db7289fb85a9b941ebdd549781a", size = 13599516, upload-time = "2025-10-23T19:36:39.208Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ea/c7810322086db68989fb20a8d5221dd3b79e49e396b01badca07b433ab45/ruff-0.14.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1967e40286f63ee23c615e8e7e98098dedc7301568bd88991f6e544d8ae096", size = 13272690, upload-time = "2025-10-23T19:36:41.453Z" }, + { url = "https://files.pythonhosted.org/packages/a9/39/10b05acf8c45786ef501d454e00937e1b97964f846bf28883d1f9619928a/ruff-0.14.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2877f02119cdebf52a632d743a2e302dea422bfae152ebe2f193d3285a3a65df", size = 13496497, upload-time = "2025-10-23T19:36:43.61Z" }, + { url = "https://files.pythonhosted.org/packages/59/a1/1f25f8301e13751c30895092485fada29076e5e14264bdacc37202e85d24/ruff-0.14.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e681c5bc777de5af898decdcb6ba3321d0d466f4cb43c3e7cc2c3b4e7b843a05", size = 12266116, upload-time = "2025-10-23T19:36:45.625Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/0029bfc9ce16ae78164e6923ef392e5f173b793b26cc39aa1d8b366cf9dc/ruff-0.14.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e21be42d72e224736f0c992cdb9959a2fa53c7e943b97ef5d081e13170e3ffc5", size = 12281345, upload-time = "2025-10-23T19:36:47.618Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ab/ece7baa3c0f29b7683be868c024f0838770c16607bea6852e46b202f1ff6/ruff-0.14.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b8264016f6f209fac16262882dbebf3f8be1629777cf0f37e7aff071b3e9b92e", size = 12629296, upload-time = "2025-10-23T19:36:49.789Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7f/638f54b43f3d4e48c6a68062794e5b367ddac778051806b9e235dfb7aa81/ruff-0.14.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5ca36b4cb4db3067a3b24444463ceea5565ea78b95fe9a07ca7cb7fd16948770", size = 13371610, upload-time = "2025-10-23T19:36:51.882Z" }, + { url = "https://files.pythonhosted.org/packages/8d/35/3654a973ebe5b32e1fd4a08ed2d46755af7267da7ac710d97420d7b8657d/ruff-0.14.2-py3-none-win32.whl", hash = "sha256:41775927d287685e08f48d8eb3f765625ab0b7042cc9377e20e64f4eb0056ee9", size = 12415318, upload-time = "2025-10-23T19:36:53.961Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/3758bcf9e0b6a4193a6f51abf84254aba00887dfa8c20aba18aa366c5f57/ruff-0.14.2-py3-none-win_amd64.whl", hash = "sha256:0df3424aa5c3c08b34ed8ce099df1021e3adaca6e90229273496b839e5a7e1af", size = 13565279, upload-time = "2025-10-23T19:36:56.578Z" }, + { url = "https://files.pythonhosted.org/packages/2e/5d/aa883766f8ef9ffbe6aa24f7192fb71632f31a30e77eb39aa2b0dc4290ac/ruff-0.14.2-py3-none-win_arm64.whl", hash = "sha256:ea9d635e83ba21569fbacda7e78afbfeb94911c9434aff06192d9bc23fd5495a", size = 12554956, upload-time = "2025-10-23T19:36:58.714Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.44" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/f2/840d7b9496825333f532d2e3976b8eadbf52034178aac53630d09fe6e1ef/sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22", size = 9819830, upload-time = "2025-10-10T14:39:12.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/d3/c67077a2249fdb455246e6853166360054c331db4613cda3e31ab1cadbef/sqlalchemy-2.0.44-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1", size = 2135479, upload-time = "2025-10-10T16:03:37.671Z" }, + { url = "https://files.pythonhosted.org/packages/2b/91/eabd0688330d6fd114f5f12c4f89b0d02929f525e6bf7ff80aa17ca802af/sqlalchemy-2.0.44-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45", size = 2123212, upload-time = "2025-10-10T16:03:41.755Z" }, + { url = "https://files.pythonhosted.org/packages/b0/bb/43e246cfe0e81c018076a16036d9b548c4cc649de241fa27d8d9ca6f85ab/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976", size = 3255353, upload-time = "2025-10-10T15:35:31.221Z" }, + { url = "https://files.pythonhosted.org/packages/b9/96/c6105ed9a880abe346b64d3b6ddef269ddfcab04f7f3d90a0bf3c5a88e82/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c", size = 3260222, upload-time = "2025-10-10T15:43:50.124Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/1857e35a47155b5ad927272fee81ae49d398959cb749edca6eaa399b582f/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d", size = 3189614, upload-time = "2025-10-10T15:35:32.578Z" }, + { url = "https://files.pythonhosted.org/packages/88/ee/4afb39a8ee4fc786e2d716c20ab87b5b1fb33d4ac4129a1aaa574ae8a585/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40", size = 3226248, upload-time = "2025-10-10T15:43:51.862Z" }, + { url = "https://files.pythonhosted.org/packages/32/d5/0e66097fc64fa266f29a7963296b40a80d6a997b7ac13806183700676f86/sqlalchemy-2.0.44-cp313-cp313-win32.whl", hash = "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73", size = 2101275, upload-time = "2025-10-10T15:03:26.096Z" }, + { url = "https://files.pythonhosted.org/packages/03/51/665617fe4f8c6450f42a6d8d69243f9420f5677395572c2fe9d21b493b7b/sqlalchemy-2.0.44-cp313-cp313-win_amd64.whl", hash = "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e", size = 2127901, upload-time = "2025-10-10T15:03:27.548Z" }, + { url = "https://files.pythonhosted.org/packages/9c/5e/6a29fa884d9fb7ddadf6b69490a9d45fded3b38541713010dad16b77d015/sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05", size = 1928718, upload-time = "2025-10-10T15:29:45.32Z" }, +] + +[[package]] +name = "sqlmodel" +version = "0.0.27" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/5a/693d90866233e837d182da76082a6d4c2303f54d3aaaa5c78e1238c5d863/sqlmodel-0.0.27.tar.gz", hash = "sha256:ad1227f2014a03905aef32e21428640848ac09ff793047744a73dfdd077ff620", size = 118053, upload-time = "2025-10-08T16:39:11.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/92/c35e036151fe53822893979f8a13e6f235ae8191f4164a79ae60a95d66aa/sqlmodel-0.0.27-py3-none-any.whl", hash = "sha256:667fe10aa8ff5438134668228dc7d7a08306f4c5c4c7e6ad3ad68defa0e7aa49", size = 29131, upload-time = "2025-10-08T16:39:10.917Z" }, +] + +[[package]] +name = "starlette" +version = "0.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" }, +] + +[[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 = "uvicorn" +version = "0.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]