diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cca140c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,33 @@ +# Never send the archived codebase or local configuration to a build context. +old_repo/ +.env +.env.* + +# Local Python environments and generated files. +.venv/ +venv/ +__pycache__/ +*.py[cod] +.pytest_cache/ +.ruff_cache/ +.pyright/ +.coverage* +htmlcov/ + +# Local web dependencies and build output. +node_modules/ +**/node_modules/ +.bun/ +**/.bun/ +.svelte-kit/ +**/.svelte-kit/ +playwright-report/ +test-results/ + +# Repository metadata and unrelated local artifacts. +.git/ +.claude/ +.codex/ +.scratch/ +vault/ +rewrite_architect.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..822617f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.{yml,yaml,json,toml,ini,md}] +indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/.env.example b/.env.example index a76635e..781a8e5 100644 --- a/.env.example +++ b/.env.example @@ -1,19 +1,28 @@ -# Local development only. Replace all credentials in deployed environments. -ENVIRONMENT=development -DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5433/lead_scoring -AIHUB_BASE_URL=http://localhost:8080 -DEBUG=false +# Example values for local development only. Do not use these credentials in staging/production. +# .env is ignored by Git. .env.test is local-only and must point to ai_platform_test. +# Compose interpolates this file; only explicitly listed environment entries reach containers. -# JSON arrays consumed by pydantic-settings. These values work with the Dev Console example. -SERVICE_CALLERS=[{"caller_id":"dev-console","token":"local-dev-token","allowed_apps":["dev-console"],"allowed_features":["lead-scoring"],"allowed_tenants":["omni-account-42","omni-test-tenant-42"]}] -TENANT_ALLOWLIST=["omni-account-42","omni-test-tenant-42"] -SCORING_CONFIGS=[{"version":"1.0.0","confidence":0.8,"confidence_threshold":0.7,"hitl_policy_version":"1.0.0","effort_profile":"medium","fallback_policy":"rules_only_review","ai_model":"openai/gpt-4o-mini"}] +# Docker Compose PostgreSQL (local only) +POSTGRES_USER=ai_platform +POSTGRES_PASSWORD=local-change-this-password +POSTGRES_DB=ai_platform +POSTGRES_PORT=55432 +BACKEND_PORT=8000 +WEB_PORT=3000 -WORKER_LEASE_SECONDS=30 -WORKER_RECOVERY_INTERVAL_SECONDS=5 -KILL_SWITCH_ENABLED=false +# Backend: read by ai_platform.infrastructure.config.settings.Settings (AI_PLATFORM_ prefix) +AI_PLATFORM_ENVIRONMENT=development +AI_PLATFORM_DATABASE_URL=postgresql+asyncpg://ai_platform:local-change-this-password@127.0.0.1:55432/ai_platform +# Required for backend provisioning endpoint; generate a fresh random secret for local use. +AI_PLATFORM_TENANT_PROVISIONING_SECRET=local-provisioner-replace-before-use +AI_PLATFORM_ADMIN_COOKIE_SECURE=false +AI_PLATFORM_SESSION_TTL_SECONDS=28800 +# Optional, required together when sending webhook to a local Omni receiver. +# AI_PLATFORM_WEBHOOK_URL=http://localhost:8080/api/v1/internal/lead-scoring/events +# AI_PLATFORM_WEBHOOK_SIGNING_SECRET=local-replace-before-use +# Optional; no global V-Key: Omni sends X-Tenant-VKey per scoring request. +# AI_PLATFORM_AIHUB_BASE_URL=http://localhost:8080 -# Enable webhook delivery by setting both values. -# WEBHOOK_SUBSCRIBER_URL=http://localhost:8080/api/v1/internal/lead-scoring/events -# WEBHOOK_SIGNING_SECRET=replace-with-local-signing-secret -WEBHOOK_SIGNING_KEY_ID=pilot-2026-01 +# Compose web origin only; native SvelteKit dev reads apps/web/.env. +WEB_ORIGIN=http://localhost:3000 +# Do not place tenant keys, V-Keys, admin sessions or backend URLs in PUBLIC_* variables. diff --git a/.gitignore b/.gitignore index fe13e33..27b5e02 100644 --- a/.gitignore +++ b/.gitignore @@ -1,76 +1,42 @@ -# Python & Virtual Environments -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -.venv/ -venv/ -ENV/ -env/ -.uv/ - -# Linters, Type Checkers & Test Caches -.ruff_cache/ -.pytest_cache/ -.mypy_cache/ -.pyre/ -.pytype/ -.pyright/ -.coverage -.coverage.* -htmlcov/ -nosetests.xml -coverage.xml -*.cover -*.py,cover - -# Node / Bun / Frontend -node_modules/ -node_modules -apps/dev-console/node_modules -.bun/ -dist-ssr/ -*.local -.svelte-kit/ - -# Playwright & Testing Reports -playwright-report/ -test-results/ -blob-report/ - -# Environment Variables & Local Secrets +# Local configuration and private data .env .env.* !.env.example - -# Project-local vault/ .scratch/ -.codex/ -docs/agents/ .claude/ +.codex/ CLAUDE.LOCAL.md -# Generated artifacts -/openapi.json -# IDE, OS & Editor Artifacts +# Python +.venv/ +venv/ +__pycache__/ +*.py[cod] +*.egg-info/ +.pytest_cache/ +.ruff_cache/ +.pyright/ +.mypy_cache/ +.coverage* +htmlcov/ +build/ +dist/ + +# Web and generated output +node_modules/ +.bun/ +.svelte-kit/ +.playwright/ +playwright-report/ +test-results/ +*.local + +# Generated contracts: versioned Contract Pack is tracked separately +/openapi.json +/apps/web/src/lib/generated/ + +# OS and editor .DS_Store -Thumbs.db *.swp *.swo -*~ diff --git a/.importlinter b/.importlinter new file mode 100644 index 0000000..44672e7 --- /dev/null +++ b/.importlinter @@ -0,0 +1,23 @@ +[importlinter] +root_package = ai_platform + +[importlinter:contract:domain] +name = Lead Scoring domain stays independent of effects and HTTP +type = forbidden +source_modules = + ai_platform.lead_scoring.domain +forbidden_modules = + ai_platform.infrastructure + ai_platform.presentation + ai_platform.bootstrap + ai_platform.platform + +[importlinter:contract:application] +name = Lead Scoring application does not import adapters or HTTP +type = forbidden +source_modules = + ai_platform.lead_scoring.application +forbidden_modules = + ai_platform.infrastructure + ai_platform.presentation + ai_platform.bootstrap diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..0db4180 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +exclude: ^old_repo/ + +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.11 + hooks: + - id: ruff + - id: ruff-format + args: [--check] + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-yaml + - id: check-toml + - id: check-merge-conflict + - id: detect-private-key diff --git a/AGENTS.md b/AGENTS.md index 85982db..8f8dc4e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,172 +1,12 @@ -# AGENTS.md +# Repository workflow -This file provides guidance to Codex (Codex.ai/code) when working with this repository. +This repository is the rebuilt AI Platform. `old_repo/` is historical and excluded from builds. The approved rebuild plan authorizes implementation; `docs/specs/ai-platform-lead-scoring.md` is review-ready for cross-service acceptance, so unverified Omni/AIHub wire details are not release claims. -## Purpose and status +1. Before changing Lead Scoring behavior, read `docs/specs/ai-platform-lead-scoring.md`, `CONTEXT.md`, and `ARCHITECTURE.md`. Read `stack.md` when choosing a runtime or dependency. Generated OpenAPI and small Omni fixtures own exact wire shapes. +2. Put tenant/admin authentication, configuration and shared DB setup in `platform`; put deterministic rules/metrics and run lifecycle in `lead_scoring/domain`; orchestration in `application`, I/O in `infrastructure`, HTTP mapping in `presentation`, and wiring in `bootstrap`. Churn follows the same layout only when a specification exists. +3. Omni owns rules/weights/HITL/CRM; AI Platform owns AI profile and technical run status. V-Key is request-scoped RAM only. One tenant key per tenant authenticates the platform; Admin Web uses a distinct session. Terminal webhook triggers Omni GET. +4. **Kiểm tra tính nhất quán trước khi tối ưu một phần:** đối chiếu API với fixture Omni, tên env với `Settings`/Compose/CI, lệnh build với package/lockfile và UI với design system ở `ARCHITECTURE.md`. Web dùng Tailwind CSS v4 cho semantic tokens, shadcn-svelte cho component cần thiết, Bits UI cho tương tác phức tạp; dùng HTML native cho form/bảng đơn giản, không tự tái tạo primitives. Xem `rewrite_architect.md` để hiểu lý do chọn stack, không dùng nó làm contract song song. +5. File trên 200 dòng là tín hiệu review: kiểm tra trách nhiệm trộn lẫn, tách tại boundary có nghĩa. Giữ một OpenAPI generated và vài Omni fixtures thật, không tạo hệ schema thứ hai. +6. Trước bàn giao, chạy Ruff/Pyright/Import Linter/pytest, Svelte checks/build/tests, migration và contract generation có liên quan. Báo chính xác pass/fail; hook opt-in, CI là gate. Không stage `.env`, `.env.test`, ignored assets hoặc secrets. -This repository is the Python-first AI Platform workspace. Lead Scoring is the first service; related systems are `DotB-Omni`, `lead-scoring-agent`, and `aihub` under `/home/hung/code/dotb_workspace/`. - -The repository contains specifications, project documentation, and the initialized baseline for the Lead Scoring service (`src/lead_scoring/`). - -Before implementing or changing Lead Scoring Customer Pilot behavior, read: - -- `docs/specs/lead-scoring-customer-pilot.md`: authoritative product behavior, scope, invariants, acceptance boundaries, and cross-service ownership. -- `stack.md`: technology decisions, candidates, evidence, and unresolved stack questions. It does not override the approved specification. - -The DotB Vault RFC, initial brief, Grill Brief, legacy reports, drafts, and agent messages are historical/supporting context rather than implementation authority. Exact wire formats become authoritative through released Pydantic models, generated OpenAPI/JSON Schemas, and shared fixtures. - -## Communication - -- Keep responses short, specific, and decision-oriented. -- Put the decision or conclusion first, followed by only the evidence and next action needed. -- Keep the main context high-signal. Put raw logs, scratch data, and long intermediate reports in `/tmp/` or the appropriate durable document. -- Use lists and compact structure when they make the result easier to scan. -- State assumptions, unresolved decisions, and verification status explicitly. - -## Writing - -- Write directly and precisely. Avoid buzzwords, filler, and repeated explanations. -- Comments explain intent, invariants, trade-offs, or non-obvious constraints, not syntax or line-by-line behavior. -- Do not add speculative documentation, abstractions, or metadata that the repository does not need. -- Do not use emoji or em dashes in new prose, comments, or reports. Preserve emoji and punctuation that are part of existing paths or quoted source text. - -## Delegation - -- Prioritize standard subagents over teammates to minimize token overhead. Spawn multiple subagents when parallel execution is genuinely needed. -- Pick the agent type matching the task from available configured agents (e.g., `explorer`, `implementer`, `tester`, `researcher`, `adversarial-reviewer`, `general`, `codebase-memory`). -- Delegate context-heavy or tool-heavy work with a clear scope, completion criteria, and evidence requirements. -- Require condensed handoffs containing conclusions, changed paths, verification, and uncertainty. Do not paste raw logs into the main context. -- The main context retains final decisions and integration responsibility. Agent messages are evidence, not user approval. - -## Agent skills - -### Issue tracker - -Local Markdown issues are stored under `.scratch//`. See `docs/agents/issue-tracker.md`. - -### Domain docs - -The repository uses a single-context domain layout. See `docs/agents/domain.md`. - -## Project knowledge - -- `AGENTS.md` is the project map for conventions, verified commands, boundaries, and gotchas. -- `docs/specs/lead-scoring-customer-pilot.md` is the approved source of truth for Lead Scoring Customer Pilot product behavior and build boundaries. -- `stack.md` is the primary record for technology decisions and unresolved stack questions; it cannot override the approved spec. -- `docs/` contains shared project knowledge intended for version control. -- `/tmp/` is for scratch data, raw logs, and temporary handoffs. -- Released Pydantic models, generated OpenAPI/JSON Schemas, tests, and shared fixtures become the exact executable contract when they exist. - -## Documentation - -- Put temporary reports, raw logs, and scratch material in `/tmp/`. -- Put durable team knowledge in `docs/` and include it in version control. -- Use `vault/` only for durable project-local knowledge that must not be committed. -- New durable Markdown documents under `docs/` or `vault/` use minimal YAML frontmatter: - -```yaml ---- -title: Document title -description: A short description of what the document contains and when it is useful. -status: draft -tags: [] ---- -``` - -- `title` and `description` are required. For governed specs, `status` may be `draft`, `review_ready`, `approved`, or `deprecated`; other existing document schemas keep their established status vocabulary. `tags` is optional. -- Do not add metadata that Git, the filesystem, or existing tooling can derive. -- Preserve the format and metadata schema of existing documents. Do not add frontmatter, reformat, move, or normalize existing files unless the task explicitly requests migration. -- `AGENTS.md`, README files, and files with an existing schema keep their established format. - -## Repository structure - -- `src/lead_scoring/`: Lead Scoring production service package and CLI tools. -- `alembic/`: database migrations for PostgreSQL. -- `tests/`: test suite including health check, OpenAPI CLI, and migration lifecycle. -- `docs/specs/lead-scoring-customer-pilot.md`: approved Lead Scoring Customer Pilot specification. -- `docs/`: durable shared project documentation. -- `.scratch/`: local untracked draft specs and issue tickets used by agent skills. -- `stack.md`: technology decisions and unresolved stack questions. -- `.codex/settings.json`: project hook configuration. -- `.codex/hooks/post-edit-check.py`: Python-only PostToolUse feedback hook. - -## Product and frontend boundary - -The frontend boundary is approved for Customer Pilot: DotB-Omni owns all customer-facing Lead Scoring configuration and HITL UI. AI Platform may provide only an internal Dev Console for fixtures, contract testing, failure testing, and allowlisted integration verification; it must not duplicate Omni business UI. - -When the internal Dev Console is implemented: - -- Use Svelte 5 and TypeScript with Bun, Vite, and Vitest. Add SvelteKit only when routing, SSR, or deployment requires it. -- Generate types from backend OpenAPI and consume them through `openapi-fetch`. -- Keep shared transport concerns such as authentication and error handling outside generated code. -- Use the official Svelte tooling, including `@sveltejs/mcp` autofixer feedback and `svelte-check`. -- Use Playwright only for important end-to-end user flows. - -TypeScript 7 native compiler and language server are pilot candidates, not the adopted project default. Test them project-locally before changing global runtimes or the shared `mcpls` configuration. - -## Engineering principles - -- Follow YAGNI. Implement only what the current requirement needs; do not add speculative abstractions, extension points, compatibility layers, or infrastructure. -- Prefer the simplest design that preserves clear boundaries and can be changed safely. -- Reuse the standard library and established, actively maintained packages when they reduce code and operational burden. Avoid dependencies for trivial behavior. -- Keep code explicit. Use precise names, short control flow, and direct data transformations. Avoid cleverness, hidden side effects, global mutable state, and premature optimization. -- Prefer composition over inheritance and functions over classes. Introduce a class only when state, lifecycle, or a framework contract genuinely requires one. -- Keep a functional core with side effects at the boundaries. Isolate I/O, persistence, network calls, clocks, and environment access. -- Organize modules by capability or domain responsibility, not by arbitrary technical layers. -- Avoid wrappers that only rename another API. Every abstraction must remove meaningful complexity or enforce a real boundary. -- Preserve one clear source of truth for contracts, configuration, and domain rules. - -## AI-native code - -Write code that humans and coding agents can understand with minimal hidden context: - -- Make dependencies, inputs, outputs, errors, and side effects explicit. -- Use typed, stable contracts at module and service boundaries. -- Keep related implementation and tests easy to locate. -- Prefer deterministic behavior and small composable functions. -- Document decisions and non-obvious constraints, not syntax or line-by-line behavior. -- Match existing conventions before introducing a new pattern. -- Make the smallest coherent change and remove superseded code instead of leaving parallel implementations. - -## Verified commands - -- `uv sync`: sync workspace dependencies and environment. -- `uv run pytest`: run pytest test suite against Python 3.13. -- `uv run ruff check src tests`: run lint checks across source and tests. -- `uv run ruff format --check src tests`: check code formatting. -- `uv run pyright`: run Pyright static type checker. -- `uv run ty check`: run project-local ty type checker. -- `uv run generate-openapi`: deterministically generate root `openapi.json`. -- `uv run generate-openapi --check`: verify that `openapi.json` is present and matches the current backend contract. -- `uv run generate-webhook-contract`: deterministically generate terminal webhook contract pack. -- `uv run generate-webhook-contract --check`: verify that webhook contract pack is present and matches current models. -- `uv run generate-polling-contract`: deterministically generate scoring run polling contract pack. -- `uv run generate-polling-contract --check`: verify that polling contract pack is present and matches current models. -- `uv run generate-contract-pack`: deterministically generate the versioned shared Contract Pack. -- `uv run generate-contract-pack --validate-fixtures`: validate provider and consumer fixture expectations. -- `uv run generate-contract-pack --check`: verify the versioned Contract Pack and checksums are present and current. -- `uv run alembic upgrade head`: apply database migrations up to head. -- `uv run alembic downgrade base`: revert all database migrations. - -## Deterministic generation - -- When a verified project generator exists, use it instead of manually editing derived files. -- Treat OpenAPI output and generated frontend types as derived artifacts. Regenerate them from the backend contract and require the authoritative verification command to detect drift. -- Do not edit generated files manually unless the project workflow explicitly requires it. - -## Safety and scope - -- Inspect a path before overwriting or deleting it. Preserve existing configuration, documentation, code, and data unless the task explicitly authorizes removal or migration. -- Do not expose secrets in logs, reports, handoffs, or generated artifacts. -- Do not install packages, modify system configuration, or create permissions and background services without explicit approval. -- Bootstrap production code only after the relevant product and implementation decision. Customer-facing Lead Scoring UI remains owned by DotB-Omni. - -## Tooling and verification gotchas - -- Root `openapi.json` and frontend build output remain covered by the ignore policy; the versioned Contract Pack OpenAPI artifact is generated and committed, and must not be edited manually. -- The project PostToolUse hook is configured in `.codex/settings.json` for `Write` and `Edit`. It handles Python files via `ruff` (safe fix and format). Full type-checking (`pyright`, `ty`) and test suites belong to explicit verification steps. Preserve the working-tree modification to `.codex/hooks/post-edit-check.py` unless explicitly asked to change or reset it. -- Do not modify `/etc`, package-manager state, system services, drivers, kernel settings, global Node/TypeScript versions, or the shared `mcpls` configuration as part of repository work unless explicitly approved. - -## Quality of life - -Pyright LSP is available for semantic navigation. Use it for definitions, references, hover types, document and workspace symbols, and call hierarchy; then read the relevant implementation to understand business rules and side effects. +Use `README.md` for current local commands; `stack.md` for decisions and deferred production hosting/TLS/secret/backup/alerts. Preserve the user's existing ignored files unless explicitly authorized to replace them. No global tool installs or system configuration changes during repo work. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..3863012 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,60 @@ +# Architecture + +## Status and scope + +This document describes the intended architecture for the rebuilt AI Platform, not a verified description of working code. The root Lead Scoring provider specification is `docs/specs/ai-platform-lead-scoring.md`; it remains `draft/review_ready` pending review. Its ownership boundary is authoritative only after approval. The archived code and documents under `old_repo/` are historical evidence, not the new architecture or implementation. + +AI Platform is a provider service: it executes bounded rule scoring and a selected AI profile, maintains scoring-run lifecycle, and exposes result retrieval. DotB-Omni owns customer-configured rules/weights, HITL and customer actions, snapshot construction, user authorization, and CRM writeback. Omni is notified of terminal outcome through an agreed signed HMAC webhook and then calls the authenticated GET endpoint for run details. DotB EMS remains the CRM system of record. There is no customer-facing settings or review UI in this service scope. + +## Intended backend boundaries + +The proposed modular monolith has one runtime and one deployable backend. These names describe target ownership, not files already present. + +- **Bootstrap/composition:** loads validated settings and composes framework, database, provider client, and application workflows. Resource lifetime is established here. +- **Platform:** tenant-key verification and tenant isolation, shared persistence/runtime concerns, and operational interfaces. It does not own Omni customer configuration or user permissions. +- **Lead Scoring domain:** rule execution, scoring-run state/invariants, and profile semantics. Deterministic scoring rules should be pure functions; run identity and lifecycle belong in an entity/value model where useful. +- **Lead Scoring application:** admission, idempotency, execution coordination, result retrieval, and notification workflow. Use cases own transaction meaning; infrastructure supplies mechanisms. AI profile contains model, prompts and timeout; app-provided rules and weights arrive in each run body. +- **Infrastructure:** database mappings/repositories, AIHub HTTP client, HMAC signing, and delivery adapters. Provider details stop at these boundaries. +- **Presentation:** HTTP request/response mapping, tenant-auth dependency at the edge, health/readiness, and API error mapping. Transport schemas do not become domain objects by accident. + +Dependency direction is inward: presentation and infrastructure may depend on application/domain contracts; domain does not depend on frameworks, persistence, network, environment, or application orchestration. Bootstrap is the composition root. Avoid generic base services, empty ports, service locators, and multiple deployment processes unless evidence requires them. + +## Lifecycle and data invariants + +1. Authenticate and establish the tenant boundary before accessing a run. +2. Validate bounded request input and calculate a stable canonical-body fingerprint. +3. Scope client-supplied UUID idempotency to the tenant. Same UUID/same canonical body resolves to the existing run; same UUID/different body conflicts. The service generates a distinct run UUID. +4. Commit admitted run before acknowledging acceptance. Execution holds raw V-Key only in current process memory; it is never persisted. +5. Rules-only requires no AIHub or V-Key; medium bounded two-pass is the default when AI is requested. Light/heavy are deferred until verified demand, not placeholders. +6. Persist terminal outcome independently from notification delivery. Signed webhook informs Omni, which performs authenticated GET for the authoritative result. +7. One-process recovery terminalizes work that cannot resume after interruption. It does not restore the lost V-Key or promise an automatic retry. +8. Delete/redact sensitive run material at 90 days under an approved policy; tenant offboarding and backup treatment need operational decisions. + +These are target constraints, not statements that the code currently enforces them. See the spec for unresolved contract and review gates. + +## Transaction and effect boundaries + +- Application use cases define atomic boundaries, including run transition and any durable notification intent where selected. Persistence adapters implement transactions. +- AIHub requests are network effects with explicit timeout and bounded policy once AIHub behavior has been verified. Never add speculative provider retries. +- HMAC delivery is an effect with bounded retry and observable terminal delivery state. Exact signing and replay rules are contract decisions with Omni. +- Clock, UUID generation, provider client, and persistence are explicit inputs at the application boundary when deterministic testing or lifecycle behavior requires them. +- Domain functions are deterministic and I/O-free. Stateful objects are justified by identity, invariant, or lifecycle rather than used as namespaces. + +## Admin Web design system + +Admin Web dùng SvelteKit 5, Tailwind CSS v4 semantic tokens, shadcn-svelte component source cho button/input/card/badge/table khi UI cần, Bits UI cho dialog/dropdown/popover hoặc tương tác phức tạp, và một icon family Lucide nếu cần icon. Chỉ thêm component đang dùng, không scaffold cả catalog. `src/lib/components/ui/` chỉ chứa primitives không biết domain; route giữ thành phần chỉ dùng tại route. HTML native là lựa chọn đúng cho form, label và bảng đơn giản khi không cần behavior phức tạp. Style tokens có một nguồn trong `apps/web/src/app.css`; UI phải kiểm tra focus, keyboard, validation, responsive và contrast. Browser chỉ gọi SvelteKit; backend auth/credentials và `BACKEND_INTERNAL_URL` thuộc `$lib/server/`. Không tạo frontend Clean Architecture giả lập. + +## Verification structure (target) + +- Unit: domain calculations, validation and transitions without network/database. +- Integration: persistence/migrations on the selected production database and AIHub HTTP adapter with controlled transport. +- Functional: HTTP request through application/persistence and back, including tenant isolation and idempotency. +- Architecture: import direction, framework isolation, and composition-root rules. +- Contract: shared provider/Omni fixtures for requests, replay conflicts, result GET, HMAC delivery, acknowledgements, and errors. +- Operational: process interruption, recovery terminality, 90-day redaction, secret-leak sentinels, and deployment restore drills. + +Actual test commands, directories, and enforcement must follow checked-in tooling once present; do not describe this target taxonomy as an existing test suite. + +## Deferred decisions + +Production hosting/orchestrator, network and TLS topology, secret manager and rotation ownership, database backup/restore and deletion guarantees, alerting/observability destination, load/SLO targets, exact contract publication/versioning, and AIHub protocol/model/timeouts remain deferred. Resolve them with accountable owners before the corresponding implementation or deployment. No production-readiness claim is warranted while these remain open. diff --git a/CLAUDE.md b/CLAUDE.md index a767f37..9802a69 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,159 +1 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with this repository. - -## Purpose and status - -This repository is the Python-first AI Platform workspace. Lead Scoring is the first service; related systems are `DotB-Omni`, `lead-scoring-agent`, and `aihub` under `/home/hung/code/dotb_workspace/`. - -The repository contains specifications, project documentation, and the initialized baseline for the Lead Scoring service (`src/lead_scoring/`). - -Before implementing or changing Lead Scoring Customer Pilot behavior, read: - -- `docs/specs/lead-scoring-customer-pilot.md`: authoritative product behavior, scope, invariants, acceptance boundaries, and cross-service ownership. -- `stack.md`: technology decisions, candidates, evidence, and unresolved stack questions. It does not override the approved specification. - -The DotB Vault RFC, initial brief, Grill Brief, legacy reports, drafts, and agent messages are historical/supporting context rather than implementation authority. Exact wire formats become authoritative through released Pydantic models, generated OpenAPI/JSON Schemas, and shared fixtures. - -## Communication - -- Keep responses short, specific, and decision-oriented. -- Put the decision or conclusion first, followed by only the evidence and next action needed. -- Keep the main context high-signal. Put raw logs, scratch data, and long intermediate reports in `/tmp/` or the appropriate durable document. -- Use lists and compact structure when they make the result easier to scan. -- State assumptions, unresolved decisions, and verification status explicitly. - -## Writing - -- Write directly and precisely. Avoid buzzwords, filler, and repeated explanations. -- Comments explain intent, invariants, trade-offs, or non-obvious constraints, not syntax or line-by-line behavior. -- Do not add speculative documentation, abstractions, or metadata that the repository does not need. -- Do not use emoji or em dashes in new prose, comments, or reports. Preserve emoji and punctuation that are part of existing paths or quoted source text. - -## Delegation - -- Use the configured `researcher` agent for read-only exploration and research. -- Use the configured `primary-engineer` agent for implementation, debugging, and multi-step engineering work. If it is unavailable, use the narrowest suitable implementation agent. -- Implementation agents own TDD tests; use the `tester` agent for independent verification before completion. -- Delegate context-heavy or tool-heavy work with a clear scope, completion criteria, and evidence requirements. -- Require condensed handoffs containing conclusions, changed paths, verification, and uncertainty. Do not paste raw logs into the main context. -- The main context retains final decisions and integration responsibility. Agent messages are evidence, not user approval. - -## Project knowledge - -- `CLAUDE.md` is the project map for conventions, verified commands, boundaries, and gotchas. -- `docs/specs/lead-scoring-customer-pilot.md` is the approved source of truth for Lead Scoring Customer Pilot product behavior and build boundaries. -- `stack.md` is the primary record for technology decisions and unresolved stack questions; it cannot override the approved spec. -- `docs/` contains shared project knowledge intended for version control. -- `/tmp/` is for scratch data, raw logs, and temporary handoffs. -- Released Pydantic models, generated OpenAPI/JSON Schemas, tests, and shared fixtures become the exact executable contract when they exist. - -## Documentation - -- Put temporary reports, raw logs, and scratch material in `/tmp/`. -- Put durable team knowledge in `docs/` and include it in version control. -- Use `vault/` only for durable project-local knowledge that must not be committed. -- New durable Markdown documents under `docs/` or `vault/` use minimal YAML frontmatter: - -```yaml ---- -title: Document title -description: A short description of what the document contains and when it is useful. -status: draft -tags: [] ---- -``` - -- `title` and `description` are required. For governed specs, `status` may be `draft`, `review_ready`, `approved`, or `deprecated`; other existing document schemas keep their established status vocabulary. `tags` is optional. -- Do not add metadata that Git, the filesystem, or existing tooling can derive. -- Preserve the format and metadata schema of existing documents. Do not add frontmatter, reformat, move, or normalize existing files unless the task explicitly requests migration. -- `CLAUDE.md`, README files, and files with an existing schema keep their established format. - -## Repository structure - -- `src/lead_scoring/`: Lead Scoring production service package and CLI tools. -- `alembic/`: database migrations for PostgreSQL. -- `tests/`: test suite including health check, OpenAPI CLI, and migration lifecycle. -- `docs/specs/lead-scoring-customer-pilot.md`: approved Lead Scoring Customer Pilot specification. -- `docs/`: durable shared project documentation. -- `stack.md`: technology decisions and unresolved stack questions. -- `.claude/settings.json`: project hook configuration. -- `.claude/hooks/post-edit-check.py`: Python-only PostToolUse feedback hook. - -## Product and frontend boundary - -The frontend boundary is approved for Customer Pilot: DotB-Omni owns all customer-facing Lead Scoring configuration and HITL UI. AI Platform may provide only an internal Dev Console for fixtures, contract testing, failure testing, and allowlisted integration verification; it must not duplicate Omni business UI. - -When the internal Dev Console is implemented: - -- Use Svelte 5 and TypeScript with Bun, Vite, and Vitest. Add SvelteKit only when routing, SSR, or deployment requires it. -- Generate types from backend OpenAPI and consume them through `openapi-fetch`. -- Keep shared transport concerns such as authentication and error handling outside generated code. -- Use the official Svelte tooling, including `@sveltejs/mcp` autofixer feedback and `svelte-check`. -- Use Playwright only for important end-to-end user flows. - -TypeScript 7 native compiler and language server are pilot candidates, not the adopted project default. Test them project-locally before changing global runtimes or the shared `mcpls` configuration. - -## Engineering principles - -- Follow YAGNI. Implement only what the current requirement needs; do not add speculative abstractions, extension points, compatibility layers, or infrastructure. -- Prefer the simplest design that preserves clear boundaries and can be changed safely. -- Reuse the standard library and established, actively maintained packages when they reduce code and operational burden. Avoid dependencies for trivial behavior. -- Keep code explicit. Use precise names, short control flow, and direct data transformations. Avoid cleverness, hidden side effects, global mutable state, and premature optimization. -- Prefer composition over inheritance and functions over classes. Introduce a class only when state, lifecycle, or a framework contract genuinely requires one. -- Keep a functional core with side effects at the boundaries. Isolate I/O, persistence, network calls, clocks, and environment access. -- Organize modules by capability or domain responsibility, not by arbitrary technical layers. -- Avoid wrappers that only rename another API. Every abstraction must remove meaningful complexity or enforce a real boundary. -- Preserve one clear source of truth for contracts, configuration, and domain rules. - -## AI-native code - -Write code that humans and coding agents can understand with minimal hidden context: - -- Make dependencies, inputs, outputs, errors, and side effects explicit. -- Use typed, stable contracts at module and service boundaries. -- Keep related implementation and tests easy to locate. -- Prefer deterministic behavior and small composable functions. -- Document decisions and non-obvious constraints, not syntax or line-by-line behavior. -- Match existing conventions before introducing a new pattern. -- Make the smallest coherent change and remove superseded code instead of leaving parallel implementations. - -## Verified commands - -- `uv sync`: sync workspace dependencies and environment. -- `uv run pytest`: run pytest test suite against Python 3.13. -- `uv run ruff check src tests`: run lint checks across source and tests. -- `uv run ruff format --check src tests`: check code formatting. -- `uv run pyright`: run Pyright static type checker. -- `uv run ty check`: run project-local ty type checker. -- `uv run generate-openapi`: deterministically generate root `openapi.json`. -- `uv run generate-openapi --check`: verify that `openapi.json` is present and matches the current backend contract. -- `uv run generate-webhook-contract`: deterministically generate terminal webhook contract pack. -- `uv run generate-webhook-contract --check`: verify that webhook contract pack is present and matches current models. -- `uv run generate-polling-contract`: deterministically generate scoring run polling contract pack. -- `uv run generate-polling-contract --check`: verify that polling contract pack is present and matches current models. -- `uv run alembic upgrade head`: apply database migrations up to head. -- `uv run alembic downgrade base`: revert all database migrations. - -## Deterministic generation - -- When a verified project generator exists, use it instead of manually editing derived files. -- Treat OpenAPI output and generated frontend types as derived artifacts. Regenerate them from the backend contract and require the authoritative verification command to detect drift. -- Do not edit generated files manually unless the project workflow explicitly requires it. - -## Safety and scope - -- Inspect a path before overwriting or deleting it. Preserve existing configuration, documentation, code, and data unless the task explicitly authorizes removal or migration. -- Do not expose secrets in logs, reports, handoffs, or generated artifacts. -- Do not install packages, modify system configuration, or create permissions and background services without explicit approval. -- Bootstrap production code only after the relevant product and implementation decision. Customer-facing Lead Scoring UI remains owned by DotB-Omni. - -## Tooling and verification gotchas - -- Generated OpenAPI and frontend build output must remain covered by the existing ignore policy and should not be edited manually. -- The project PostToolUse hook is configured in `.claude/settings.json` for `Write` and `Edit`. It currently handles Python files only and invokes the existing `ruff` and `ty` tools. Preserve the pre-existing working-tree modification to `.claude/hooks/post-edit-check.py` unless explicitly asked to change or reset it. -- Do not modify `/etc`, package-manager state, system services, drivers, kernel settings, global Node/TypeScript versions, or the shared `mcpls` configuration as part of repository work unless explicitly approved. - -## Quality of life - -Pyright LSP is available for semantic navigation. Use it for definitions, references, hover types, document and workspace symbols, and call hierarchy; then read the relevant implementation to understand business rules and side effects. +Follow `AGENTS.md` for repository-wide instructions and workflow. diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..d4e6163 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,34 @@ +# Context + +Canonical language for the AI Platform Lead Scoring provider. The implementation described here is planned, not verified as working. The provider specification `docs/specs/ai-platform-lead-scoring.md` is a `draft/review_ready`, pending approval. + +## Systems and ownership + +- **AI Platform:** provider service for bounded AI profiles, rule execution, scoring-run lifecycle, result retrieval, and terminal notification. +- **DotB-Omni / Omni:** customer-facing authority for configured rules and weights, HITL policy and actions, caller-side authorization, lead identity/snapshot construction, and CRM writeback. Omni reads details from the provider after a signed notification. +- **DotB EMS:** CRM system of record for leads. +- **AIHub:** external AI gateway expected to validate/use V-Key and own model access, quota, and provider behavior. Its protocol and integration have not yet been independently verified; never infer endpoints, headers, or payloads. + +## Domain terms + +- **Tenant:** customer/account boundary used to scope credentials, idempotency keys, and run access. A run lookup must remain inside its authenticated tenant. +- **Tenant key:** service credential used to authenticate a tenant/caller at the provider boundary, subject to final agreed provisioning contract. It is distinct from the raw **V-Key** used for AIHub access. Do not treat the terms as interchangeable. +- **V-Key:** sensitive AIHub credential material. Target rule: keep raw value in memory only for the active execution attempt; never persist, log, trace, or send it to the browser. Source and exact forwarding protocol are unresolved. +- **Canonical snapshot:** bounded input assembled by Omni from the lead and permitted evidence. It is an input snapshot, not an AI Platform-owned CRM record. +- **Scoring configuration:** customer-authored rule/weight and HITL configuration owned by Omni. The provider may consume an immutable version with the request for reproducibility; it does not become the customer configuration authority. +- **AI profile:** provider execution choice `light`, `medium`, or `heavy`. These are three bounded orchestration modes, not confirmed AIHub-native offerings. Concrete models, passes, limits, and timeouts are unresolved. +- **Scoring run:** one provider execution for one tenant, request UUID, subject, snapshot, configuration version, and AI profile. The server-generated run UUID identifies the persisted run. +- **Request UUID / idempotency identity:** client-provided UUID included in the request body, scoped to a tenant. Same UUID and same canonical body replays the existing run; same UUID and different body conflicts. A new scoring attempt uses a new UUID. Exact fingerprint normalization remains contract work. +- **Result GET:** tenant-authenticated read of the authoritative run result. Omni calls it after receiving a signed terminal notification. +- **HITL (human-in-the-loop):** human review workflow and customer actions owned by Omni. The provider does not authorize end users or own the review interface. +- **Terminal notification:** provider-to-Omni signed HMAC webhook indicating a run outcome. It is a notification, not the result source of truth; Omni GETs the run. HMAC envelope, replay window, and retry protocol are unresolved. +- **Recovery:** after interruption of the single-process runtime, terminalize work that cannot continue because its volatile task/V-Key was lost. Recovery is not resumption and does not imply automatic re-execution. +- **Retention:** proposed deletion/redaction of sensitive run material after 90 days. Backup deletion, audit retention, and tenant offboarding behavior need explicit approval. + +## Disambiguation + +- **Rules and weights** are customer policy owned by Omni; **rule execution** is provider behavior owned by AI Platform. +- **Customer action** means Omni-authorized configuration/review/initiation behavior; an API caller's identity does not establish end-user permission. +- **Webhook delivery state** is separate from scoring-run state. A notification failure does not by itself rewrite a completed scoring result. +- **Idempotency** prevents duplicate admission for a body UUID; it does not guarantee exactly-once AIHub invocation or webhook delivery. +- **Draft/review_ready** means prepared for stakeholder review only. It does not mean approved, released, or implemented. diff --git a/README.md b/README.md index 567fce0..d36059e 100644 --- a/README.md +++ b/README.md @@ -1,133 +1,71 @@ -# AI Platform - Lead Scoring Service +# AI Platform -Lead Scoring service and internal Dev Console for the AI Platform. +New AI Platform repository scaffold. The backend and SvelteKit implementation are being developed concurrently; this document records the intended local workflow, not a claim that every command already works. -## What runs locally +## Runtime layout -| Component | Purpose | Default address | -| --- | --- | --- | -| PostgreSQL 16 | Stores scoring runs and migrations | `localhost:5433` | -| Lead Scoring API | REST API, async scoring worker, OpenAPI | `http://localhost:8000` | -| Dev Console | Internal fixture and contract-testing UI | `http://localhost:5173` | -| AIHub | Optional external AI inference dependency | `http://localhost:8080` | +- Python 3.13 backend package: `src/ai_platform`, served as `ai_platform.main:app`. +- PostgreSQL 16 with a new, project-specific Compose volume named `ai_platform_pgdata`. +- Alembic schema changes are applied as a one-shot Compose service before the backend starts. +- SvelteKit is a separate web image under `apps/web`. +- `old_repo/` is archived source only and is excluded from Docker build contexts and local checks. -The service still runs if local AIHub is unavailable: the default config uses `rules_only_review` fallback. A reachable AIHub and a valid tenant VKey are required for live AI scoring. +The local Compose stack runs one backend process. It is for development only; default credentials and loopback port mappings are not deployment settings. Keep production secrets outside `.env` and inject them through the deployment environment or a secrets manager. -## Requirements +## Local setup -Install before starting: +Copy the backend/Compose template and the separate SvelteKit server-only template. Both `.env` files are ignored by Git; the previous root `.env` is preserved under `old_repo/.env` as historical local data, not loaded by the rebuilt service. -- Git -- Python 3.13 or newer -- [uv](https://docs.astral.sh/uv/) -- [Bun](https://bun.sh/) -- Docker with Docker Compose plugin - -## Start from a fresh clone - -```bash -git clone https://github.com/HungTran24/dotb_ai-platform.git -cd dotb_ai-platform +```sh cp .env.example .env -cp apps/dev-console/.env.example apps/dev-console/.env +cp apps/web/.env.example apps/web/.env +make install ``` -The copied `.env` files are local only and ignored by Git. Their example values are intentionally for development only. +Start the database and run the backend and web servers in separate terminals: -### 1. Start PostgreSQL - -```bash -docker compose up -d postgres -docker compose ps +```sh +make db-up +make migrate +make backend-dev +make web-dev ``` -The included Compose service exposes PostgreSQL on port `5433` to avoid conflicting with other local projects. If port `5433` is occupied, choose another port and update both values before migration: +The local web server should call the backend through server-only configuration. Do not expose `BACKEND_INTERNAL_URL`, database credentials, or provider keys as browser-visible `PUBLIC_*` variables. -```bash -POSTGRES_PORT=15432 docker compose up -d postgres -# Then set DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:15432/lead_scoring in .env +Compose can instead build and start all services, including the one-shot migration step: + +```sh +docker compose up --build ``` -### 2. Install backend dependencies and migrate the database +The database is reachable from the host at `127.0.0.1:55432`, backend at `127.0.0.1:8000`, and web at `127.0.0.1:3000` by default. To discard only this new database and its data, run `docker compose down -v`; this does not target archived or unrelated volumes. -```bash -uv sync -uv run alembic upgrade head +The local database starts with only the development database. To run tests safely, create a separate database in that same new PostgreSQL instance, then pass its URL explicitly: + +```sh +docker compose exec postgres createdb -U ai_platform ai_platform_test +AI_PLATFORM_DATABASE_URL=postgresql+asyncpg://ai_platform:local-change-this-password@127.0.0.1:55432/ai_platform_test make test ``` -### 3. Start the Lead Scoring API +`make test` rejects a database URL that does not contain `ai_platform_test`. CI provisions its own isolated PostgreSQL 16 test database. -Run in a separate terminal from the repository root: +## Quality checks -```bash -uv run uvicorn lead_scoring.main:app --reload --host 0.0.0.0 --port 8000 +The opt-in pre-commit runner is installed by `make install` as a project-local development dependency. Run: + +```sh +make check-fast +make check ``` -Check the API: +`make check-fast` runs the pinned Ruff and pre-commit-hooks rules. `make check` additionally runs Python type/import checks and tests; tests must use a separate test database, never a developer or production database. CI uses the same fast hooks and the Makefile test/check commands with an isolated PostgreSQL 16 service. -```bash -curl http://localhost:8000/health -``` +## Operations notes -OpenAPI UI: `http://localhost:8000/docs` +- Apply migrations as a release step; application startup must not migrate the database. +- `/health/live` is liveness; `/health/ready` checks readiness and PostgreSQL. The Compose backend health check waits for readiness. +- Avoid logging credentials, request payloads containing personal data, or provider responses with sensitive content. +- The Compose PostgreSQL volume is disposable development data. Backup, restore, retention, secrets, TLS, and alerting must be defined for the eventual deployment target. -### 4. Start the Dev Console - -Run in a second terminal: - -```bash -cd apps/dev-console -bun install --frozen-lockfile -bun run dev -``` - -Open `http://localhost:5173`. - -## Local configuration - -Root `.env` configures the backend. `apps/dev-console/.env` configures the Vite development server. - -| Variable | Default | Purpose | -| --- | --- | --- | -| `DATABASE_URL` | PostgreSQL at `localhost:5433` | Backend database connection | -| `AIHUB_BASE_URL` | `http://localhost:8080` | Optional AIHub endpoint | -| `DEV_CONSOLE_API_URL` | `http://localhost:8000` | Dev Console API proxy target | -| `DEV_CONSOLE_PORT` | `5173` | Dev Console development port | -| `DEV_CONSOLE_PREVIEW_PORT` | `4173` | Dev Console preview port | -| `DEV_CONSOLE_SERVICE_TOKEN` | `local-dev-token` | Must match the local backend `SERVICE_CALLERS` entry | -| `DEV_CONSOLE_TEST_VKEY` | `local-test-vkey` | Forwarded only to AIHub calls; replace for real AIHub use | - -The local backend template includes an allowlisted `dev-console` caller and baseline scoring config version `1.0.0`, so the Dev Console can open immediately after startup. - -## Stop local services - -```bash -docker compose down -``` - -To remove the local database data too: - -```bash -docker compose down -v -``` - -## Verification - -Backend: - -```bash -uv run pytest -uv run ruff check src tests -uv run ruff format --check src tests -uv run pyright -uv run ty check -``` - -Frontend: - -```bash -cd apps/dev-console -bun run check -bun run test -bun run build -``` +The web container currently assumes `package.json`, `bun.lock`, a production SvelteKit build at `build/index.js`, and Bun compatibility with the selected SvelteKit adapter. Confirm those assumptions with the web implementation before treating the image as verified. diff --git a/docs/specs/ai-platform-lead-scoring.md b/docs/specs/ai-platform-lead-scoring.md new file mode 100644 index 0000000..d25d85c --- /dev/null +++ b/docs/specs/ai-platform-lead-scoring.md @@ -0,0 +1,43 @@ +--- +title: AI Platform Lead Scoring v2 +description: Hành vi nhà cung cấp Lead Scoring và ranh giới với DotB-Omni trong bản rebuild. +status: review_ready +--- + +# Lead Scoring v2 + +Đặc tả này thay thế hành vi Pilot trong `old_repo/`. API còn phải được DotB-Omni kiểm tra bằng vài fixture chung trước khi coi là hợp đồng tích hợp đã phát hành. Các quyết định dưới đây đã được chốt trong kế hoạch rebuild; các giới hạn hạ tầng/AIHub chưa xác minh được ghi riêng ở cuối. + +## Sở hữu + +- DotB-Omni tạo snapshot, định nghĩa rules/points và rule/AI weights, score bands, HITL/review/override, quyền người dùng và hành động CRM. Omni gửi tenant key và V-Key từ backend của mình, không từ browser. +- AI Platform xác thực tenant, thực thi rules của Omni, gọi AIHub khi cần, tính rule_score/ai_score/final_score, lưu run và gửi thông báo kết thúc. Admin Web nội bộ chỉ quản lý AI profile và xem/chạy fixtures; không quản lý customer rules/review. +- DotB EMS là nguồn sự thật CRM. Omni nhận webhook terminal rồi GET chi tiết run trước khi ghi EMS. Webhook không chứa snapshot, V-Key hay AI evidence. + +## Admission và idempotency + +`POST /v1/lead-scoring/runs` nhận `Authorization: Bearer `, `Idempotency-Key: ` và `X-Tenant-VKey` tùy chọn. Body chứa `request_id` cùng UUID, `subject_ref`, snapshot, app-defined scoring configuration (`version`, rules/points, `rule_weight`, `ai_weight`) và AI profile. Tenant được suy ra từ key xác thực, không từ body. `GET /v1/lead-scoring/runs/{run_id}` chỉ trả dữ liệu của tenant đó. + +- Fingerprint SHA-256 của canonical **body Omni**; không bao gồm tenant key, V-Key hoặc AI profile do server chọn. Lookup `(tenant, request_id)` trước khi chọn profile. Cùng key/body trả run cũ mà không schedule AI; cùng key/body khác trả 409. PostgreSQL unique constraint giải quyết concurrent requests; retry admission đã commit nhưng mất response dùng cùng UUID/body. +- Run mới đóng băng AI profile đang active, INSERT + COMMIT trước khi schedule và trả 202; `Location` trỏ đến GET. Nếu không schedule được, run thành failed và response phản ánh trạng thái thực, không để pending giả. Chấm lại hoặc thử profile mới dùng UUID mới. Không reuse UUID quá 90 ngày: run và fingerprint bị xóa, không có tombstone. +- Validate chặt loại rule/operator/value, tổng weights bằng 1, score/points và dữ liệu snapshot; đầu vào sai trả 422. Snapshot tối đa 10 conversations, 50 messages, 20.000 ký tự nội dung và 90 ngày evidence; giữ thứ tự deterministic. Kích thước toàn body có hard cap. Không hard-code rule giáo dục hay category/action/review state. + +## Kết quả và lỗi + +- `ai_weight=0`: `rules_only`, `degraded=false`, `ai_score=null`, không gọi AIHub và không cần V-Key. +- `ai_weight>0` nhưng thiếu V-Key hoặc AIHub lỗi: `rules_only_fallback`, `degraded=true`, `ai_score/ai_confidence=null`, `final_score=rule_score`, `error_code` đã chuẩn hóa. Không tự AI retry. +- AI thành công: `hybrid`, `degraded=false`, `final_score=clamp(round(rule_score*rule_weight + ai_score*ai_weight), 0, 100)`; trả breakdown, reasoning/evidence/confidence được giới hạn. Cùng raw signals có thể ảnh hưởng cả rules lẫn AI; không gửi app rules/points vào AI prompt để hạn chế chấm hai lần. +- Bug/code/DB failure thành `failed`, không giả là AI fallback. AI profile gồm versioned model, prompts và timeout/token limits, không sở hữu app rules hoặc weights. Medium two-pass là mặc định; Light/Heavy chỉ bổ sung khi có nhu cầu và được xác minh với AIHub. + +## Key, session và delivery + +- Platform có một active tenant key/tenant dùng chung cho LS và feature sau này: provisioner secret server-side cấp qua `POST /v1/platform/tenant-keys`, raw trả một lần, DB chỉ có verifier/digest; cấp trùng 409. Nếu response mất sau commit, admin CLI rotate cũ/thay mới atomically; không tự rotate khi POST bị replay. +- `X-Tenant-VKey` chỉ giữ trong RAM của current scoring task và forward `Authorization: Bearer` tới AIHub. Không persist/log/trace/error/browser. V-Key không phải platform authentication. Admin session độc lập, password hash, CSRF cho mutation, cookie bảo vệ, logout/expiry và audit thay đổi config/key không kèm secrets. +- Một backend process + asyncio tasks. Startup trước readiness terminalize pending/running cũ thành `failed/worker_interrupted`; task exception cũng terminalize. V-Key không durable nên không resume AI sau crash; caller muốn làm lại dùng UUID mới. +- Terminal transition và PostgreSQL outbox event commit cùng transaction. Dispatcher gửi HMAC-signed webhook tới một Omni URL cố định với event ID/timestamp, bounded retry và dead-letter. Omni xác minh chữ ký/chống replay/dedupe, GET chi tiết và áp dụng writeback theo run mới nhất của `(tenant, subject)`; delivery replay không chấm AI lại. Không broker, dynamic subscription, polling scheduler tại Omni. + +## Privacy và nghiệm thu + +- Scheduled retention xóa run, snapshot, fingerprint, evidence và outbox liên quan sau 90 ngày; test xóa thật. Không lưu thông tin nhạy cảm trong payload webhook/log. Chính sách xóa tenant/offboarding và vòng đời backup phải chốt trước production; xóa DB không có nghĩa xóa ngay backup. +- Fixture chung tối thiểu: admission/replay và 409, rules-only/hybrid/fallback, failed run, webhook HMAC và Omni GET/writeback. Generated OpenAPI là nguồn schema; không lặp lại bộ schema cho từng projection không có consumer. +- Cần xác minh bằng sandbox AIHub về endpoint, strict structured output, model, timeout, quota và lỗi; Omni phải xác nhận fixture, signature/replay và writeback. Hosting, TLS, secret injection, backup/restore và alert destination chốt theo nơi deploy. Chưa khẳng định production-ready khi các điều đó chưa được kiểm chứng. diff --git a/old_repo/.env.example b/old_repo/.env.example new file mode 100644 index 0000000..a76635e --- /dev/null +++ b/old_repo/.env.example @@ -0,0 +1,19 @@ +# Local development only. Replace all credentials in deployed environments. +ENVIRONMENT=development +DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5433/lead_scoring +AIHUB_BASE_URL=http://localhost:8080 +DEBUG=false + +# JSON arrays consumed by pydantic-settings. These values work with the Dev Console example. +SERVICE_CALLERS=[{"caller_id":"dev-console","token":"local-dev-token","allowed_apps":["dev-console"],"allowed_features":["lead-scoring"],"allowed_tenants":["omni-account-42","omni-test-tenant-42"]}] +TENANT_ALLOWLIST=["omni-account-42","omni-test-tenant-42"] +SCORING_CONFIGS=[{"version":"1.0.0","confidence":0.8,"confidence_threshold":0.7,"hitl_policy_version":"1.0.0","effort_profile":"medium","fallback_policy":"rules_only_review","ai_model":"openai/gpt-4o-mini"}] + +WORKER_LEASE_SECONDS=30 +WORKER_RECOVERY_INTERVAL_SECONDS=5 +KILL_SWITCH_ENABLED=false + +# Enable webhook delivery by setting both values. +# WEBHOOK_SUBSCRIBER_URL=http://localhost:8080/api/v1/internal/lead-scoring/events +# WEBHOOK_SIGNING_SECRET=replace-with-local-signing-secret +WEBHOOK_SIGNING_KEY_ID=pilot-2026-01 diff --git a/old_repo/.gitignore b/old_repo/.gitignore new file mode 100644 index 0000000..fe13e33 --- /dev/null +++ b/old_repo/.gitignore @@ -0,0 +1,76 @@ +# Python & Virtual Environments +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +.venv/ +venv/ +ENV/ +env/ +.uv/ + +# Linters, Type Checkers & Test Caches +.ruff_cache/ +.pytest_cache/ +.mypy_cache/ +.pyre/ +.pytype/ +.pyright/ +.coverage +.coverage.* +htmlcov/ +nosetests.xml +coverage.xml +*.cover +*.py,cover + +# Node / Bun / Frontend +node_modules/ +node_modules +apps/dev-console/node_modules +.bun/ +dist-ssr/ +*.local +.svelte-kit/ + +# Playwright & Testing Reports +playwright-report/ +test-results/ +blob-report/ + +# Environment Variables & Local Secrets +.env +.env.* +!.env.example + +# Project-local +vault/ +.scratch/ +.codex/ +docs/agents/ +.claude/ +CLAUDE.LOCAL.md +# Generated artifacts +/openapi.json + +# IDE, OS & Editor Artifacts +.DS_Store +Thumbs.db +*.swp +*.swo +*~ diff --git a/old_repo/.python-version b/old_repo/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/old_repo/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/old_repo/AGENTS.md b/old_repo/AGENTS.md new file mode 100644 index 0000000..85982db --- /dev/null +++ b/old_repo/AGENTS.md @@ -0,0 +1,172 @@ +# AGENTS.md + +This file provides guidance to Codex (Codex.ai/code) when working with this repository. + +## Purpose and status + +This repository is the Python-first AI Platform workspace. Lead Scoring is the first service; related systems are `DotB-Omni`, `lead-scoring-agent`, and `aihub` under `/home/hung/code/dotb_workspace/`. + +The repository contains specifications, project documentation, and the initialized baseline for the Lead Scoring service (`src/lead_scoring/`). + +Before implementing or changing Lead Scoring Customer Pilot behavior, read: + +- `docs/specs/lead-scoring-customer-pilot.md`: authoritative product behavior, scope, invariants, acceptance boundaries, and cross-service ownership. +- `stack.md`: technology decisions, candidates, evidence, and unresolved stack questions. It does not override the approved specification. + +The DotB Vault RFC, initial brief, Grill Brief, legacy reports, drafts, and agent messages are historical/supporting context rather than implementation authority. Exact wire formats become authoritative through released Pydantic models, generated OpenAPI/JSON Schemas, and shared fixtures. + +## Communication + +- Keep responses short, specific, and decision-oriented. +- Put the decision or conclusion first, followed by only the evidence and next action needed. +- Keep the main context high-signal. Put raw logs, scratch data, and long intermediate reports in `/tmp/` or the appropriate durable document. +- Use lists and compact structure when they make the result easier to scan. +- State assumptions, unresolved decisions, and verification status explicitly. + +## Writing + +- Write directly and precisely. Avoid buzzwords, filler, and repeated explanations. +- Comments explain intent, invariants, trade-offs, or non-obvious constraints, not syntax or line-by-line behavior. +- Do not add speculative documentation, abstractions, or metadata that the repository does not need. +- Do not use emoji or em dashes in new prose, comments, or reports. Preserve emoji and punctuation that are part of existing paths or quoted source text. + +## Delegation + +- Prioritize standard subagents over teammates to minimize token overhead. Spawn multiple subagents when parallel execution is genuinely needed. +- Pick the agent type matching the task from available configured agents (e.g., `explorer`, `implementer`, `tester`, `researcher`, `adversarial-reviewer`, `general`, `codebase-memory`). +- Delegate context-heavy or tool-heavy work with a clear scope, completion criteria, and evidence requirements. +- Require condensed handoffs containing conclusions, changed paths, verification, and uncertainty. Do not paste raw logs into the main context. +- The main context retains final decisions and integration responsibility. Agent messages are evidence, not user approval. + +## Agent skills + +### Issue tracker + +Local Markdown issues are stored under `.scratch//`. See `docs/agents/issue-tracker.md`. + +### Domain docs + +The repository uses a single-context domain layout. See `docs/agents/domain.md`. + +## Project knowledge + +- `AGENTS.md` is the project map for conventions, verified commands, boundaries, and gotchas. +- `docs/specs/lead-scoring-customer-pilot.md` is the approved source of truth for Lead Scoring Customer Pilot product behavior and build boundaries. +- `stack.md` is the primary record for technology decisions and unresolved stack questions; it cannot override the approved spec. +- `docs/` contains shared project knowledge intended for version control. +- `/tmp/` is for scratch data, raw logs, and temporary handoffs. +- Released Pydantic models, generated OpenAPI/JSON Schemas, tests, and shared fixtures become the exact executable contract when they exist. + +## Documentation + +- Put temporary reports, raw logs, and scratch material in `/tmp/`. +- Put durable team knowledge in `docs/` and include it in version control. +- Use `vault/` only for durable project-local knowledge that must not be committed. +- New durable Markdown documents under `docs/` or `vault/` use minimal YAML frontmatter: + +```yaml +--- +title: Document title +description: A short description of what the document contains and when it is useful. +status: draft +tags: [] +--- +``` + +- `title` and `description` are required. For governed specs, `status` may be `draft`, `review_ready`, `approved`, or `deprecated`; other existing document schemas keep their established status vocabulary. `tags` is optional. +- Do not add metadata that Git, the filesystem, or existing tooling can derive. +- Preserve the format and metadata schema of existing documents. Do not add frontmatter, reformat, move, or normalize existing files unless the task explicitly requests migration. +- `AGENTS.md`, README files, and files with an existing schema keep their established format. + +## Repository structure + +- `src/lead_scoring/`: Lead Scoring production service package and CLI tools. +- `alembic/`: database migrations for PostgreSQL. +- `tests/`: test suite including health check, OpenAPI CLI, and migration lifecycle. +- `docs/specs/lead-scoring-customer-pilot.md`: approved Lead Scoring Customer Pilot specification. +- `docs/`: durable shared project documentation. +- `.scratch/`: local untracked draft specs and issue tickets used by agent skills. +- `stack.md`: technology decisions and unresolved stack questions. +- `.codex/settings.json`: project hook configuration. +- `.codex/hooks/post-edit-check.py`: Python-only PostToolUse feedback hook. + +## Product and frontend boundary + +The frontend boundary is approved for Customer Pilot: DotB-Omni owns all customer-facing Lead Scoring configuration and HITL UI. AI Platform may provide only an internal Dev Console for fixtures, contract testing, failure testing, and allowlisted integration verification; it must not duplicate Omni business UI. + +When the internal Dev Console is implemented: + +- Use Svelte 5 and TypeScript with Bun, Vite, and Vitest. Add SvelteKit only when routing, SSR, or deployment requires it. +- Generate types from backend OpenAPI and consume them through `openapi-fetch`. +- Keep shared transport concerns such as authentication and error handling outside generated code. +- Use the official Svelte tooling, including `@sveltejs/mcp` autofixer feedback and `svelte-check`. +- Use Playwright only for important end-to-end user flows. + +TypeScript 7 native compiler and language server are pilot candidates, not the adopted project default. Test them project-locally before changing global runtimes or the shared `mcpls` configuration. + +## Engineering principles + +- Follow YAGNI. Implement only what the current requirement needs; do not add speculative abstractions, extension points, compatibility layers, or infrastructure. +- Prefer the simplest design that preserves clear boundaries and can be changed safely. +- Reuse the standard library and established, actively maintained packages when they reduce code and operational burden. Avoid dependencies for trivial behavior. +- Keep code explicit. Use precise names, short control flow, and direct data transformations. Avoid cleverness, hidden side effects, global mutable state, and premature optimization. +- Prefer composition over inheritance and functions over classes. Introduce a class only when state, lifecycle, or a framework contract genuinely requires one. +- Keep a functional core with side effects at the boundaries. Isolate I/O, persistence, network calls, clocks, and environment access. +- Organize modules by capability or domain responsibility, not by arbitrary technical layers. +- Avoid wrappers that only rename another API. Every abstraction must remove meaningful complexity or enforce a real boundary. +- Preserve one clear source of truth for contracts, configuration, and domain rules. + +## AI-native code + +Write code that humans and coding agents can understand with minimal hidden context: + +- Make dependencies, inputs, outputs, errors, and side effects explicit. +- Use typed, stable contracts at module and service boundaries. +- Keep related implementation and tests easy to locate. +- Prefer deterministic behavior and small composable functions. +- Document decisions and non-obvious constraints, not syntax or line-by-line behavior. +- Match existing conventions before introducing a new pattern. +- Make the smallest coherent change and remove superseded code instead of leaving parallel implementations. + +## Verified commands + +- `uv sync`: sync workspace dependencies and environment. +- `uv run pytest`: run pytest test suite against Python 3.13. +- `uv run ruff check src tests`: run lint checks across source and tests. +- `uv run ruff format --check src tests`: check code formatting. +- `uv run pyright`: run Pyright static type checker. +- `uv run ty check`: run project-local ty type checker. +- `uv run generate-openapi`: deterministically generate root `openapi.json`. +- `uv run generate-openapi --check`: verify that `openapi.json` is present and matches the current backend contract. +- `uv run generate-webhook-contract`: deterministically generate terminal webhook contract pack. +- `uv run generate-webhook-contract --check`: verify that webhook contract pack is present and matches current models. +- `uv run generate-polling-contract`: deterministically generate scoring run polling contract pack. +- `uv run generate-polling-contract --check`: verify that polling contract pack is present and matches current models. +- `uv run generate-contract-pack`: deterministically generate the versioned shared Contract Pack. +- `uv run generate-contract-pack --validate-fixtures`: validate provider and consumer fixture expectations. +- `uv run generate-contract-pack --check`: verify the versioned Contract Pack and checksums are present and current. +- `uv run alembic upgrade head`: apply database migrations up to head. +- `uv run alembic downgrade base`: revert all database migrations. + +## Deterministic generation + +- When a verified project generator exists, use it instead of manually editing derived files. +- Treat OpenAPI output and generated frontend types as derived artifacts. Regenerate them from the backend contract and require the authoritative verification command to detect drift. +- Do not edit generated files manually unless the project workflow explicitly requires it. + +## Safety and scope + +- Inspect a path before overwriting or deleting it. Preserve existing configuration, documentation, code, and data unless the task explicitly authorizes removal or migration. +- Do not expose secrets in logs, reports, handoffs, or generated artifacts. +- Do not install packages, modify system configuration, or create permissions and background services without explicit approval. +- Bootstrap production code only after the relevant product and implementation decision. Customer-facing Lead Scoring UI remains owned by DotB-Omni. + +## Tooling and verification gotchas + +- Root `openapi.json` and frontend build output remain covered by the ignore policy; the versioned Contract Pack OpenAPI artifact is generated and committed, and must not be edited manually. +- The project PostToolUse hook is configured in `.codex/settings.json` for `Write` and `Edit`. It handles Python files via `ruff` (safe fix and format). Full type-checking (`pyright`, `ty`) and test suites belong to explicit verification steps. Preserve the working-tree modification to `.codex/hooks/post-edit-check.py` unless explicitly asked to change or reset it. +- Do not modify `/etc`, package-manager state, system services, drivers, kernel settings, global Node/TypeScript versions, or the shared `mcpls` configuration as part of repository work unless explicitly approved. + +## Quality of life + +Pyright LSP is available for semantic navigation. Use it for definitions, references, hover types, document and workspace symbols, and call hierarchy; then read the relevant implementation to understand business rules and side effects. diff --git a/old_repo/CLAUDE.md b/old_repo/CLAUDE.md new file mode 100644 index 0000000..a767f37 --- /dev/null +++ b/old_repo/CLAUDE.md @@ -0,0 +1,159 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with this repository. + +## Purpose and status + +This repository is the Python-first AI Platform workspace. Lead Scoring is the first service; related systems are `DotB-Omni`, `lead-scoring-agent`, and `aihub` under `/home/hung/code/dotb_workspace/`. + +The repository contains specifications, project documentation, and the initialized baseline for the Lead Scoring service (`src/lead_scoring/`). + +Before implementing or changing Lead Scoring Customer Pilot behavior, read: + +- `docs/specs/lead-scoring-customer-pilot.md`: authoritative product behavior, scope, invariants, acceptance boundaries, and cross-service ownership. +- `stack.md`: technology decisions, candidates, evidence, and unresolved stack questions. It does not override the approved specification. + +The DotB Vault RFC, initial brief, Grill Brief, legacy reports, drafts, and agent messages are historical/supporting context rather than implementation authority. Exact wire formats become authoritative through released Pydantic models, generated OpenAPI/JSON Schemas, and shared fixtures. + +## Communication + +- Keep responses short, specific, and decision-oriented. +- Put the decision or conclusion first, followed by only the evidence and next action needed. +- Keep the main context high-signal. Put raw logs, scratch data, and long intermediate reports in `/tmp/` or the appropriate durable document. +- Use lists and compact structure when they make the result easier to scan. +- State assumptions, unresolved decisions, and verification status explicitly. + +## Writing + +- Write directly and precisely. Avoid buzzwords, filler, and repeated explanations. +- Comments explain intent, invariants, trade-offs, or non-obvious constraints, not syntax or line-by-line behavior. +- Do not add speculative documentation, abstractions, or metadata that the repository does not need. +- Do not use emoji or em dashes in new prose, comments, or reports. Preserve emoji and punctuation that are part of existing paths or quoted source text. + +## Delegation + +- Use the configured `researcher` agent for read-only exploration and research. +- Use the configured `primary-engineer` agent for implementation, debugging, and multi-step engineering work. If it is unavailable, use the narrowest suitable implementation agent. +- Implementation agents own TDD tests; use the `tester` agent for independent verification before completion. +- Delegate context-heavy or tool-heavy work with a clear scope, completion criteria, and evidence requirements. +- Require condensed handoffs containing conclusions, changed paths, verification, and uncertainty. Do not paste raw logs into the main context. +- The main context retains final decisions and integration responsibility. Agent messages are evidence, not user approval. + +## Project knowledge + +- `CLAUDE.md` is the project map for conventions, verified commands, boundaries, and gotchas. +- `docs/specs/lead-scoring-customer-pilot.md` is the approved source of truth for Lead Scoring Customer Pilot product behavior and build boundaries. +- `stack.md` is the primary record for technology decisions and unresolved stack questions; it cannot override the approved spec. +- `docs/` contains shared project knowledge intended for version control. +- `/tmp/` is for scratch data, raw logs, and temporary handoffs. +- Released Pydantic models, generated OpenAPI/JSON Schemas, tests, and shared fixtures become the exact executable contract when they exist. + +## Documentation + +- Put temporary reports, raw logs, and scratch material in `/tmp/`. +- Put durable team knowledge in `docs/` and include it in version control. +- Use `vault/` only for durable project-local knowledge that must not be committed. +- New durable Markdown documents under `docs/` or `vault/` use minimal YAML frontmatter: + +```yaml +--- +title: Document title +description: A short description of what the document contains and when it is useful. +status: draft +tags: [] +--- +``` + +- `title` and `description` are required. For governed specs, `status` may be `draft`, `review_ready`, `approved`, or `deprecated`; other existing document schemas keep their established status vocabulary. `tags` is optional. +- Do not add metadata that Git, the filesystem, or existing tooling can derive. +- Preserve the format and metadata schema of existing documents. Do not add frontmatter, reformat, move, or normalize existing files unless the task explicitly requests migration. +- `CLAUDE.md`, README files, and files with an existing schema keep their established format. + +## Repository structure + +- `src/lead_scoring/`: Lead Scoring production service package and CLI tools. +- `alembic/`: database migrations for PostgreSQL. +- `tests/`: test suite including health check, OpenAPI CLI, and migration lifecycle. +- `docs/specs/lead-scoring-customer-pilot.md`: approved Lead Scoring Customer Pilot specification. +- `docs/`: durable shared project documentation. +- `stack.md`: technology decisions and unresolved stack questions. +- `.claude/settings.json`: project hook configuration. +- `.claude/hooks/post-edit-check.py`: Python-only PostToolUse feedback hook. + +## Product and frontend boundary + +The frontend boundary is approved for Customer Pilot: DotB-Omni owns all customer-facing Lead Scoring configuration and HITL UI. AI Platform may provide only an internal Dev Console for fixtures, contract testing, failure testing, and allowlisted integration verification; it must not duplicate Omni business UI. + +When the internal Dev Console is implemented: + +- Use Svelte 5 and TypeScript with Bun, Vite, and Vitest. Add SvelteKit only when routing, SSR, or deployment requires it. +- Generate types from backend OpenAPI and consume them through `openapi-fetch`. +- Keep shared transport concerns such as authentication and error handling outside generated code. +- Use the official Svelte tooling, including `@sveltejs/mcp` autofixer feedback and `svelte-check`. +- Use Playwright only for important end-to-end user flows. + +TypeScript 7 native compiler and language server are pilot candidates, not the adopted project default. Test them project-locally before changing global runtimes or the shared `mcpls` configuration. + +## Engineering principles + +- Follow YAGNI. Implement only what the current requirement needs; do not add speculative abstractions, extension points, compatibility layers, or infrastructure. +- Prefer the simplest design that preserves clear boundaries and can be changed safely. +- Reuse the standard library and established, actively maintained packages when they reduce code and operational burden. Avoid dependencies for trivial behavior. +- Keep code explicit. Use precise names, short control flow, and direct data transformations. Avoid cleverness, hidden side effects, global mutable state, and premature optimization. +- Prefer composition over inheritance and functions over classes. Introduce a class only when state, lifecycle, or a framework contract genuinely requires one. +- Keep a functional core with side effects at the boundaries. Isolate I/O, persistence, network calls, clocks, and environment access. +- Organize modules by capability or domain responsibility, not by arbitrary technical layers. +- Avoid wrappers that only rename another API. Every abstraction must remove meaningful complexity or enforce a real boundary. +- Preserve one clear source of truth for contracts, configuration, and domain rules. + +## AI-native code + +Write code that humans and coding agents can understand with minimal hidden context: + +- Make dependencies, inputs, outputs, errors, and side effects explicit. +- Use typed, stable contracts at module and service boundaries. +- Keep related implementation and tests easy to locate. +- Prefer deterministic behavior and small composable functions. +- Document decisions and non-obvious constraints, not syntax or line-by-line behavior. +- Match existing conventions before introducing a new pattern. +- Make the smallest coherent change and remove superseded code instead of leaving parallel implementations. + +## Verified commands + +- `uv sync`: sync workspace dependencies and environment. +- `uv run pytest`: run pytest test suite against Python 3.13. +- `uv run ruff check src tests`: run lint checks across source and tests. +- `uv run ruff format --check src tests`: check code formatting. +- `uv run pyright`: run Pyright static type checker. +- `uv run ty check`: run project-local ty type checker. +- `uv run generate-openapi`: deterministically generate root `openapi.json`. +- `uv run generate-openapi --check`: verify that `openapi.json` is present and matches the current backend contract. +- `uv run generate-webhook-contract`: deterministically generate terminal webhook contract pack. +- `uv run generate-webhook-contract --check`: verify that webhook contract pack is present and matches current models. +- `uv run generate-polling-contract`: deterministically generate scoring run polling contract pack. +- `uv run generate-polling-contract --check`: verify that polling contract pack is present and matches current models. +- `uv run alembic upgrade head`: apply database migrations up to head. +- `uv run alembic downgrade base`: revert all database migrations. + +## Deterministic generation + +- When a verified project generator exists, use it instead of manually editing derived files. +- Treat OpenAPI output and generated frontend types as derived artifacts. Regenerate them from the backend contract and require the authoritative verification command to detect drift. +- Do not edit generated files manually unless the project workflow explicitly requires it. + +## Safety and scope + +- Inspect a path before overwriting or deleting it. Preserve existing configuration, documentation, code, and data unless the task explicitly authorizes removal or migration. +- Do not expose secrets in logs, reports, handoffs, or generated artifacts. +- Do not install packages, modify system configuration, or create permissions and background services without explicit approval. +- Bootstrap production code only after the relevant product and implementation decision. Customer-facing Lead Scoring UI remains owned by DotB-Omni. + +## Tooling and verification gotchas + +- Generated OpenAPI and frontend build output must remain covered by the existing ignore policy and should not be edited manually. +- The project PostToolUse hook is configured in `.claude/settings.json` for `Write` and `Edit`. It currently handles Python files only and invokes the existing `ruff` and `ty` tools. Preserve the pre-existing working-tree modification to `.claude/hooks/post-edit-check.py` unless explicitly asked to change or reset it. +- Do not modify `/etc`, package-manager state, system services, drivers, kernel settings, global Node/TypeScript versions, or the shared `mcpls` configuration as part of repository work unless explicitly approved. + +## Quality of life + +Pyright LSP is available for semantic navigation. Use it for definitions, references, hover types, document and workspace symbols, and call hierarchy; then read the relevant implementation to understand business rules and side effects. diff --git a/old_repo/README.md b/old_repo/README.md new file mode 100644 index 0000000..567fce0 --- /dev/null +++ b/old_repo/README.md @@ -0,0 +1,133 @@ +# AI Platform - Lead Scoring Service + +Lead Scoring service and internal Dev Console for the AI Platform. + +## What runs locally + +| Component | Purpose | Default address | +| --- | --- | --- | +| PostgreSQL 16 | Stores scoring runs and migrations | `localhost:5433` | +| Lead Scoring API | REST API, async scoring worker, OpenAPI | `http://localhost:8000` | +| Dev Console | Internal fixture and contract-testing UI | `http://localhost:5173` | +| AIHub | Optional external AI inference dependency | `http://localhost:8080` | + +The service still runs if local AIHub is unavailable: the default config uses `rules_only_review` fallback. A reachable AIHub and a valid tenant VKey are required for live AI scoring. + +## Requirements + +Install before starting: + +- Git +- Python 3.13 or newer +- [uv](https://docs.astral.sh/uv/) +- [Bun](https://bun.sh/) +- Docker with Docker Compose plugin + +## Start from a fresh clone + +```bash +git clone https://github.com/HungTran24/dotb_ai-platform.git +cd dotb_ai-platform +cp .env.example .env +cp apps/dev-console/.env.example apps/dev-console/.env +``` + +The copied `.env` files are local only and ignored by Git. Their example values are intentionally for development only. + +### 1. Start PostgreSQL + +```bash +docker compose up -d postgres +docker compose ps +``` + +The included Compose service exposes PostgreSQL on port `5433` to avoid conflicting with other local projects. If port `5433` is occupied, choose another port and update both values before migration: + +```bash +POSTGRES_PORT=15432 docker compose up -d postgres +# Then set DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:15432/lead_scoring in .env +``` + +### 2. Install backend dependencies and migrate the database + +```bash +uv sync +uv run alembic upgrade head +``` + +### 3. Start the Lead Scoring API + +Run in a separate terminal from the repository root: + +```bash +uv run uvicorn lead_scoring.main:app --reload --host 0.0.0.0 --port 8000 +``` + +Check the API: + +```bash +curl http://localhost:8000/health +``` + +OpenAPI UI: `http://localhost:8000/docs` + +### 4. Start the Dev Console + +Run in a second terminal: + +```bash +cd apps/dev-console +bun install --frozen-lockfile +bun run dev +``` + +Open `http://localhost:5173`. + +## Local configuration + +Root `.env` configures the backend. `apps/dev-console/.env` configures the Vite development server. + +| Variable | Default | Purpose | +| --- | --- | --- | +| `DATABASE_URL` | PostgreSQL at `localhost:5433` | Backend database connection | +| `AIHUB_BASE_URL` | `http://localhost:8080` | Optional AIHub endpoint | +| `DEV_CONSOLE_API_URL` | `http://localhost:8000` | Dev Console API proxy target | +| `DEV_CONSOLE_PORT` | `5173` | Dev Console development port | +| `DEV_CONSOLE_PREVIEW_PORT` | `4173` | Dev Console preview port | +| `DEV_CONSOLE_SERVICE_TOKEN` | `local-dev-token` | Must match the local backend `SERVICE_CALLERS` entry | +| `DEV_CONSOLE_TEST_VKEY` | `local-test-vkey` | Forwarded only to AIHub calls; replace for real AIHub use | + +The local backend template includes an allowlisted `dev-console` caller and baseline scoring config version `1.0.0`, so the Dev Console can open immediately after startup. + +## Stop local services + +```bash +docker compose down +``` + +To remove the local database data too: + +```bash +docker compose down -v +``` + +## Verification + +Backend: + +```bash +uv run pytest +uv run ruff check src tests +uv run ruff format --check src tests +uv run pyright +uv run ty check +``` + +Frontend: + +```bash +cd apps/dev-console +bun run check +bun run test +bun run build +``` diff --git a/old_repo/alembic.ini b/old_repo/alembic.ini new file mode 100644 index 0000000..39c4734 --- /dev/null +++ b/old_repo/alembic.ini @@ -0,0 +1,79 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = %(here)s/alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# file_template = %%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +prepend_sys_path = %(here)s +path_separator = os + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator +version_path_separator = os + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/alembic/env.py b/old_repo/alembic/env.py similarity index 100% rename from alembic/env.py rename to old_repo/alembic/env.py diff --git a/alembic/script.py.mako b/old_repo/alembic/script.py.mako similarity index 100% rename from alembic/script.py.mako rename to old_repo/alembic/script.py.mako diff --git a/alembic/versions/0001_baseline.py b/old_repo/alembic/versions/0001_baseline.py similarity index 100% rename from alembic/versions/0001_baseline.py rename to old_repo/alembic/versions/0001_baseline.py diff --git a/alembic/versions/0002_create_scoring_runs.py b/old_repo/alembic/versions/0002_create_scoring_runs.py similarity index 100% rename from alembic/versions/0002_create_scoring_runs.py rename to old_repo/alembic/versions/0002_create_scoring_runs.py diff --git a/alembic/versions/0003_rule_only_execution.py b/old_repo/alembic/versions/0003_rule_only_execution.py similarity index 100% rename from alembic/versions/0003_rule_only_execution.py rename to old_repo/alembic/versions/0003_rule_only_execution.py diff --git a/alembic/versions/0004_medium_ai_pipeline.py b/old_repo/alembic/versions/0004_medium_ai_pipeline.py similarity index 100% rename from alembic/versions/0004_medium_ai_pipeline.py rename to old_repo/alembic/versions/0004_medium_ai_pipeline.py diff --git a/alembic/versions/0005_review_api_and_audit.py b/old_repo/alembic/versions/0005_review_api_and_audit.py similarity index 100% rename from alembic/versions/0005_review_api_and_audit.py rename to old_repo/alembic/versions/0005_review_api_and_audit.py diff --git a/alembic/versions/0006_reevaluate_child_lineage.py b/old_repo/alembic/versions/0006_reevaluate_child_lineage.py similarity index 100% rename from alembic/versions/0006_reevaluate_child_lineage.py rename to old_repo/alembic/versions/0006_reevaluate_child_lineage.py diff --git a/alembic/versions/0007_outbox_delivery_state.py b/old_repo/alembic/versions/0007_outbox_delivery_state.py similarity index 100% rename from alembic/versions/0007_outbox_delivery_state.py rename to old_repo/alembic/versions/0007_outbox_delivery_state.py diff --git a/alembic/versions/0008_retention_redaction_state.py b/old_repo/alembic/versions/0008_retention_redaction_state.py similarity index 100% rename from alembic/versions/0008_retention_redaction_state.py rename to old_repo/alembic/versions/0008_retention_redaction_state.py diff --git a/alembic/versions/0009_execution_trace.py b/old_repo/alembic/versions/0009_execution_trace.py similarity index 100% rename from alembic/versions/0009_execution_trace.py rename to old_repo/alembic/versions/0009_execution_trace.py diff --git a/alembic/versions/0010_dotb_headers.py b/old_repo/alembic/versions/0010_dotb_headers.py similarity index 100% rename from alembic/versions/0010_dotb_headers.py rename to old_repo/alembic/versions/0010_dotb_headers.py diff --git a/apps/dev-console/.env.example b/old_repo/apps/dev-console/.env.example similarity index 100% rename from apps/dev-console/.env.example rename to old_repo/apps/dev-console/.env.example diff --git a/apps/dev-console/bun.lock b/old_repo/apps/dev-console/bun.lock similarity index 100% rename from apps/dev-console/bun.lock rename to old_repo/apps/dev-console/bun.lock diff --git a/apps/dev-console/index.html b/old_repo/apps/dev-console/index.html similarity index 100% rename from apps/dev-console/index.html rename to old_repo/apps/dev-console/index.html diff --git a/apps/dev-console/package.json b/old_repo/apps/dev-console/package.json similarity index 100% rename from apps/dev-console/package.json rename to old_repo/apps/dev-console/package.json diff --git a/apps/dev-console/public/brand-assets/logo.svg b/old_repo/apps/dev-console/public/brand-assets/logo.svg similarity index 100% rename from apps/dev-console/public/brand-assets/logo.svg rename to old_repo/apps/dev-console/public/brand-assets/logo.svg diff --git a/apps/dev-console/public/brand-assets/logo_dark.svg b/old_repo/apps/dev-console/public/brand-assets/logo_dark.svg similarity index 100% rename from apps/dev-console/public/brand-assets/logo_dark.svg rename to old_repo/apps/dev-console/public/brand-assets/logo_dark.svg diff --git a/apps/dev-console/public/brand-assets/logo_thumbnail.svg b/old_repo/apps/dev-console/public/brand-assets/logo_thumbnail.svg similarity index 100% rename from apps/dev-console/public/brand-assets/logo_thumbnail.svg rename to old_repo/apps/dev-console/public/brand-assets/logo_thumbnail.svg diff --git a/apps/dev-console/public/favicon.ico b/old_repo/apps/dev-console/public/favicon.ico similarity index 100% rename from apps/dev-console/public/favicon.ico rename to old_repo/apps/dev-console/public/favicon.ico diff --git a/apps/dev-console/public/favicon.svg b/old_repo/apps/dev-console/public/favicon.svg similarity index 100% rename from apps/dev-console/public/favicon.svg rename to old_repo/apps/dev-console/public/favicon.svg diff --git a/apps/dev-console/public/images/lead-counseling-review.jpg b/old_repo/apps/dev-console/public/images/lead-counseling-review.jpg similarity index 100% rename from apps/dev-console/public/images/lead-counseling-review.jpg rename to old_repo/apps/dev-console/public/images/lead-counseling-review.jpg diff --git a/apps/dev-console/public/images/lead-enterprise-training.jpg b/old_repo/apps/dev-console/public/images/lead-enterprise-training.jpg similarity index 100% rename from apps/dev-console/public/images/lead-enterprise-training.jpg rename to old_repo/apps/dev-console/public/images/lead-enterprise-training.jpg diff --git a/apps/dev-console/public/images/lead-ielts-student.jpg b/old_repo/apps/dev-console/public/images/lead-ielts-student.jpg similarity index 100% rename from apps/dev-console/public/images/lead-ielts-student.jpg rename to old_repo/apps/dev-console/public/images/lead-ielts-student.jpg diff --git a/apps/dev-console/public/images/lead-system-fault.jpg b/old_repo/apps/dev-console/public/images/lead-system-fault.jpg similarity index 100% rename from apps/dev-console/public/images/lead-system-fault.jpg rename to old_repo/apps/dev-console/public/images/lead-system-fault.jpg diff --git a/apps/dev-console/public/lead-scoring-api-docs.html b/old_repo/apps/dev-console/public/lead-scoring-api-docs.html similarity index 100% rename from apps/dev-console/public/lead-scoring-api-docs.html rename to old_repo/apps/dev-console/public/lead-scoring-api-docs.html diff --git a/apps/dev-console/src/App.svelte b/old_repo/apps/dev-console/src/App.svelte similarity index 100% rename from apps/dev-console/src/App.svelte rename to old_repo/apps/dev-console/src/App.svelte diff --git a/apps/dev-console/src/app.css b/old_repo/apps/dev-console/src/app.css similarity index 100% rename from apps/dev-console/src/app.css rename to old_repo/apps/dev-console/src/app.css diff --git a/apps/dev-console/src/lib/api/client.ts b/old_repo/apps/dev-console/src/lib/api/client.ts similarity index 100% rename from apps/dev-console/src/lib/api/client.ts rename to old_repo/apps/dev-console/src/lib/api/client.ts diff --git a/apps/dev-console/src/lib/api/schema.d.ts b/old_repo/apps/dev-console/src/lib/api/schema.d.ts similarity index 100% rename from apps/dev-console/src/lib/api/schema.d.ts rename to old_repo/apps/dev-console/src/lib/api/schema.d.ts diff --git a/apps/dev-console/src/lib/api/types.ts b/old_repo/apps/dev-console/src/lib/api/types.ts similarity index 100% rename from apps/dev-console/src/lib/api/types.ts rename to old_repo/apps/dev-console/src/lib/api/types.ts diff --git a/apps/dev-console/src/lib/components/ChurnPrediction.svelte b/old_repo/apps/dev-console/src/lib/components/ChurnPrediction.svelte similarity index 100% rename from apps/dev-console/src/lib/components/ChurnPrediction.svelte rename to old_repo/apps/dev-console/src/lib/components/ChurnPrediction.svelte diff --git a/apps/dev-console/src/lib/components/DemoFlow.svelte b/old_repo/apps/dev-console/src/lib/components/DemoFlow.svelte similarity index 100% rename from apps/dev-console/src/lib/components/DemoFlow.svelte rename to old_repo/apps/dev-console/src/lib/components/DemoFlow.svelte diff --git a/apps/dev-console/src/lib/components/DynamicRuleBuilder.svelte b/old_repo/apps/dev-console/src/lib/components/DynamicRuleBuilder.svelte similarity index 100% rename from apps/dev-console/src/lib/components/DynamicRuleBuilder.svelte rename to old_repo/apps/dev-console/src/lib/components/DynamicRuleBuilder.svelte diff --git a/apps/dev-console/src/lib/components/FixtureSelector.svelte b/old_repo/apps/dev-console/src/lib/components/FixtureSelector.svelte similarity index 100% rename from apps/dev-console/src/lib/components/FixtureSelector.svelte rename to old_repo/apps/dev-console/src/lib/components/FixtureSelector.svelte diff --git a/apps/dev-console/src/lib/components/Icon.svelte b/old_repo/apps/dev-console/src/lib/components/Icon.svelte similarity index 100% rename from apps/dev-console/src/lib/components/Icon.svelte rename to old_repo/apps/dev-console/src/lib/components/Icon.svelte diff --git a/apps/dev-console/src/lib/components/RunInspector.svelte b/old_repo/apps/dev-console/src/lib/components/RunInspector.svelte similarity index 100% rename from apps/dev-console/src/lib/components/RunInspector.svelte rename to old_repo/apps/dev-console/src/lib/components/RunInspector.svelte diff --git a/apps/dev-console/src/lib/components/RunStatusBadge.svelte b/old_repo/apps/dev-console/src/lib/components/RunStatusBadge.svelte similarity index 100% rename from apps/dev-console/src/lib/components/RunStatusBadge.svelte rename to old_repo/apps/dev-console/src/lib/components/RunStatusBadge.svelte diff --git a/apps/dev-console/src/lib/components/ScoreBreakdown.svelte b/old_repo/apps/dev-console/src/lib/components/ScoreBreakdown.svelte similarity index 100% rename from apps/dev-console/src/lib/components/ScoreBreakdown.svelte rename to old_repo/apps/dev-console/src/lib/components/ScoreBreakdown.svelte diff --git a/apps/dev-console/src/lib/components/ScoreMeter.svelte b/old_repo/apps/dev-console/src/lib/components/ScoreMeter.svelte similarity index 100% rename from apps/dev-console/src/lib/components/ScoreMeter.svelte rename to old_repo/apps/dev-console/src/lib/components/ScoreMeter.svelte diff --git a/apps/dev-console/src/lib/fixtures/golden.ts b/old_repo/apps/dev-console/src/lib/fixtures/golden.ts similarity index 100% rename from apps/dev-console/src/lib/fixtures/golden.ts rename to old_repo/apps/dev-console/src/lib/fixtures/golden.ts diff --git a/apps/dev-console/src/lib/state/consoleState.svelte.ts b/old_repo/apps/dev-console/src/lib/state/consoleState.svelte.ts similarity index 100% rename from apps/dev-console/src/lib/state/consoleState.svelte.ts rename to old_repo/apps/dev-console/src/lib/state/consoleState.svelte.ts diff --git a/apps/dev-console/src/main.ts b/old_repo/apps/dev-console/src/main.ts similarity index 100% rename from apps/dev-console/src/main.ts rename to old_repo/apps/dev-console/src/main.ts diff --git a/apps/dev-console/src/tests/client.test.ts b/old_repo/apps/dev-console/src/tests/client.test.ts similarity index 100% rename from apps/dev-console/src/tests/client.test.ts rename to old_repo/apps/dev-console/src/tests/client.test.ts diff --git a/apps/dev-console/src/tests/fixtures.test.ts b/old_repo/apps/dev-console/src/tests/fixtures.test.ts similarity index 100% rename from apps/dev-console/src/tests/fixtures.test.ts rename to old_repo/apps/dev-console/src/tests/fixtures.test.ts diff --git a/apps/dev-console/src/tests/state.test.ts b/old_repo/apps/dev-console/src/tests/state.test.ts similarity index 100% rename from apps/dev-console/src/tests/state.test.ts rename to old_repo/apps/dev-console/src/tests/state.test.ts diff --git a/apps/dev-console/svelte.config.js b/old_repo/apps/dev-console/svelte.config.js similarity index 100% rename from apps/dev-console/svelte.config.js rename to old_repo/apps/dev-console/svelte.config.js diff --git a/apps/dev-console/tsconfig.json b/old_repo/apps/dev-console/tsconfig.json similarity index 100% rename from apps/dev-console/tsconfig.json rename to old_repo/apps/dev-console/tsconfig.json diff --git a/apps/dev-console/vite.config.ts b/old_repo/apps/dev-console/vite.config.ts similarity index 100% rename from apps/dev-console/vite.config.ts rename to old_repo/apps/dev-console/vite.config.ts diff --git a/old_repo/compose.yaml b/old_repo/compose.yaml new file mode 100644 index 0000000..f3f0153 --- /dev/null +++ b/old_repo/compose.yaml @@ -0,0 +1,14 @@ +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: lead_scoring + ports: + - "${POSTGRES_PORT:-5433}:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d lead_scoring"] + interval: 5s + timeout: 5s + retries: 10 diff --git a/contracts/lead-scoring-contract-pack/v1/SHA256SUMS b/old_repo/contracts/lead-scoring-contract-pack/v1/SHA256SUMS similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/SHA256SUMS rename to old_repo/contracts/lead-scoring-contract-pack/v1/SHA256SUMS diff --git a/contracts/lead-scoring-contract-pack/v1/compatibility.json b/old_repo/contracts/lead-scoring-contract-pack/v1/compatibility.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/compatibility.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/compatibility.json diff --git a/contracts/lead-scoring-contract-pack/v1/contract.json b/old_repo/contracts/lead-scoring-contract-pack/v1/contract.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/contract.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/contract.json diff --git a/contracts/lead-scoring-contract-pack/v1/error-taxonomy.json b/old_repo/contracts/lead-scoring-contract-pack/v1/error-taxonomy.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/error-taxonomy.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/error-taxonomy.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixture-index.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixture-index.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixture-index.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixture-index.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/admission-request.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/admission-request.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/admission-request.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/admission-request.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/admission-response.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/admission-response.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/admission-response.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/admission-response.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/error-validation.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/error-validation.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/error-validation.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/error-validation.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/lineage-child.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/lineage-child.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/lineage-child.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/lineage-child.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/lineage-root.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/lineage-root.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/lineage-root.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/lineage-root.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/marker/deleted-block-followed-by-append.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/marker/deleted-block-followed-by-append.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/marker/deleted-block-followed-by-append.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/marker/deleted-block-followed-by-append.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/marker/empty-description-append.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/marker/empty-description-append.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/marker/empty-description-append.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/marker/empty-description-append.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/marker/failed-run-no-render.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/marker/failed-run-no-render.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/marker/failed-run-no-render.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/marker/failed-run-no-render.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/marker/multiple-complete-blocks.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/marker/multiple-complete-blocks.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/marker/multiple-complete-blocks.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/marker/multiple-complete-blocks.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/marker/nested-block.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/marker/nested-block.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/marker/nested-block.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/marker/nested-block.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/marker/orphan-closing.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/marker/orphan-closing.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/marker/orphan-closing.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/marker/orphan-closing.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/marker/orphan-opening.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/marker/orphan-opening.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/marker/orphan-opening.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/marker/orphan-opening.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/marker/overlapping-block.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/marker/overlapping-block.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/marker/overlapping-block.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/marker/overlapping-block.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/marker/plain-description-append.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/marker/plain-description-append.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/marker/plain-description-append.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/marker/plain-description-append.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/marker/single-valid-block-replace.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/marker/single-valid-block-replace.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/marker/single-valid-block-replace.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/marker/single-valid-block-replace.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/marker/unicode-multiline-preservation.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/marker/unicode-multiline-preservation.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/marker/unicode-multiline-preservation.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/marker/unicode-multiline-preservation.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/negative/admission-subject-mismatch.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/negative/admission-subject-mismatch.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/negative/admission-subject-mismatch.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/negative/admission-subject-mismatch.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/negative/event-missing-version.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/negative/event-missing-version.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/negative/event-missing-version.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/negative/event-missing-version.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/negative/review-override-out-of-range.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/negative/review-override-out-of-range.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/negative/review-override-out-of-range.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/negative/review-override-out-of-range.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/negative/snapshot-future-message.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/negative/snapshot-future-message.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/negative/snapshot-future-message.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/negative/snapshot-future-message.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/polling/completed.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/polling/completed.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/polling/completed.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/polling/completed.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/polling/failed.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/polling/failed.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/polling/failed.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/polling/failed.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/polling/pending.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/polling/pending.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/polling/pending.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/polling/pending.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/polling/running.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/polling/running.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/polling/running.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/polling/running.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/review-approve-request.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/review-approve-request.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/review-approve-request.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/review-approve-request.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/review-override-request.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/review-override-request.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/review-override-request.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/review-override-request.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/review-re_evaluate-request.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/review-re_evaluate-request.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/review-re_evaluate-request.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/review-re_evaluate-request.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/review-response.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/review-response.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/review-response.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/review-response.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/scoring-config-selection.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/scoring-config-selection.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/scoring-config-selection.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/scoring-config-selection.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/scoring-config.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/scoring-config.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/scoring-config.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/scoring-config.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/snapshot.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/snapshot.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/snapshot.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/snapshot.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/tenant-unavailable.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/tenant-unavailable.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/tenant-unavailable.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/tenant-unavailable.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/webhook/completed.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/webhook/completed.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/webhook/completed.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/webhook/completed.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/webhook/review_required.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/webhook/review_required.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/webhook/review_required.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/webhook/review_required.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/webhook/reviewed_approve.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/webhook/reviewed_approve.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/webhook/reviewed_approve.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/webhook/reviewed_approve.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/webhook/reviewed_override.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/webhook/reviewed_override.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/webhook/reviewed_override.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/webhook/reviewed_override.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/webhook/reviewed_reevaluate.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/webhook/reviewed_reevaluate.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/webhook/reviewed_reevaluate.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/webhook/reviewed_reevaluate.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/webhook/tenant-unavailable.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/webhook/tenant-unavailable.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/webhook/tenant-unavailable.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/webhook/tenant-unavailable.json diff --git a/contracts/lead-scoring-contract-pack/v1/fixtures/webhook/worker_interrupted.json b/old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/webhook/worker_interrupted.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/fixtures/webhook/worker_interrupted.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/fixtures/webhook/worker_interrupted.json diff --git a/contracts/lead-scoring-contract-pack/v1/openapi.json b/old_repo/contracts/lead-scoring-contract-pack/v1/openapi.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/openapi.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/openapi.json diff --git a/contracts/lead-scoring-contract-pack/v1/outcome-taxonomy.json b/old_repo/contracts/lead-scoring-contract-pack/v1/outcome-taxonomy.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/outcome-taxonomy.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/outcome-taxonomy.json diff --git a/contracts/lead-scoring-contract-pack/v1/profiles/polling/contract.json b/old_repo/contracts/lead-scoring-contract-pack/v1/profiles/polling/contract.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/profiles/polling/contract.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/profiles/polling/contract.json diff --git a/contracts/lead-scoring-contract-pack/v1/profiles/polling/error-response.schema.json b/old_repo/contracts/lead-scoring-contract-pack/v1/profiles/polling/error-response.schema.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/profiles/polling/error-response.schema.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/profiles/polling/error-response.schema.json diff --git a/contracts/lead-scoring-contract-pack/v1/profiles/polling/scoring-run-response.schema.json b/old_repo/contracts/lead-scoring-contract-pack/v1/profiles/polling/scoring-run-response.schema.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/profiles/polling/scoring-run-response.schema.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/profiles/polling/scoring-run-response.schema.json diff --git a/contracts/lead-scoring-contract-pack/v1/profiles/webhook/contract.json b/old_repo/contracts/lead-scoring-contract-pack/v1/profiles/webhook/contract.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/profiles/webhook/contract.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/profiles/webhook/contract.json diff --git a/contracts/lead-scoring-contract-pack/v1/profiles/webhook/lead-scoring-reviewed-event.schema.json b/old_repo/contracts/lead-scoring-contract-pack/v1/profiles/webhook/lead-scoring-reviewed-event.schema.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/profiles/webhook/lead-scoring-reviewed-event.schema.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/profiles/webhook/lead-scoring-reviewed-event.schema.json diff --git a/contracts/lead-scoring-contract-pack/v1/profiles/webhook/lead-scoring-terminal-event.schema.json b/old_repo/contracts/lead-scoring-contract-pack/v1/profiles/webhook/lead-scoring-terminal-event.schema.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/profiles/webhook/lead-scoring-terminal-event.schema.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/profiles/webhook/lead-scoring-terminal-event.schema.json diff --git a/contracts/lead-scoring-contract-pack/v1/profiles/webhook/signature-vectors.json b/old_repo/contracts/lead-scoring-contract-pack/v1/profiles/webhook/signature-vectors.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/profiles/webhook/signature-vectors.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/profiles/webhook/signature-vectors.json diff --git a/contracts/lead-scoring-contract-pack/v1/profiles/webhook/webhook-ingest-error-response.schema.json b/old_repo/contracts/lead-scoring-contract-pack/v1/profiles/webhook/webhook-ingest-error-response.schema.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/profiles/webhook/webhook-ingest-error-response.schema.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/profiles/webhook/webhook-ingest-error-response.schema.json diff --git a/contracts/lead-scoring-contract-pack/v1/profiles/webhook/webhook-ingest-response.schema.json b/old_repo/contracts/lead-scoring-contract-pack/v1/profiles/webhook/webhook-ingest-response.schema.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/profiles/webhook/webhook-ingest-response.schema.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/profiles/webhook/webhook-ingest-response.schema.json diff --git a/contracts/lead-scoring-contract-pack/v1/schemas/admission-request.schema.json b/old_repo/contracts/lead-scoring-contract-pack/v1/schemas/admission-request.schema.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/schemas/admission-request.schema.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/schemas/admission-request.schema.json diff --git a/contracts/lead-scoring-contract-pack/v1/schemas/admission-response.schema.json b/old_repo/contracts/lead-scoring-contract-pack/v1/schemas/admission-response.schema.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/schemas/admission-response.schema.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/schemas/admission-response.schema.json diff --git a/contracts/lead-scoring-contract-pack/v1/schemas/canonical-snapshot.schema.json b/old_repo/contracts/lead-scoring-contract-pack/v1/schemas/canonical-snapshot.schema.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/schemas/canonical-snapshot.schema.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/schemas/canonical-snapshot.schema.json diff --git a/contracts/lead-scoring-contract-pack/v1/schemas/error-response.schema.json b/old_repo/contracts/lead-scoring-contract-pack/v1/schemas/error-response.schema.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/schemas/error-response.schema.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/schemas/error-response.schema.json diff --git a/contracts/lead-scoring-contract-pack/v1/schemas/error-taxonomy.schema.json b/old_repo/contracts/lead-scoring-contract-pack/v1/schemas/error-taxonomy.schema.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/schemas/error-taxonomy.schema.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/schemas/error-taxonomy.schema.json diff --git a/contracts/lead-scoring-contract-pack/v1/schemas/lineage.schema.json b/old_repo/contracts/lead-scoring-contract-pack/v1/schemas/lineage.schema.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/schemas/lineage.schema.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/schemas/lineage.schema.json diff --git a/contracts/lead-scoring-contract-pack/v1/schemas/outcome-taxonomy.schema.json b/old_repo/contracts/lead-scoring-contract-pack/v1/schemas/outcome-taxonomy.schema.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/schemas/outcome-taxonomy.schema.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/schemas/outcome-taxonomy.schema.json diff --git a/contracts/lead-scoring-contract-pack/v1/schemas/polling-response.schema.json b/old_repo/contracts/lead-scoring-contract-pack/v1/schemas/polling-response.schema.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/schemas/polling-response.schema.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/schemas/polling-response.schema.json diff --git a/contracts/lead-scoring-contract-pack/v1/schemas/review-request.schema.json b/old_repo/contracts/lead-scoring-contract-pack/v1/schemas/review-request.schema.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/schemas/review-request.schema.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/schemas/review-request.schema.json diff --git a/contracts/lead-scoring-contract-pack/v1/schemas/review-response.schema.json b/old_repo/contracts/lead-scoring-contract-pack/v1/schemas/review-response.schema.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/schemas/review-response.schema.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/schemas/review-response.schema.json diff --git a/contracts/lead-scoring-contract-pack/v1/schemas/reviewed-event.schema.json b/old_repo/contracts/lead-scoring-contract-pack/v1/schemas/reviewed-event.schema.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/schemas/reviewed-event.schema.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/schemas/reviewed-event.schema.json diff --git a/contracts/lead-scoring-contract-pack/v1/schemas/scoring-config-selection.schema.json b/old_repo/contracts/lead-scoring-contract-pack/v1/schemas/scoring-config-selection.schema.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/schemas/scoring-config-selection.schema.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/schemas/scoring-config-selection.schema.json diff --git a/contracts/lead-scoring-contract-pack/v1/schemas/scoring-config.schema.json b/old_repo/contracts/lead-scoring-contract-pack/v1/schemas/scoring-config.schema.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/schemas/scoring-config.schema.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/schemas/scoring-config.schema.json diff --git a/contracts/lead-scoring-contract-pack/v1/schemas/terminal-event.schema.json b/old_repo/contracts/lead-scoring-contract-pack/v1/schemas/terminal-event.schema.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/schemas/terminal-event.schema.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/schemas/terminal-event.schema.json diff --git a/contracts/lead-scoring-contract-pack/v1/schemas/webhook-event-envelope.schema.json b/old_repo/contracts/lead-scoring-contract-pack/v1/schemas/webhook-event-envelope.schema.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/schemas/webhook-event-envelope.schema.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/schemas/webhook-event-envelope.schema.json diff --git a/contracts/lead-scoring-contract-pack/v1/schemas/webhook-ingest-error-response.schema.json b/old_repo/contracts/lead-scoring-contract-pack/v1/schemas/webhook-ingest-error-response.schema.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/schemas/webhook-ingest-error-response.schema.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/schemas/webhook-ingest-error-response.schema.json diff --git a/contracts/lead-scoring-contract-pack/v1/schemas/webhook-ingest-response.schema.json b/old_repo/contracts/lead-scoring-contract-pack/v1/schemas/webhook-ingest-response.schema.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/schemas/webhook-ingest-response.schema.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/schemas/webhook-ingest-response.schema.json diff --git a/contracts/lead-scoring-contract-pack/v1/security-profile.json b/old_repo/contracts/lead-scoring-contract-pack/v1/security-profile.json similarity index 100% rename from contracts/lead-scoring-contract-pack/v1/security-profile.json rename to old_repo/contracts/lead-scoring-contract-pack/v1/security-profile.json diff --git a/contracts/lead-scoring-polling/v1/contract.json b/old_repo/contracts/lead-scoring-polling/v1/contract.json similarity index 100% rename from contracts/lead-scoring-polling/v1/contract.json rename to old_repo/contracts/lead-scoring-polling/v1/contract.json diff --git a/contracts/lead-scoring-polling/v1/error-response.schema.json b/old_repo/contracts/lead-scoring-polling/v1/error-response.schema.json similarity index 100% rename from contracts/lead-scoring-polling/v1/error-response.schema.json rename to old_repo/contracts/lead-scoring-polling/v1/error-response.schema.json diff --git a/contracts/lead-scoring-polling/v1/fixtures/completed.json b/old_repo/contracts/lead-scoring-polling/v1/fixtures/completed.json similarity index 100% rename from contracts/lead-scoring-polling/v1/fixtures/completed.json rename to old_repo/contracts/lead-scoring-polling/v1/fixtures/completed.json diff --git a/contracts/lead-scoring-polling/v1/fixtures/failed.json b/old_repo/contracts/lead-scoring-polling/v1/fixtures/failed.json similarity index 100% rename from contracts/lead-scoring-polling/v1/fixtures/failed.json rename to old_repo/contracts/lead-scoring-polling/v1/fixtures/failed.json diff --git a/contracts/lead-scoring-polling/v1/fixtures/pending.json b/old_repo/contracts/lead-scoring-polling/v1/fixtures/pending.json similarity index 100% rename from contracts/lead-scoring-polling/v1/fixtures/pending.json rename to old_repo/contracts/lead-scoring-polling/v1/fixtures/pending.json diff --git a/contracts/lead-scoring-polling/v1/fixtures/running.json b/old_repo/contracts/lead-scoring-polling/v1/fixtures/running.json similarity index 100% rename from contracts/lead-scoring-polling/v1/fixtures/running.json rename to old_repo/contracts/lead-scoring-polling/v1/fixtures/running.json diff --git a/contracts/lead-scoring-polling/v1/scoring-run-response.schema.json b/old_repo/contracts/lead-scoring-polling/v1/scoring-run-response.schema.json similarity index 100% rename from contracts/lead-scoring-polling/v1/scoring-run-response.schema.json rename to old_repo/contracts/lead-scoring-polling/v1/scoring-run-response.schema.json diff --git a/contracts/lead-scoring-webhook/v1/contract.json b/old_repo/contracts/lead-scoring-webhook/v1/contract.json similarity index 100% rename from contracts/lead-scoring-webhook/v1/contract.json rename to old_repo/contracts/lead-scoring-webhook/v1/contract.json diff --git a/contracts/lead-scoring-webhook/v1/fixtures/completed.json b/old_repo/contracts/lead-scoring-webhook/v1/fixtures/completed.json similarity index 100% rename from contracts/lead-scoring-webhook/v1/fixtures/completed.json rename to old_repo/contracts/lead-scoring-webhook/v1/fixtures/completed.json diff --git a/contracts/lead-scoring-webhook/v1/fixtures/review_required.json b/old_repo/contracts/lead-scoring-webhook/v1/fixtures/review_required.json similarity index 100% rename from contracts/lead-scoring-webhook/v1/fixtures/review_required.json rename to old_repo/contracts/lead-scoring-webhook/v1/fixtures/review_required.json diff --git a/contracts/lead-scoring-webhook/v1/fixtures/reviewed_approve.json b/old_repo/contracts/lead-scoring-webhook/v1/fixtures/reviewed_approve.json similarity index 100% rename from contracts/lead-scoring-webhook/v1/fixtures/reviewed_approve.json rename to old_repo/contracts/lead-scoring-webhook/v1/fixtures/reviewed_approve.json diff --git a/contracts/lead-scoring-webhook/v1/fixtures/reviewed_override.json b/old_repo/contracts/lead-scoring-webhook/v1/fixtures/reviewed_override.json similarity index 100% rename from contracts/lead-scoring-webhook/v1/fixtures/reviewed_override.json rename to old_repo/contracts/lead-scoring-webhook/v1/fixtures/reviewed_override.json diff --git a/contracts/lead-scoring-webhook/v1/fixtures/reviewed_reevaluate.json b/old_repo/contracts/lead-scoring-webhook/v1/fixtures/reviewed_reevaluate.json similarity index 100% rename from contracts/lead-scoring-webhook/v1/fixtures/reviewed_reevaluate.json rename to old_repo/contracts/lead-scoring-webhook/v1/fixtures/reviewed_reevaluate.json diff --git a/contracts/lead-scoring-webhook/v1/fixtures/tenant-unavailable.json b/old_repo/contracts/lead-scoring-webhook/v1/fixtures/tenant-unavailable.json similarity index 100% rename from contracts/lead-scoring-webhook/v1/fixtures/tenant-unavailable.json rename to old_repo/contracts/lead-scoring-webhook/v1/fixtures/tenant-unavailable.json diff --git a/contracts/lead-scoring-webhook/v1/fixtures/worker_interrupted.json b/old_repo/contracts/lead-scoring-webhook/v1/fixtures/worker_interrupted.json similarity index 100% rename from contracts/lead-scoring-webhook/v1/fixtures/worker_interrupted.json rename to old_repo/contracts/lead-scoring-webhook/v1/fixtures/worker_interrupted.json diff --git a/contracts/lead-scoring-webhook/v1/lead-scoring-reviewed-event.schema.json b/old_repo/contracts/lead-scoring-webhook/v1/lead-scoring-reviewed-event.schema.json similarity index 100% rename from contracts/lead-scoring-webhook/v1/lead-scoring-reviewed-event.schema.json rename to old_repo/contracts/lead-scoring-webhook/v1/lead-scoring-reviewed-event.schema.json diff --git a/contracts/lead-scoring-webhook/v1/lead-scoring-terminal-event.schema.json b/old_repo/contracts/lead-scoring-webhook/v1/lead-scoring-terminal-event.schema.json similarity index 100% rename from contracts/lead-scoring-webhook/v1/lead-scoring-terminal-event.schema.json rename to old_repo/contracts/lead-scoring-webhook/v1/lead-scoring-terminal-event.schema.json diff --git a/contracts/lead-scoring-webhook/v1/signature-vectors.json b/old_repo/contracts/lead-scoring-webhook/v1/signature-vectors.json similarity index 100% rename from contracts/lead-scoring-webhook/v1/signature-vectors.json rename to old_repo/contracts/lead-scoring-webhook/v1/signature-vectors.json diff --git a/contracts/lead-scoring-webhook/v1/webhook-ingest-error-response.schema.json b/old_repo/contracts/lead-scoring-webhook/v1/webhook-ingest-error-response.schema.json similarity index 100% rename from contracts/lead-scoring-webhook/v1/webhook-ingest-error-response.schema.json rename to old_repo/contracts/lead-scoring-webhook/v1/webhook-ingest-error-response.schema.json diff --git a/contracts/lead-scoring-webhook/v1/webhook-ingest-response.schema.json b/old_repo/contracts/lead-scoring-webhook/v1/webhook-ingest-response.schema.json similarity index 100% rename from contracts/lead-scoring-webhook/v1/webhook-ingest-response.schema.json rename to old_repo/contracts/lead-scoring-webhook/v1/webhook-ingest-response.schema.json diff --git a/docs/integrations/lead-scoring-contract-pack-v1.md b/old_repo/docs/integrations/lead-scoring-contract-pack-v1.md similarity index 100% rename from docs/integrations/lead-scoring-contract-pack-v1.md rename to old_repo/docs/integrations/lead-scoring-contract-pack-v1.md diff --git a/docs/integrations/lead-scoring-polling-v1.md b/old_repo/docs/integrations/lead-scoring-polling-v1.md similarity index 100% rename from docs/integrations/lead-scoring-polling-v1.md rename to old_repo/docs/integrations/lead-scoring-polling-v1.md diff --git a/docs/integrations/lead-scoring-webhook-v1.md b/old_repo/docs/integrations/lead-scoring-webhook-v1.md similarity index 100% rename from docs/integrations/lead-scoring-webhook-v1.md rename to old_repo/docs/integrations/lead-scoring-webhook-v1.md diff --git a/docs/operations/lead-scoring-observability.md b/old_repo/docs/operations/lead-scoring-observability.md similarity index 100% rename from docs/operations/lead-scoring-observability.md rename to old_repo/docs/operations/lead-scoring-observability.md diff --git a/docs/specs/lead-scoring-customer-pilot.md b/old_repo/docs/specs/lead-scoring-customer-pilot.md similarity index 100% rename from docs/specs/lead-scoring-customer-pilot.md rename to old_repo/docs/specs/lead-scoring-customer-pilot.md diff --git a/old_repo/pyproject.toml b/old_repo/pyproject.toml new file mode 100644 index 0000000..cf3de15 --- /dev/null +++ b/old_repo/pyproject.toml @@ -0,0 +1,67 @@ +[project] +name = "lead-scoring" +version = "0.1.0" +description = "Lead Scoring service for AI Platform" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "fastapi>=0.115.0", + "uvicorn[standard]>=0.30.0", + "pydantic>=2.8.0", + "pydantic-settings>=2.4.0", + "sqlalchemy[asyncio]>=2.0.30", + "asyncpg>=0.29.0", + "alembic>=1.13.0", + "httpx>=0.27.0", +] + +[project.scripts] +generate-openapi = "lead_scoring.cli.openapi:main" +generate-webhook-contract = "lead_scoring.cli.webhook_contract:main" +generate-polling-contract = "lead_scoring.cli.polling_contract:main" +generate-contract-pack = "lead_scoring.cli.contract_pack:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/lead_scoring"] + +[dependency-groups] +dev = [ + "pytest>=8.0.0", + "pytest-asyncio>=0.23.0", + "testcontainers[postgres]>=4.0.0", + "ruff>=0.5.0", + "pyright>=1.1.370", + "ty>=0.0.74", +] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["tests"] +pythonpath = ["src"] + +[tool.ruff] +line-length = 100 +target-version = "py313" +src = ["src", "tests"] + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "UP", # pyupgrade +] +ignore = [] + +[tool.pyright] +include = ["src", "tests"] +pythonVersion = "3.13" +typeCheckingMode = "standard" +venvPath = "." +venv = ".venv" diff --git a/src/lead_scoring/__init__.py b/old_repo/src/lead_scoring/__init__.py similarity index 100% rename from src/lead_scoring/__init__.py rename to old_repo/src/lead_scoring/__init__.py diff --git a/src/lead_scoring/aihub/__init__.py b/old_repo/src/lead_scoring/aihub/__init__.py similarity index 100% rename from src/lead_scoring/aihub/__init__.py rename to old_repo/src/lead_scoring/aihub/__init__.py diff --git a/src/lead_scoring/aihub/client.py b/old_repo/src/lead_scoring/aihub/client.py similarity index 100% rename from src/lead_scoring/aihub/client.py rename to old_repo/src/lead_scoring/aihub/client.py diff --git a/src/lead_scoring/aihub/errors.py b/old_repo/src/lead_scoring/aihub/errors.py similarity index 100% rename from src/lead_scoring/aihub/errors.py rename to old_repo/src/lead_scoring/aihub/errors.py diff --git a/src/lead_scoring/aihub/schemas.py b/old_repo/src/lead_scoring/aihub/schemas.py similarity index 100% rename from src/lead_scoring/aihub/schemas.py rename to old_repo/src/lead_scoring/aihub/schemas.py diff --git a/src/lead_scoring/api/__init__.py b/old_repo/src/lead_scoring/api/__init__.py similarity index 100% rename from src/lead_scoring/api/__init__.py rename to old_repo/src/lead_scoring/api/__init__.py diff --git a/src/lead_scoring/api/deps.py b/old_repo/src/lead_scoring/api/deps.py similarity index 100% rename from src/lead_scoring/api/deps.py rename to old_repo/src/lead_scoring/api/deps.py diff --git a/src/lead_scoring/api/v1/__init__.py b/old_repo/src/lead_scoring/api/v1/__init__.py similarity index 100% rename from src/lead_scoring/api/v1/__init__.py rename to old_repo/src/lead_scoring/api/v1/__init__.py diff --git a/src/lead_scoring/api/v1/scoring_runs.py b/old_repo/src/lead_scoring/api/v1/scoring_runs.py similarity index 100% rename from src/lead_scoring/api/v1/scoring_runs.py rename to old_repo/src/lead_scoring/api/v1/scoring_runs.py diff --git a/src/lead_scoring/auth.py b/old_repo/src/lead_scoring/auth.py similarity index 100% rename from src/lead_scoring/auth.py rename to old_repo/src/lead_scoring/auth.py diff --git a/src/lead_scoring/cli/__init__.py b/old_repo/src/lead_scoring/cli/__init__.py similarity index 100% rename from src/lead_scoring/cli/__init__.py rename to old_repo/src/lead_scoring/cli/__init__.py diff --git a/src/lead_scoring/cli/contract_pack.py b/old_repo/src/lead_scoring/cli/contract_pack.py similarity index 100% rename from src/lead_scoring/cli/contract_pack.py rename to old_repo/src/lead_scoring/cli/contract_pack.py diff --git a/src/lead_scoring/cli/openapi.py b/old_repo/src/lead_scoring/cli/openapi.py similarity index 100% rename from src/lead_scoring/cli/openapi.py rename to old_repo/src/lead_scoring/cli/openapi.py diff --git a/src/lead_scoring/cli/polling_contract.py b/old_repo/src/lead_scoring/cli/polling_contract.py similarity index 100% rename from src/lead_scoring/cli/polling_contract.py rename to old_repo/src/lead_scoring/cli/polling_contract.py diff --git a/src/lead_scoring/cli/webhook_contract.py b/old_repo/src/lead_scoring/cli/webhook_contract.py similarity index 100% rename from src/lead_scoring/cli/webhook_contract.py rename to old_repo/src/lead_scoring/cli/webhook_contract.py diff --git a/src/lead_scoring/config.py b/old_repo/src/lead_scoring/config.py similarity index 100% rename from src/lead_scoring/config.py rename to old_repo/src/lead_scoring/config.py diff --git a/src/lead_scoring/db/__init__.py b/old_repo/src/lead_scoring/db/__init__.py similarity index 100% rename from src/lead_scoring/db/__init__.py rename to old_repo/src/lead_scoring/db/__init__.py diff --git a/src/lead_scoring/db/base.py b/old_repo/src/lead_scoring/db/base.py similarity index 100% rename from src/lead_scoring/db/base.py rename to old_repo/src/lead_scoring/db/base.py diff --git a/src/lead_scoring/db/models/__init__.py b/old_repo/src/lead_scoring/db/models/__init__.py similarity index 100% rename from src/lead_scoring/db/models/__init__.py rename to old_repo/src/lead_scoring/db/models/__init__.py diff --git a/src/lead_scoring/db/models/outbox_event.py b/old_repo/src/lead_scoring/db/models/outbox_event.py similarity index 100% rename from src/lead_scoring/db/models/outbox_event.py rename to old_repo/src/lead_scoring/db/models/outbox_event.py diff --git a/src/lead_scoring/db/models/review_audit.py b/old_repo/src/lead_scoring/db/models/review_audit.py similarity index 100% rename from src/lead_scoring/db/models/review_audit.py rename to old_repo/src/lead_scoring/db/models/review_audit.py diff --git a/src/lead_scoring/db/models/scoring_run.py b/old_repo/src/lead_scoring/db/models/scoring_run.py similarity index 100% rename from src/lead_scoring/db/models/scoring_run.py rename to old_repo/src/lead_scoring/db/models/scoring_run.py diff --git a/src/lead_scoring/db/session.py b/old_repo/src/lead_scoring/db/session.py similarity index 100% rename from src/lead_scoring/db/session.py rename to old_repo/src/lead_scoring/db/session.py diff --git a/src/lead_scoring/dynamic_rules.py b/old_repo/src/lead_scoring/dynamic_rules.py similarity index 100% rename from src/lead_scoring/dynamic_rules.py rename to old_repo/src/lead_scoring/dynamic_rules.py diff --git a/src/lead_scoring/execution.py b/old_repo/src/lead_scoring/execution.py similarity index 100% rename from src/lead_scoring/execution.py rename to old_repo/src/lead_scoring/execution.py diff --git a/src/lead_scoring/main.py b/old_repo/src/lead_scoring/main.py similarity index 100% rename from src/lead_scoring/main.py rename to old_repo/src/lead_scoring/main.py diff --git a/src/lead_scoring/observability.py b/old_repo/src/lead_scoring/observability.py similarity index 100% rename from src/lead_scoring/observability.py rename to old_repo/src/lead_scoring/observability.py diff --git a/src/lead_scoring/retention.py b/old_repo/src/lead_scoring/retention.py similarity index 100% rename from src/lead_scoring/retention.py rename to old_repo/src/lead_scoring/retention.py diff --git a/src/lead_scoring/schemas/__init__.py b/old_repo/src/lead_scoring/schemas/__init__.py similarity index 100% rename from src/lead_scoring/schemas/__init__.py rename to old_repo/src/lead_scoring/schemas/__init__.py diff --git a/src/lead_scoring/schemas/dynamic_rule.py b/old_repo/src/lead_scoring/schemas/dynamic_rule.py similarity index 100% rename from src/lead_scoring/schemas/dynamic_rule.py rename to old_repo/src/lead_scoring/schemas/dynamic_rule.py diff --git a/src/lead_scoring/schemas/errors.py b/old_repo/src/lead_scoring/schemas/errors.py similarity index 100% rename from src/lead_scoring/schemas/errors.py rename to old_repo/src/lead_scoring/schemas/errors.py diff --git a/src/lead_scoring/schemas/health.py b/old_repo/src/lead_scoring/schemas/health.py similarity index 100% rename from src/lead_scoring/schemas/health.py rename to old_repo/src/lead_scoring/schemas/health.py diff --git a/src/lead_scoring/schemas/lineage.py b/old_repo/src/lead_scoring/schemas/lineage.py similarity index 100% rename from src/lead_scoring/schemas/lineage.py rename to old_repo/src/lead_scoring/schemas/lineage.py diff --git a/src/lead_scoring/schemas/outcomes.py b/old_repo/src/lead_scoring/schemas/outcomes.py similarity index 100% rename from src/lead_scoring/schemas/outcomes.py rename to old_repo/src/lead_scoring/schemas/outcomes.py diff --git a/src/lead_scoring/schemas/review.py b/old_repo/src/lead_scoring/schemas/review.py similarity index 100% rename from src/lead_scoring/schemas/review.py rename to old_repo/src/lead_scoring/schemas/review.py diff --git a/src/lead_scoring/schemas/rule_breakdown.py b/old_repo/src/lead_scoring/schemas/rule_breakdown.py similarity index 100% rename from src/lead_scoring/schemas/rule_breakdown.py rename to old_repo/src/lead_scoring/schemas/rule_breakdown.py diff --git a/src/lead_scoring/schemas/scoring_config.py b/old_repo/src/lead_scoring/schemas/scoring_config.py similarity index 100% rename from src/lead_scoring/schemas/scoring_config.py rename to old_repo/src/lead_scoring/schemas/scoring_config.py diff --git a/src/lead_scoring/schemas/scoring_run.py b/old_repo/src/lead_scoring/schemas/scoring_run.py similarity index 100% rename from src/lead_scoring/schemas/scoring_run.py rename to old_repo/src/lead_scoring/schemas/scoring_run.py diff --git a/src/lead_scoring/schemas/snapshot.py b/old_repo/src/lead_scoring/schemas/snapshot.py similarity index 100% rename from src/lead_scoring/schemas/snapshot.py rename to old_repo/src/lead_scoring/schemas/snapshot.py diff --git a/src/lead_scoring/schemas/webhook.py b/old_repo/src/lead_scoring/schemas/webhook.py similarity index 100% rename from src/lead_scoring/schemas/webhook.py rename to old_repo/src/lead_scoring/schemas/webhook.py diff --git a/src/lead_scoring/scoring.py b/old_repo/src/lead_scoring/scoring.py similarity index 100% rename from src/lead_scoring/scoring.py rename to old_repo/src/lead_scoring/scoring.py diff --git a/src/lead_scoring/webhook/__init__.py b/old_repo/src/lead_scoring/webhook/__init__.py similarity index 100% rename from src/lead_scoring/webhook/__init__.py rename to old_repo/src/lead_scoring/webhook/__init__.py diff --git a/src/lead_scoring/webhook/dispatcher.py b/old_repo/src/lead_scoring/webhook/dispatcher.py similarity index 100% rename from src/lead_scoring/webhook/dispatcher.py rename to old_repo/src/lead_scoring/webhook/dispatcher.py diff --git a/src/lead_scoring/webhook/signing.py b/old_repo/src/lead_scoring/webhook/signing.py similarity index 100% rename from src/lead_scoring/webhook/signing.py rename to old_repo/src/lead_scoring/webhook/signing.py diff --git a/old_repo/stack.md b/old_repo/stack.md new file mode 100644 index 0000000..63966bd --- /dev/null +++ b/old_repo/stack.md @@ -0,0 +1,113 @@ +# Technology Stack + +> This document records technology decisions, default candidates, deferred options, and trade-offs. It does not override the approved Lead Scoring Customer Pilot specification or released executable contracts. + +## Project Context and Boundaries + +- Purpose: Build the first Python-based Lead Scoring service in AI Platform for a controlled Customer Pilot. +- Shape: Workspace containing specifications, documentation, and the initialized baseline for the Lead Scoring service (`src/lead_scoring/`). +- Material constraints: + - DotB EMS remains the CRM source of truth. + - Omni owns EMS integration, identity resolution, conversation enrichment, Lead Scoring configuration, and HITL UI. + - Lead Scoring owns scoring logic, versioned configuration, scoring-run history, and domain events. + - AIHub owns model access, V-Key validation, quota, credit, and LLM audit. + - Global runtimes and the shared `mcpls` configuration remain outside project bootstrap scope. + +## Active Decisions + +### Service runtime and HTTP framework + +- Status: Implemented baseline in `src/lead_scoring/`. +- Primary decision owner: Lead Scoring service. +- Technology: Python 3.13, `uv`, FastAPI, and Pydantic. +- Rationale: The service is Python-first, Pydantic provides executable validation, and FastAPI derives the API contract from the same models. +- Baseline implementation: `src/lead_scoring/` package, `GET /health` endpoint, async lifespan managing database engine and session factory. + +### API contract and generated client + +- Status: Implemented baseline CLI for OpenAPI generation. +- Primary decision owner: Lead Scoring API contract. +- Technology: Pydantic models as the API source of truth, FastAPI-generated OpenAPI, CLI generator with `--check` stale detection. +- Rationale: One executable backend contract avoids handwritten DTO and frontend client drift. +- Constraints: Generated artifacts (`openapi.json`) are ignored derived artifacts produced deterministically by `uv run generate-openapi` and verified for drift via `--check`. Never edit manually. + +### Shared executable Contract Pack + +- Status: Selected pre-release baseline for the Customer Pilot. +- Primary decision owner: AI Platform, with DotB-Omni review and consumer validation. +- Technology: Versioned committed directory at `contracts/lead-scoring-contract-pack/v1/`, generated from Pydantic models, OpenAPI and shared JSON fixtures. +- Rationale: One pinned artifact corpus lets provider and consumer repositories validate the same exact wire shapes without maintaining competing payload definitions. +- Versioning: Consumers pin `contract_pack_version` together with the AI Platform Git commit. Additive changes preserve the API major and existing required fields/enums; breaking changes publish a new major directory and retain the old pack. +- Verification: `uv run generate-contract-pack --validate-fixtures` validates positive and negative provider fixtures, while `uv run generate-contract-pack --check` detects missing, stale or unexpected artifacts. + +### Persistence engine and migration tooling + +- Status: Adopted and implemented baseline. +- Primary decision owner: Lead Scoring persistence and delivery boundary. +- Technology: PostgreSQL 16+, SQLAlchemy 2 async (`asyncpg`), Alembic async migration lifecycle. +- Rationale: Production requires PostgreSQL-native features (transactional outbox, jsonb, concurrency safety). SQLite fallback is strictly prohibited to prevent dialect divergence. +- Verification: Database integration tests execute real Alembic `upgrade head` and `downgrade base` lifecycle against disposable PostgreSQL 16 containers via Docker testcontainers. SQLite and ambient database fallbacks are strictly prohibited. + +### Verification and repository command surface + +- Status: Implemented baseline command surface. +- Primary decision owner: Root-level generation and verification workflow. +- Selected baseline: `uv` lockfile, Ruff, Pyright, `ty` for fast feedback, pytest (with `pytest-asyncio` and `testcontainers[postgres]`). +- Rationale: A small command surface serves local development, agents, hooks, and CI without adding a task orchestrator. +- Constraints: Pyright remains the authoritative type-checking reference. `ty` is non-authoritative fast feedback. + +### AI integration and credential boundary + +- Status: Adopted by the approved specification. +- Primary decision owner: Lead Scoring and AIHub service boundary. +- Technology: Bounded fixed-pass scoring pipelines; caller-provided `X-Tenant-VKey` held in memory by Lead Scoring and forwarded to AIHub as a Bearer token. +- Rationale: AIHub remains the single owner of model access and quota while Lead Scoring avoids persistent raw V-Key storage and an extra resolver service. +- Constraints: Raw V-Keys must not enter databases, queues, caches, logs, crash dumps, or browser code. Autonomous tools and unbounded agent loops are outside the pilot. + +### Pilot durability and event delivery + +- Status: Selected pattern; persistence baseline adopted with PostgreSQL. +- Primary decision owner: Lead Scoring persistence and delivery boundary. +- Technology: Durable scoring runs plus transactional outbox and registered webhook delivery on PostgreSQL; no dedicated message broker for the pilot. +- Rationale: This preserves run durability and retryable delivery without adding Kafka, RabbitMQ, or another operational subsystem before the workload requires one. +- Open work: Implement retry worker, outbox table schema, and webhook delivery on the adopted PostgreSQL persistence baseline. + +### Pilot scoring execution topology + +- Status: Adopted and implemented for rule-only execution. +- Primary decision owner: Lead Scoring runtime lifecycle. +- Technology: In-process `asyncio` tasks with PostgreSQL conditional claims and expiring run leases. +- Rationale: This is the smallest topology that keeps the raw tenant V-Key in the live attempt only, prevents duplicate claims, and lets database recovery terminalize orphaned work without adding a broker. +- Constraints: A process or task loss cannot resume the attempt because the V-Key is not durable. Recovery commits `worker_interrupted` with `retryable=true` and a terminal outbox event; retry is a new admission action. + +### Frontend toolchain + +- Status: Selected for the internal Dev Console; not implemented. +- Primary decision owner: AI Platform internal console, with Omni owning customer-facing configuration and HITL UI. +- Technology: Svelte 5, TypeScript, Bun, Vite, Vitest, `@sveltejs/vite-plugin-svelte`, `svelte-check`, `@sveltejs/mcp`, ESLint, and Prettier Svelte tooling. +- Rationale: This provides a small client for generated OpenAPI types without adding SvelteKit before routing, SSR, or deployment requires it. +- Constraints: The console must not duplicate Omni business UI. It uses a backend-only internal test V-Key separate from production Omni tenant credentials; raw V-Key never reaches browser code. Add a Svelte language server only if `mcpls` must serve `.svelte` files semantically. + +### Pilot trigger boundary + +- Status: Adopted by the approved specification. +- Production initiation: Manual Omni action and Captain `create_and_score_ems_lead` tool invocation. +- Internal initiation: AI Platform Dev Console using a separate backend-only test V-Key. +- Deferred: Generic automatic input-event triggers such as message-created, conversation-resolved, or EMS lifecycle events. +- Constraint: Completion events from Lead Scoring to Omni remain required and are distinct from deferred input triggers. + +## Evaluated Options + +- TypeScript 7 native compiler and LSP: `candidate` - Run project-locally as `tsc --lsp --stdio` or `tsgo --lsp --stdio`. Do not change global TypeScript, Node, or `mcpls` defaults before a project-local pilot. +- `typescript-language-server`: `current` - Existing `mcpls` backend for TypeScript and JavaScript. Its push-only diagnostics behavior does not satisfy the current `mcpls` pull-diagnostics path; upgrading the wrapper alone is not the fix. +- `svelte-language-server`: `deferred` - Add only if `mcpls` must provide semantic LSP support for `.svelte`. +- SvelteKit: `deferred` - Add only when production routing, SSR, or deployment requires it. +- `oasdiff`: `deferred` - Adopt as a blocking compatibility check after the first reviewed and versioned OpenAPI baseline exists. +- Schemathesis: `deferred` - Adopt when the real ASGI app, declared error responses, deterministic authentication, and external-service overrides exist. +- Dedicated broker such as Kafka or RabbitMQ: `deferred` - Transactional outbox and registered webhooks are sufficient for the Customer Pilot boundary. +- Monorepo orchestrator, second formatter or linter, generic agent framework, or custom build system: `rejected` - No current requirement justifies the maintenance and duplicate source-of-truth cost. + +## Unresolved Decisions + +- Production deployment and service-to-service security: Pilot caller authentication is the approved per-app opaque Bearer service-token pattern; define hosting, network exposure, per-environment secret delivery, rotation/revocation, and operational ownership before Customer Pilot deployment. +- TypeScript 7 adoption: Decide whether TS7 is the project compiler only or also the `mcpls` language-server backend after a project-local pilot verifies build, typecheck, diagnostics, and compatibility with the selected Svelte tooling. diff --git a/old_repo/tests/conftest.py b/old_repo/tests/conftest.py new file mode 100644 index 0000000..5867eac --- /dev/null +++ b/old_repo/tests/conftest.py @@ -0,0 +1,57 @@ +from collections.abc import Generator + +import pytest + +# Attempt import of testcontainers +postgres_container_cls = None +try: + try: + from testcontainers.community.postgres import PostgresContainer as _PostgresContainer + + postgres_container_cls = _PostgresContainer + except ImportError: + from testcontainers.postgres import PostgresContainer as _PostgresContainer + + postgres_container_cls = _PostgresContainer +except ImportError: + postgres_container_cls = None + + +@pytest.fixture(scope="session") +def postgres_container_url() -> Generator[str]: + """Provides a disposable PostgreSQL 16 container URL strictly via Docker testcontainers. + + Scoped to session to avoid duplicating container startup across tests. + No ambient admin or SQLite fallbacks. + """ + if postgres_container_cls is None: + raise RuntimeError( + "testcontainers package is required for PostgreSQL integration tests. " + "SQLite/localhost fallbacks are strictly prohibited." + ) + + try: + container = postgres_container_cls("postgres:16-alpine") + container.start() + host = container.get_container_host_ip() + port = container.get_exposed_port(5432) + user = container.username + password = container.password + dbname = container.dbname + url = f"postgresql+asyncpg://{user}:{password}@{host}:{port}/{dbname}" + try: + yield url + finally: + container.stop() + except Exception as exc: + raise RuntimeError( + f"Failed to start PostgreSQL testcontainer: {exc}. " + "Docker daemon must be running and accessible for integration tests. " + "SQLite/localhost fallbacks are prohibited." + ) from exc + + +@pytest.fixture +def temp_postgres_db(postgres_container_url: str) -> str: + """Yields disposable PostgreSQL container database URL.""" + return postgres_container_url diff --git a/tests/e2e/api/lead-scoring-local.postman_collection.json b/old_repo/tests/e2e/api/lead-scoring-local.postman_collection.json similarity index 100% rename from tests/e2e/api/lead-scoring-local.postman_collection.json rename to old_repo/tests/e2e/api/lead-scoring-local.postman_collection.json diff --git a/tests/e2e/api/lead-scoring-local.postman_environment.json b/old_repo/tests/e2e/api/lead-scoring-local.postman_environment.json similarity index 100% rename from tests/e2e/api/lead-scoring-local.postman_environment.json rename to old_repo/tests/e2e/api/lead-scoring-local.postman_environment.json diff --git a/tests/test_aihub_client.py b/old_repo/tests/test_aihub_client.py similarity index 100% rename from tests/test_aihub_client.py rename to old_repo/tests/test_aihub_client.py diff --git a/tests/test_bounded_profiles_execution.py b/old_repo/tests/test_bounded_profiles_execution.py similarity index 100% rename from tests/test_bounded_profiles_execution.py rename to old_repo/tests/test_bounded_profiles_execution.py diff --git a/tests/test_configurable_demo_backend.py b/old_repo/tests/test_configurable_demo_backend.py similarity index 100% rename from tests/test_configurable_demo_backend.py rename to old_repo/tests/test_configurable_demo_backend.py diff --git a/tests/test_contract_pack_cli.py b/old_repo/tests/test_contract_pack_cli.py similarity index 100% rename from tests/test_contract_pack_cli.py rename to old_repo/tests/test_contract_pack_cli.py diff --git a/tests/test_dynamic_rules.py b/old_repo/tests/test_dynamic_rules.py similarity index 100% rename from tests/test_dynamic_rules.py rename to old_repo/tests/test_dynamic_rules.py diff --git a/tests/test_execution.py b/old_repo/tests/test_execution.py similarity index 100% rename from tests/test_execution.py rename to old_repo/tests/test_execution.py diff --git a/tests/test_health.py b/old_repo/tests/test_health.py similarity index 100% rename from tests/test_health.py rename to old_repo/tests/test_health.py diff --git a/tests/test_medium_execution.py b/old_repo/tests/test_medium_execution.py similarity index 100% rename from tests/test_medium_execution.py rename to old_repo/tests/test_medium_execution.py diff --git a/tests/test_medium_scoring.py b/old_repo/tests/test_medium_scoring.py similarity index 100% rename from tests/test_medium_scoring.py rename to old_repo/tests/test_medium_scoring.py diff --git a/tests/test_migrations.py b/old_repo/tests/test_migrations.py similarity index 100% rename from tests/test_migrations.py rename to old_repo/tests/test_migrations.py diff --git a/tests/test_observability.py b/old_repo/tests/test_observability.py similarity index 100% rename from tests/test_observability.py rename to old_repo/tests/test_observability.py diff --git a/tests/test_openapi_cli.py b/old_repo/tests/test_openapi_cli.py similarity index 100% rename from tests/test_openapi_cli.py rename to old_repo/tests/test_openapi_cli.py diff --git a/tests/test_polling_contract_cli.py b/old_repo/tests/test_polling_contract_cli.py similarity index 100% rename from tests/test_polling_contract_cli.py rename to old_repo/tests/test_polling_contract_cli.py diff --git a/tests/test_retention.py b/old_repo/tests/test_retention.py similarity index 100% rename from tests/test_retention.py rename to old_repo/tests/test_retention.py diff --git a/tests/test_rule_scoring.py b/old_repo/tests/test_rule_scoring.py similarity index 100% rename from tests/test_rule_scoring.py rename to old_repo/tests/test_rule_scoring.py diff --git a/tests/test_scoring_runs_api.py b/old_repo/tests/test_scoring_runs_api.py similarity index 100% rename from tests/test_scoring_runs_api.py rename to old_repo/tests/test_scoring_runs_api.py diff --git a/tests/test_webhook_contract_cli.py b/old_repo/tests/test_webhook_contract_cli.py similarity index 100% rename from tests/test_webhook_contract_cli.py rename to old_repo/tests/test_webhook_contract_cli.py diff --git a/tests/test_webhook_dispatcher.py b/old_repo/tests/test_webhook_dispatcher.py similarity index 100% rename from tests/test_webhook_dispatcher.py rename to old_repo/tests/test_webhook_dispatcher.py diff --git a/old_repo/uv.lock b/old_repo/uv.lock new file mode 100644 index 0000000..2ee5c66 --- /dev/null +++ b/old_repo/uv.lock @@ -0,0 +1,1165 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "alembic" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/2b/e4153978368de59918115c9e01d3ebf58a558a7285efa7e960c383c4b59a/alembic-1.19.1.tar.gz", hash = "sha256:e0fca0518118c78acc493e31bcb5402f190057aaf6df8b5b95ce94c4789cf648", size = 2070816, upload-time = "2026-08-08T16:32:01.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/89/e62cc37b69ad357cc8ecd6e7367f5245f523d3cbb338a66197212bdf6749/alembic-1.19.1-py3-none-any.whl", hash = "sha256:b39018cb3d9413a19cbd54cf3c02ad33998641f0538eb77413a488a21c3e14be", size = 265946, upload-time = "2026-08-08T16:32:03.153Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + +[[package]] +name = "asyncpg" +version = "0.31.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111, upload-time = "2025-11-24T23:26:04.467Z" }, + { url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928, upload-time = "2025-11-24T23:26:05.944Z" }, + { url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067, upload-time = "2025-11-24T23:26:07.957Z" }, + { url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156, upload-time = "2025-11-24T23:26:09.591Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636, upload-time = "2025-11-24T23:26:11.168Z" }, + { url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079, upload-time = "2025-11-24T23:26:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606, upload-time = "2025-11-24T23:26:14.78Z" }, + { url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569, upload-time = "2025-11-24T23:26:16.189Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/e9450d62e84a13aea6580c83a47a437f26c7ca6fa0f0fd40b6670793ea30/asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44", size = 660867, upload-time = "2025-11-24T23:26:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/82/4b/1d0a2b33b3102d210439338e1beea616a6122267c0df459ff0265cd5807a/asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5", size = 638349, upload-time = "2025-11-24T23:26:19.689Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/e7f7ac9a7974f08eff9183e392b2d62516f90412686532d27e196c0f0eeb/asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2", size = 3410428, upload-time = "2025-11-24T23:26:21.275Z" }, + { url = "https://files.pythonhosted.org/packages/6f/de/bf1b60de3dede5c2731e6788617a512bc0ebd9693eac297ee74086f101d7/asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2", size = 3471678, upload-time = "2025-11-24T23:26:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/fc3ade003e22d8bd53aaf8f75f4be48f0b460fa73738f0391b9c856a9147/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218", size = 3313505, upload-time = "2025-11-24T23:26:25.235Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e9/73eb8a6789e927816f4705291be21f2225687bfa97321e40cd23055e903a/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d", size = 3434744, upload-time = "2025-11-24T23:26:26.944Z" }, + { url = "https://files.pythonhosted.org/packages/08/4b/f10b880534413c65c5b5862f79b8e81553a8f364e5238832ad4c0af71b7f/asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b", size = 532251, upload-time = "2025-11-24T23:26:28.404Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2d/7aa40750b7a19efa5d66e67fc06008ca0f27ba1bd082e457ad82f59aba49/asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be", size = 604901, upload-time = "2025-11-24T23:26:30.34Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fe/b9dfe349b83b9dee28cc42360d2c86b2cdce4cb551a2c2d27e156bcac84d/asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2", size = 702280, upload-time = "2025-11-24T23:26:32Z" }, + { url = "https://files.pythonhosted.org/packages/6a/81/e6be6e37e560bd91e6c23ea8a6138a04fd057b08cf63d3c5055c98e81c1d/asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31", size = 682931, upload-time = "2025-11-24T23:26:33.572Z" }, + { url = "https://files.pythonhosted.org/packages/a6/45/6009040da85a1648dd5bc75b3b0a062081c483e75a1a29041ae63a0bf0dc/asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7", size = 3581608, upload-time = "2025-11-24T23:26:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/7e/06/2e3d4d7608b0b2b3adbee0d0bd6a2d29ca0fc4d8a78f8277df04e2d1fd7b/asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e", size = 3498738, upload-time = "2025-11-24T23:26:37.275Z" }, + { url = "https://files.pythonhosted.org/packages/7d/aa/7d75ede780033141c51d83577ea23236ba7d3a23593929b32b49db8ed36e/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c", size = 3401026, upload-time = "2025-11-24T23:26:39.423Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7a/15e37d45e7f7c94facc1e9148c0e455e8f33c08f0b8a0b1deb2c5171771b/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a", size = 3429426, upload-time = "2025-11-24T23:26:41.032Z" }, + { url = "https://files.pythonhosted.org/packages/13/d5/71437c5f6ae5f307828710efbe62163974e71237d5d46ebd2869ea052d10/asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d", size = 614495, upload-time = "2025-11-24T23:26:42.659Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584, upload-time = "2026-08-15T08:17:33.597Z" }, + { url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359, upload-time = "2026-08-15T08:17:35.022Z" }, + { url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464, upload-time = "2026-08-15T08:17:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676, upload-time = "2026-08-15T08:17:37.819Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473, upload-time = "2026-08-15T08:17:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156, upload-time = "2026-08-15T08:17:40.66Z" }, + { url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246, upload-time = "2026-08-15T08:17:42.003Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660, upload-time = "2026-08-15T08:17:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354, upload-time = "2026-08-15T08:17:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638, upload-time = "2026-08-15T08:17:46.496Z" }, + { url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583, upload-time = "2026-08-15T08:17:47.963Z" }, + { url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038, upload-time = "2026-08-15T08:17:49.481Z" }, + { url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677, upload-time = "2026-08-15T08:17:50.845Z" }, + { url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491, upload-time = "2026-08-15T08:17:52.61Z" }, + { url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196, upload-time = "2026-08-15T08:17:54.023Z" }, + { url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660, upload-time = "2026-08-15T08:17:55.491Z" }, + { url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618, upload-time = "2026-08-15T08:17:57.025Z" }, + { url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362, upload-time = "2026-08-15T08:17:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755, upload-time = "2026-08-15T08:18:00.037Z" }, + { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295, upload-time = "2026-08-15T08:18:01.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856, upload-time = "2026-08-15T08:18:02.929Z" }, + { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" }, + { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" }, + { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" }, + { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" }, + { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" }, + { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" }, + { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" }, + { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" }, + { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" }, + { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" }, + { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" }, + { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048, upload-time = "2026-08-15T08:19:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997, upload-time = "2026-08-15T08:19:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014, upload-time = "2026-08-15T08:19:03.943Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174, upload-time = "2026-08-15T08:19:05.598Z" }, + { url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361, upload-time = "2026-08-15T08:19:07.251Z" }, + { url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143, upload-time = "2026-08-15T08:19:08.816Z" }, + { url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086, upload-time = "2026-08-15T08:19:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231, upload-time = "2026-08-15T08:19:11.807Z" }, + { url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546, upload-time = "2026-08-15T08:19:13.416Z" }, + { url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033, upload-time = "2026-08-15T08:19:14.981Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045, upload-time = "2026-08-15T08:19:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866, upload-time = "2026-08-15T08:19:18.101Z" }, + { url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932, upload-time = "2026-08-15T08:19:19.734Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320, upload-time = "2026-08-15T08:19:21.37Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174, upload-time = "2026-08-15T08:19:23.088Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126, upload-time = "2026-08-15T08:19:24.471Z" }, + { url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441, upload-time = "2026-08-15T08:19:26.038Z" }, + { url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742, upload-time = "2026-08-15T08:19:27.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298, upload-time = "2026-08-15T08:19:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500, upload-time = "2026-08-15T08:19:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888, upload-time = "2026-08-15T08:19:32.458Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243, upload-time = "2026-08-15T08:19:33.94Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871, upload-time = "2026-08-15T08:19:35.413Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580, upload-time = "2026-08-15T08:19:36.886Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807, upload-time = "2026-08-15T08:19:38.392Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083, upload-time = "2026-08-15T08:19:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317, upload-time = "2026-08-15T08:19:41.53Z" }, + { url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173, upload-time = "2026-08-15T08:19:43.07Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960, upload-time = "2026-08-15T08:19:44.587Z" }, + { url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186, upload-time = "2026-08-15T08:19:46.293Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947, upload-time = "2026-08-15T08:19:47.853Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909, upload-time = "2026-08-15T08:19:49.383Z" }, + { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" }, + { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" }, + { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" }, + { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" }, + { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" }, +] + +[[package]] +name = "click" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "docker" +version = "7.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/7f/731ff914b0255d3d065f45fd4e626d4b8c95dbcbaada049f337a6ac16410/docker-7.2.0.tar.gz", hash = "sha256:cebb93773d334f778e023a7ee352a8d6e13ab1bd3b863a4d4a59dec897df43ac", size = 118731, upload-time = "2026-07-09T14:53:46.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/23/529140fe1aab80fc6992f93a706deec709140a6397439139a054e1515c45/docker-7.2.0-py3-none-any.whl", hash = "sha256:a3f45fdeb9165e2d25d9a1d02ddf3bc70fb572cf5ebbf9b58558c22caf29b71f", size = 148775, upload-time = "2026-07-09T14:53:45.224Z" }, +] + +[[package]] +name = "fastapi" +version = "0.141.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" }, +] + +[[package]] +name = "greenlet" +version = "3.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/d8/7cc97c142388aef03f622e001c572c4f84e9252a439549d483f555771970/greenlet-3.5.5.tar.gz", hash = "sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c", size = 207585, upload-time = "2026-08-10T15:09:36.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/3d/8cef5f724ec0d4add2af8961d504535ec60c3cca9e464f6d03bdba29d85b/greenlet-3.5.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa", size = 294730, upload-time = "2026-08-10T13:27:51.206Z" }, + { url = "https://files.pythonhosted.org/packages/88/4b/8e7aa3f514273aecff30a16ab1bac09ff54cfc7e6860fdd8058c37ff2499/greenlet-3.5.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed", size = 614536, upload-time = "2026-08-10T14:14:36.589Z" }, + { url = "https://files.pythonhosted.org/packages/85/48/4e95e9dd5a8a397dc6a6345dd7f1935113d0fca4f85e89d3976da9cd988d/greenlet-3.5.5-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef", size = 626924, upload-time = "2026-08-10T14:27:27.048Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/eaa476d6bf3816828d0d70e80dcc36bf30a058233bd889e707e693f6e860/greenlet-3.5.5-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7278591501941bb2456af102bb9cd59aab48c6cfd6e2dd68fa1290bb0c49a42", size = 632726, upload-time = "2026-08-10T14:30:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/89/5d/398a1c71fa7a277deeb376c999979de6786f08fc2d5747a0b9d6e11738dd/greenlet-3.5.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0", size = 623906, upload-time = "2026-08-10T13:40:50.501Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f2/0cc2849ede68579291e9c59b3ab6ec1958f98681cca5b14d8fc75bf674a4/greenlet-3.5.5-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:4dfc7c4470354e7b09184d1a3a985761053a2fd694ddb5b5c80242afc2c8c90b", size = 434966, upload-time = "2026-08-10T14:30:03.729Z" }, + { url = "https://files.pythonhosted.org/packages/04/1b/745450fc5ea9e0cb17d840d248f284db3363de736d362c7d2d883e3eadba/greenlet-3.5.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537", size = 1581430, upload-time = "2026-08-10T14:15:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/d4/29/d51b296e3191bb15d3d81ec375af1909e4466c0f395d744ed475801798a9/greenlet-3.5.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e", size = 1645684, upload-time = "2026-08-10T13:40:32.133Z" }, + { url = "https://files.pythonhosted.org/packages/12/63/369f1a1625e64e9e31df3963c6044056e3fdfa3fa3fdba3c54ffefa6e987/greenlet-3.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd", size = 324075, upload-time = "2026-08-10T13:26:58.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/78/649cb5c09d4d81f6dd1444e75474a7206784743283a21d24171562ac4899/greenlet-3.5.5-cp313-cp313-win_arm64.whl", hash = "sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc", size = 308260, upload-time = "2026-08-10T13:27:50.795Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/080e881fa2be95ff1ddbd6994b2bab3b1a78df3b3fcab39306011764fcc7/greenlet-3.5.5-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d", size = 295309, upload-time = "2026-08-10T13:26:03.032Z" }, + { url = "https://files.pythonhosted.org/packages/25/cc/0ac614e6586c0e42d4cc281a5819150f4f43685744a4c5ff77139286409d/greenlet-3.5.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328", size = 661185, upload-time = "2026-08-10T14:14:37.867Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b9/6808725354be8ad305dfe5172377664fc9642d4fc043be246b3314cf4482/greenlet-3.5.5-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926", size = 673419, upload-time = "2026-08-10T14:27:28.652Z" }, + { url = "https://files.pythonhosted.org/packages/eb/52/f005d579acde46c3d1cc3cab1c9f3d5708c8a3006a4120e8cf5da801afe9/greenlet-3.5.5-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d98ef6f92e67c6dbf299dbfd8facc1b0d2d9cedf91e325e73b3d0373fe4309d8", size = 677863, upload-time = "2026-08-10T14:30:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/42/2e/40c509967da7f254680826a2fa0dd22138ec79946c70b97542d74cde8b43/greenlet-3.5.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e", size = 670822, upload-time = "2026-08-10T13:40:51.833Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8a/a75f8a2bdcef3c358a3147cdc9db3aa83755f0a038f766ab0bedb66f512c/greenlet-3.5.5-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:159df1942d88e8f784cbb38d6f18bdb365cd11319cfbb3e89623de2b97892d53", size = 480554, upload-time = "2026-08-10T14:30:05.171Z" }, + { url = "https://files.pythonhosted.org/packages/2d/22/c3c2eee4a8fe191d6d1d183086c56133d646024e3d70bfd414829f64560b/greenlet-3.5.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc", size = 1628469, upload-time = "2026-08-10T14:15:08.11Z" }, + { url = "https://files.pythonhosted.org/packages/f7/87/25babd09b94cb1f03e71db815fde463f0262e40cfbd953d58a8d77311351/greenlet-3.5.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9", size = 1691952, upload-time = "2026-08-10T13:40:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/5cc9701117ea4dc0eb7bf1f4f9b7888a6e2e5277ddfae095805ace50f2b6/greenlet-3.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1", size = 327458, upload-time = "2026-08-10T13:27:02.868Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6b/594fa2de7fae7629168a404a4305d7d7e31a5742c50a801b1839543cb93d/greenlet-3.5.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e2afcfc4d4305dd715809b03da5cbe437c8984f61d8917751eb5fe4aefa3e07", size = 311146, upload-time = "2026-08-10T13:27:25.046Z" }, + { url = "https://files.pythonhosted.org/packages/24/e0/50cd600b469e5734c72709b6b1838b6bc63f307b573c772c3132d6ecfe92/greenlet-3.5.5-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277", size = 305471, upload-time = "2026-08-10T13:26:20.568Z" }, + { url = "https://files.pythonhosted.org/packages/75/a3/77acd66dfc6387b5219b2080806c0cabb73c10eb1bb44b413c40a62015ba/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b", size = 672470, upload-time = "2026-08-10T14:14:39.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/71/0d178142dca3ec19f46fb2212ae73d30ad53b9d548dc64804086033a7089/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272", size = 679973, upload-time = "2026-08-10T14:27:30.072Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ac/0d7887aa4bbfc9eba075cc428244dfc96f623478454d5ec81180d0d6bd5a/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e9ec2e7c98e895fcea0c5cc57b2606cf86ece6d0a56578f3eb225e2af4f0387", size = 681587, upload-time = "2026-08-10T14:30:13.519Z" }, + { url = "https://files.pythonhosted.org/packages/6e/31/46eb8567302eaf787abf88d09df014e14ae3baf460af1b8b0efdbd3efcd5/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476", size = 676634, upload-time = "2026-08-10T13:40:53.004Z" }, + { url = "https://files.pythonhosted.org/packages/4f/18/8d58ba1c429b0383e3219a3d0e0bba241d0444d8ed05b73349953c7d7c7b/greenlet-3.5.5-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:102817506f6090b5176c746a82603341a549b40e5c3d5b72a4c672228a918c41", size = 510175, upload-time = "2026-08-10T14:30:07.047Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e9/b88bbf5b29970cb84172dc2c32aa3e5e579ceb94c808e81c826454138850/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874", size = 1637320, upload-time = "2026-08-10T14:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/6d/8c/7631ed29cc6f0392f11830076e172ce4885e70b0bc2c1bce1731176d4b4e/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71", size = 1697412, upload-time = "2026-08-10T13:40:34.924Z" }, + { url = "https://files.pythonhosted.org/packages/da/0f/f7dd935f9c4cb1be49098770587f54d8a78518e55c89bce86c4fb4109057/greenlet-3.5.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0", size = 331514, upload-time = "2026-08-10T13:29:20.611Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e5/681b01f8fbc1b55232822f99e8f8afeb78a55a7c76a7bf9dbdc7ccb03a6d/greenlet-3.5.5-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206", size = 295975, upload-time = "2026-08-10T13:28:45.985Z" }, + { url = "https://files.pythonhosted.org/packages/11/f2/69b488cd9e7267bf4b0fe8cdebf25d8d6df680d21bdf41150d23e23d6652/greenlet-3.5.5-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad", size = 666823, upload-time = "2026-08-10T14:14:40.222Z" }, + { url = "https://files.pythonhosted.org/packages/84/d4/d5bc2fdebbdda0c94555925ba79948b8395d75a7f6a36cc85dce5bab9f11/greenlet-3.5.5-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0", size = 677613, upload-time = "2026-08-10T14:27:31.543Z" }, + { url = "https://files.pythonhosted.org/packages/65/53/4e13642efc4d7ad6554ecb2242a5be42666b2e1a067323e88dfc0124a04b/greenlet-3.5.5-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37faa97daccb6d9f4c2141ce3118d023c3c5506864a7d8bdf726f665018c1f76", size = 681436, upload-time = "2026-08-10T14:30:14.839Z" }, + { url = "https://files.pythonhosted.org/packages/bd/93/542d8a3a90f3b35c6ad8bf7e56a03010287f2cafa289a5b7985b5207db39/greenlet-3.5.5-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552", size = 675930, upload-time = "2026-08-10T13:40:54.205Z" }, + { url = "https://files.pythonhosted.org/packages/cd/32/188447c9a468d6977d2989397226b0c6b65ab6f4cf943f931643328512fc/greenlet-3.5.5-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:b18007dc2473a7942fd157366b55f01da6fed7ce85318591005b419e0a439474", size = 487404, upload-time = "2026-08-10T14:30:08.903Z" }, + { url = "https://files.pythonhosted.org/packages/52/b5/89c9f2e8460d71101037d47a1feed11928615a5edd42370be290e0657eeb/greenlet-3.5.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007", size = 1633878, upload-time = "2026-08-10T14:15:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/b8/60/297de93f3b02ac78a5e04d32bb8bbe3080f4a73d8ed95016561463b70618/greenlet-3.5.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773", size = 1696597, upload-time = "2026-08-10T13:40:36.252Z" }, + { url = "https://files.pythonhosted.org/packages/18/25/54c6eaff4f337fb670215e89eb2d00d9499487b658e709d4b477be4a342e/greenlet-3.5.5-cp315-cp315-win_amd64.whl", hash = "sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e", size = 327700, upload-time = "2026-08-10T13:28:06.752Z" }, + { url = "https://files.pythonhosted.org/packages/67/67/857e88a36301caa0e029870132c2478bd55d896630321432afab03a3115f/greenlet-3.5.5-cp315-cp315-win_arm64.whl", hash = "sha256:2d57406c3efd32d7a81e17a674314e8bd00792cdab49ea3228a49aa1bfb2e769", size = 311750, upload-time = "2026-08-10T13:34:08.815Z" }, + { url = "https://files.pythonhosted.org/packages/10/e2/3144c0a116067ac1e30457b0139a94d60d1d36a86e015de68e9ac87cb3bc/greenlet-3.5.5-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c", size = 306387, upload-time = "2026-08-10T13:27:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a1/cb4223a7e9b9f43b8807e8eb212358bfe2dfaa174a9ea2889eb1714dcba2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6", size = 676472, upload-time = "2026-08-10T14:14:41.417Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cd/a154b4498e5d8f12ada291cfb3b8d596eadde2177f5bf09a9be699d2a446/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae", size = 684238, upload-time = "2026-08-10T14:27:32.946Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f4/e450a68a152f819491d8c7df6a8254e761d87e6a78759268961f8c5bd4dd/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2888a3a38bc5ee5bb6c438372197152e815837e4fab7ed7a1f86ef18ffd58ad1", size = 686022, upload-time = "2026-08-10T14:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/bf/bb/b0031d260c2968a3c87deebc51d80c64e499377f993aafe06ee3b7488cc2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3", size = 681246, upload-time = "2026-08-10T13:40:55.402Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/17e63d6bf3b9c9b9dbea981b7f643a71f79603bdfb4f1c3a9cf353e22aed/greenlet-3.5.5-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:1e8d9391fe77f15649589a907cef972dbbd6352ef7ff7dc0492f658c0c26495f", size = 516951, upload-time = "2026-08-10T14:30:10.907Z" }, + { url = "https://files.pythonhosted.org/packages/9a/07/da554b71ab88e649da146e1065d86a48a5c5d92e50ab74ef41b504aa7f56/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0", size = 1642735, upload-time = "2026-08-10T14:15:11.92Z" }, + { url = "https://files.pythonhosted.org/packages/78/76/26a3782a051677668af9d92beaa47cd87ba9dd5072f762961144a03dd4c6/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5", size = 1700925, upload-time = "2026-08-10T13:40:37.656Z" }, + { url = "https://files.pythonhosted.org/packages/28/d9/fe7baf4190c2ae71f267efb9de21b3172bb35bc0ed1ef53dd6027d658e33/greenlet-3.5.5-cp315-cp315t-win_amd64.whl", hash = "sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8", size = 331829, upload-time = "2026-08-10T13:26:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/df/af/419a4e383bd600858a9b67e9b280a60fdc383ee3f2fe5b6c0c1ef04e74d1/greenlet-3.5.5-cp315-cp315t-win_arm64.whl", hash = "sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b", size = 315093, upload-time = "2026-08-10T13:29:34.949Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" }, + { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" }, + { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" }, + { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" }, + { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" }, + { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" }, + { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, +] + +[[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 = "lead-scoring" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "alembic" }, + { name = "asyncpg" }, + { name = "fastapi" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "sqlalchemy", extra = ["asyncio"] }, + { name = "uvicorn", extra = ["standard"] }, +] + +[package.dev-dependencies] +dev = [ + { name = "pyright" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "ruff" }, + { name = "testcontainers" }, + { name = "ty" }, +] + +[package.metadata] +requires-dist = [ + { name = "alembic", specifier = ">=1.13.0" }, + { name = "asyncpg", specifier = ">=0.29.0" }, + { name = "fastapi", specifier = ">=0.115.0" }, + { name = "httpx", specifier = ">=0.27.0" }, + { name = "pydantic", specifier = ">=2.8.0" }, + { name = "pydantic-settings", specifier = ">=2.4.0" }, + { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.30" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.30.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pyright", specifier = ">=1.1.370" }, + { name = "pytest", specifier = ">=8.0.0" }, + { name = "pytest-asyncio", specifier = ">=0.23.0" }, + { name = "ruff", specifier = ">=0.5.0" }, + { name = "testcontainers", extras = ["postgres"], specifier = ">=4.0.0" }, + { name = "ty", specifier = ">=0.0.74" }, +] + +[[package]] +name = "mako" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/12/b5fa2353e2754cd67fb9f83793fa48ff42c213a5da7e719869d2301f6ab8/mako-1.4.1.tar.gz", hash = "sha256:d7904710b662996425a21627710c4777c45053146942cf8a7aebf757c92b8c27", size = 410165, upload-time = "2026-08-05T06:10:56.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/54/12ed58d458474aaab5c3d180173e745a4fe131bb330370596876d19ff60f/mako-1.4.1-py3-none-any.whl", hash = "sha256:a359d9a94a541213958742b2698d0a7757bb83551767bc468a74b9905aba9617", size = 80010, upload-time = "2026-08-05T06:10:58.248Z" }, +] + +[[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 = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.411" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/53/ed9d74092561d4b01a2ef1349d52cdbc135e526c245f366b089cfca6de49/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35", size = 58945, upload-time = "2026-08-16T16:54:54.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780, upload-time = "2026-08-16T16:54:52.473Z" }, +] + +[[package]] +name = "pywin32" +version = "312" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" }, + { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" }, + { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/8f/d8074b1f25e003164087a8bfe79a0f1a3945135764dbb6aaab04103dcaf9/ruff-0.16.4.tar.gz", hash = "sha256:13171aa9d9af2240ee3504e639de73122c67e74036de5ba2e1d01422cd17e3dc", size = 4899731, upload-time = "2026-08-20T17:43:59.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/80/779895ef584e089d22f2c6df0d0e99a65ec2df0805f1fffd439415b8c1f0/ruff-0.16.4-py3-none-linux_armv6l.whl", hash = "sha256:df4075f71ddac40b9934af60c3ec8a53047dd5a5fdc43224e6e4e8e9a27cb6f7", size = 10006909, upload-time = "2026-08-20T17:43:16.888Z" }, + { url = "https://files.pythonhosted.org/packages/a9/e6/f553199b5e8927a05cb5c422d921fd0656b29ab976e91c44802107c6b0da/ruff-0.16.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0c95538517af68004306b0fb3214ff2f2af67a65092aee77cd9eb86db6656604", size = 10240201, upload-time = "2026-08-20T17:43:19.337Z" }, + { url = "https://files.pythonhosted.org/packages/1c/70/4a6dc4bb34da4dee35e30f09bbd1bfbdd26f33b62fb9b8df31f08a199cd2/ruff-0.16.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:963f83df8e69e575b64d67dd447ebbc917db41a14bf38d4593a4183e7aaa8255", size = 9835122, upload-time = "2026-08-20T17:43:21.708Z" }, + { url = "https://files.pythonhosted.org/packages/24/12/c6e22d686372c15bcb7af99831f1a1be96df696491babf4f24e4f942c527/ruff-0.16.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32a5057c7ff3f6e6480a48fccfb3a412a690f48a3d03ac5cf08177d6c2da3ade", size = 9977162, upload-time = "2026-08-20T17:43:24.236Z" }, + { url = "https://files.pythonhosted.org/packages/46/49/72b10ec912f5ab5854992eaf7aa7cd36729b6937d9dc4e0fb41b3bf428ec/ruff-0.16.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3dce8d9b0c57c265b91885a66a567d8ea1372e8eb4e250fa8e5e3f579e99cff", size = 9829789, upload-time = "2026-08-20T17:43:26.966Z" }, + { url = "https://files.pythonhosted.org/packages/fa/80/0f30e32e7f6ee26edc39075502db9d368d788a44a79b55f763eb4ab03796/ruff-0.16.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7dc651db49283c69f8e72c834eec4fe5573e4c646856aebece0ce385dceb2a80", size = 10527949, upload-time = "2026-08-20T17:43:29.384Z" }, + { url = "https://files.pythonhosted.org/packages/52/3d/86e8ad3542169e56cac3859a343afdb9df2ad54d35a59ce1e67baee83421/ruff-0.16.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3817b87dbcabc92f13b05019257c5b89b5b4d51b5fb20f56fb5235ceb723cd07", size = 11333695, upload-time = "2026-08-20T17:43:31.872Z" }, + { url = "https://files.pythonhosted.org/packages/d0/16/481c29b380c20a0054a8261066665e1b3488e23636c49d0a43e75975b9bb/ruff-0.16.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9fce1499134b2c8c68e5166f95705a5812062bb93aacc5f9873bb1a27084bc7", size = 10727741, upload-time = "2026-08-20T17:43:34.596Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b6/56bc0b8cf45b54b28b3a5e6381c8945d51b5b18adf659454c32295209a31/ruff-0.16.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2d812e482f5a7e02eee26cd73d2a37ebbdf47d795ea63ba1b89110ae93e9fb3", size = 10286522, upload-time = "2026-08-20T17:43:37.288Z" }, + { url = "https://files.pythonhosted.org/packages/e8/8b/b345b4fb110f2fbe2bd31eabd271e5e8b3b7e4ee6c0e02f2dc6be78db000/ruff-0.16.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6baaf984aa7976edf93d3b627fe2d1d22ee94bbca05fa6f90fc76d73924e3454", size = 10584182, upload-time = "2026-08-20T17:43:39.984Z" }, + { url = "https://files.pythonhosted.org/packages/29/e5/827b34041c35f58774a9681a4213994c164fc987800f4dddabcf451da0bf/ruff-0.16.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bdfcf0b28662eb890372d50f92c283bb94e67e7635ed93c7fd533970acff7b2b", size = 10134195, upload-time = "2026-08-20T17:43:42.351Z" }, + { url = "https://files.pythonhosted.org/packages/0f/10/d0bffcdd6729b87afc82ba0ef377173356a7dc8e972f5179968cf2fdf98c/ruff-0.16.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b66b02cb9b04f537643cadf5768e5f98dc461890d530cb67113d71c8c76e605d", size = 9825821, upload-time = "2026-08-20T17:43:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/f5/32/0db2a863b796ca62d83e92a07a3ccf00921b14db02059347576a2fda3d4b/ruff-0.16.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8528bf9a4b291a60bf02ea453511e8ce6215bd2b982ee80405b66b008b6c30a0", size = 10267658, upload-time = "2026-08-20T17:43:46.989Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a0/fbdeb59e48c6261f523e56c8f12e9c08fbe693786595cc7e3959207a9232/ruff-0.16.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fbd85d2875fdd67e833213a651f613bbf25303abf6aa822a5121f4531195678d", size = 10697071, upload-time = "2026-08-20T17:43:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/aa/28/0c6dd865859c6d17bc8ccc34cb72b0e02d6c7eb25e8a1e22b5bea681e2c0/ruff-0.16.4-py3-none-win32.whl", hash = "sha256:312769988007aaeb8e189b443ccdd03c0e6374489e053467be6d96518ebff76e", size = 10021687, upload-time = "2026-08-20T17:43:52.281Z" }, + { url = "https://files.pythonhosted.org/packages/a3/03/e724450f621698117f9aa6dd241c94d0274ae96781378dc86745ae29f0e7/ruff-0.16.4-py3-none-win_amd64.whl", hash = "sha256:05d9d27a18c4bcbefada602480ec9e01e0bc949d432e0ced5df77edac195919c", size = 10567657, upload-time = "2026-08-20T17:43:54.78Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fe/da8b9e1347696bb22120b77280ec5ce25d500ca5cb39d5ad6e5c18de19c1/ruff-0.16.4-py3-none-win_arm64.whl", hash = "sha256:a3a61621c9b6f6a89573e938a080e648f1695baa3f58570a3a707bc51ff65a21", size = 10451579, upload-time = "2026-08-20T17:43:57.135Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.52" +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/3b/21/77b4c147963073040dc3c3a5cb7a8c3001a1893c0209432cb77f9df836aa/sqlalchemy-2.0.52.tar.gz", hash = "sha256:5e2d46356ac2ccb7d268ab6c2319ac6a2b42f1b8d5fd8bd3d46855cd82abee97", size = 9945637, upload-time = "2026-08-11T19:07:09.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/18/e30c6fe1eca1bf34a39fbdd6066121cc9974c850faf6f349eac563697a26/sqlalchemy-2.0.52-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2eb3c6a64b1bfe6704777cfd504e7b8ad093a5f3e03ce67663a5e6742f294e43", size = 2167724, upload-time = "2026-08-11T20:58:12.679Z" }, + { url = "https://files.pythonhosted.org/packages/d0/56/2e17d161a4f7ecc1c2ffb93e607b4e1898bb551b451b283235acb8f6ce47/sqlalchemy-2.0.52-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:923bb183c1dc64fdf7b717965e3d59938ec4f8b8710b419a21ce403e5da9a9e1", size = 3321189, upload-time = "2026-08-11T21:02:41.932Z" }, + { url = "https://files.pythonhosted.org/packages/cf/b8/8490916e893f3f8d74dc9cc54c078619364999dee37047a188e73abbc852/sqlalchemy-2.0.52-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:651d6d8782e80679e6151707c7b490834d46ada526328895abf567f25e63d29c", size = 3338185, upload-time = "2026-08-11T21:17:02.597Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f7/752cc8ee453da222829b3f5c4613614bf750d97429363b70414fa10478e4/sqlalchemy-2.0.52-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b08cddb8989775e3c88799d86704bdfc3ee6e9846118201aa5997f16f27e3a15", size = 3271698, upload-time = "2026-08-11T21:02:43.963Z" }, + { url = "https://files.pythonhosted.org/packages/51/e6/074ade0c07b9e4c8e8bca46820320ed94df9702afdb6f2af06623068d2e6/sqlalchemy-2.0.52-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab66fa9618269390d4dfa222f2f2f88f7bc4bf5da13905131b818217db7e8057", size = 3308936, upload-time = "2026-08-11T21:17:04.172Z" }, + { url = "https://files.pythonhosted.org/packages/66/07/557c0d04716705599227945ac14e0a17ad0338e899f37d8c2ddff4dcc663/sqlalchemy-2.0.52-cp313-cp313-win32.whl", hash = "sha256:c63bda077685c85ca513286547a531ba57e7a68cf0a7ed3bafcc2bbd18896f4d", size = 2127308, upload-time = "2026-08-11T21:14:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/96/4e/226eda27654318ce525d043025221f689abef883da2c7126f9065121618c/sqlalchemy-2.0.52-cp313-cp313-win_amd64.whl", hash = "sha256:9876b09b9f1ce7398b0ffece585c0a911244c53191187341f6bcae640e133751", size = 2153876, upload-time = "2026-08-11T21:14:55.527Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f5/71cb30af58c9b80a4e1fac0b73bb48f86d497a774a6a2eb6d2f1e657bb73/sqlalchemy-2.0.52-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:410d52be41d17f1a236d19520fbe776257dc16516ed06bd16d433311842aefd9", size = 2169537, upload-time = "2026-08-11T20:58:13.855Z" }, + { url = "https://files.pythonhosted.org/packages/4c/93/d07ebd645d1b07b6b5ed63450a70f063a346a7e0f2c8810daf2e532400cb/sqlalchemy-2.0.52-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfe9ce533dbe4d0a2ae1486546619bd30b76bcd670539a44d910361376175f5e", size = 3319606, upload-time = "2026-08-11T21:02:45.829Z" }, + { url = "https://files.pythonhosted.org/packages/ae/5c/290c84c7c2566ecd3b65baaae0fddec9bc33b033b398a06123bb86fbfc6e/sqlalchemy-2.0.52-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:812bae5138bfc0aa46fb0686da0fc7f581f68e2bbb05bc24c3713bebaedd1437", size = 3323642, upload-time = "2026-08-11T21:17:05.675Z" }, + { url = "https://files.pythonhosted.org/packages/13/f5/2cc160590ca49173359557880b92a0572293ccb899e8f6cedf150c5a3ddf/sqlalchemy-2.0.52-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:50bff43b632a56fbf5ed9afdd76307e1512b62051bcd5afb341ae67205bbb6c8", size = 3268125, upload-time = "2026-08-11T21:02:47.649Z" }, + { url = "https://files.pythonhosted.org/packages/35/f3/ea8933fc9f7d1353e9c2ff9965eae687c4cef181120574591ed2fa0633e1/sqlalchemy-2.0.52-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:49565daf5af554f538e23aef1fc81a95a4e49658f152285e45c02f5fc44f04cd", size = 3289516, upload-time = "2026-08-11T21:17:07.267Z" }, + { url = "https://files.pythonhosted.org/packages/45/67/05cf86541c1e1716fca1e4a996954a439cd74501707cda607fb7cb02ef50/sqlalchemy-2.0.52-cp314-cp314-win32.whl", hash = "sha256:ab9da41e61b9979b910499d633b241df20c51ee5037e5405b11c2faac3cbe1a2", size = 2130249, upload-time = "2026-08-11T21:14:57.273Z" }, + { url = "https://files.pythonhosted.org/packages/96/d7/8ac6ffa1e36169e762ef65bd835046abb2251b1bc17f8f6708e14ed8d31f/sqlalchemy-2.0.52-cp314-cp314-win_amd64.whl", hash = "sha256:a593db51b3bae75db17a5738ad5f992244b3a03863f83c28117ee482c6a3f76d", size = 2156718, upload-time = "2026-08-11T21:14:58.667Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4b/e01a737eef378e734cc6394a82248a6ce13b167dfa36c731075ce9fc9c64/sqlalchemy-2.0.52-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1e61d08bdf4ee2f41024569e3400de7d6734ba498144766b11260936ccfa582", size = 2190344, upload-time = "2026-08-11T19:53:21.393Z" }, + { url = "https://files.pythonhosted.org/packages/b3/3f/3582293d1e185e71d19d7c731c3e2ee20ba21981c4a1115c0806c1f62120/sqlalchemy-2.0.52-py3-none-any.whl", hash = "sha256:3b81b8363a919ce53453591cdb93702e6bd54ade6c4fa2f468fc053baee5ed89", size = 1950700, upload-time = "2026-08-11T20:47:21.603Z" }, +] + +[package.optional-dependencies] +asyncio = [ + { name = "greenlet" }, +] + +[[package]] +name = "starlette" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969, upload-time = "2026-08-08T18:27:57.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" }, +] + +[[package]] +name = "testcontainers" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docker" }, + { name = "python-dotenv" }, + { name = "typing-extensions" }, + { name = "urllib3" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/13/2cc466bddf26d0085f30a2b2bd56b7f8708b54a54db833eec97c5c69129b/testcontainers-4.15.0.tar.gz", hash = "sha256:085cde086337632e19002719460b7b80bbab2bdd51bb3ea04f77d0de96504706", size = 95340, upload-time = "2026-07-24T23:08:01.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/7e/424aac8b355597835deb333e757a0e94b5ccf38ad00f07fe6ed1f4e17c88/testcontainers-4.15.0-py3-none-any.whl", hash = "sha256:8796c14e76604031ad39cf0ed3b8e9806283a1fbf5270965c2b1c594caa31b74", size = 160771, upload-time = "2026-07-24T23:08:00.13Z" }, +] + +[[package]] +name = "ty" +version = "0.0.74" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/0f/c767853e88567a2ec7e996dd95e3105b1bc62c95d103689311ef0f4a603c/ty-0.0.74.tar.gz", hash = "sha256:da14344fc8625fc9ff359bafb856ad575636ea86d9bb6a629b146bff27b380e6", size = 6786318, upload-time = "2026-08-22T15:05:54.054Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/95/6ded58bc97885c6d88fa1f9cd815031489200738f961cbf0466663213f80/ty-0.0.74-py3-none-linux_armv6l.whl", hash = "sha256:8969ef4e508debf00cf58f9ea85a539f799b1732c59cdfcecd037630b9755b30", size = 12790043, upload-time = "2026-08-22T15:05:05.015Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8a/5e323603b6ab8731144421877ee8a0f8ac5a5511e67857127caa09f6730e/ty-0.0.74-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:51fb6cf5b98e1e1140825b2430943f78d744876a735231656eafbb4c3f7eca3c", size = 12371748, upload-time = "2026-08-22T15:05:08.609Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/ee72e08cb705281e8d8c42917dd577aa598a8a098008495fda5176ee3f6e/ty-0.0.74-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ebe60b1f0a948c793d6c77fc9e9ddda599e4f023c04ab16e8e03bcb428c3fa0", size = 12282403, upload-time = "2026-08-22T15:05:11.448Z" }, + { url = "https://files.pythonhosted.org/packages/da/b3/fd935b694ff68bc278af50f7ad04770b36ce6306399baef7e1847b553a9d/ty-0.0.74-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa97f407a695c890a53615966a663c7d2167e2cabe88db7ca1a24d62635cdfc8", size = 12345164, upload-time = "2026-08-22T15:05:14.19Z" }, + { url = "https://files.pythonhosted.org/packages/54/5c/5b5825268e029ebb164c909780103dbbae367f069801410068bf1cef29b3/ty-0.0.74-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:673ddb733d4a0db31385ba1ed9ff1f6bd9dc5565413ce57b1ca5ac4c7803da5d", size = 12556646, upload-time = "2026-08-22T15:05:16.994Z" }, + { url = "https://files.pythonhosted.org/packages/56/e7/515914e571d62ce0101744fed3f881936eeb1b30dc37beb72b4f7ca1e289/ty-0.0.74-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1028e7c6b4f6145e9704552f43a5fffdcd51b42263ffdcd9c9677762bc395a4a", size = 13311653, upload-time = "2026-08-22T15:05:20.254Z" }, + { url = "https://files.pythonhosted.org/packages/b0/07/d1452babb6f9266c2122cabc095180b70ed306fb770b2996753814d2237d/ty-0.0.74-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79841a8890493021fb308772474983316eb91f7b56cb227a6a05a06b262a36f0", size = 13768284, upload-time = "2026-08-22T15:05:23.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/60/8d4a2fc7842a47210a1cb0a16a187d9de39ad5d509a00fb74c1c073afcde/ty-0.0.74-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94859d321f3c6a6c8f7bfc3f40e8319cda7e6e012e613440f3dfd145d5010e2e", size = 13422306, upload-time = "2026-08-22T15:05:26.248Z" }, + { url = "https://files.pythonhosted.org/packages/de/76/ebbc269a8c4efcc4d44624993bd188145f20d60ebda9680b15aaec42cc50/ty-0.0.74-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:970a8b2c09ff3be04c8a1c6767332d861be4fce85efe7bb205e4ade7c8655274", size = 12970637, upload-time = "2026-08-22T15:05:29.15Z" }, + { url = "https://files.pythonhosted.org/packages/9e/dd/b99f7236acbf856780ca1779a48143d2d9f2c24d7f531a0ce15a022b8a87/ty-0.0.74-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:795f763b3ded85574c2c2846a6fb8acf2aa76e9e83d761143e92b1f0c7ffa2cd", size = 13344891, upload-time = "2026-08-22T15:05:32.033Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d7/9ff7449a4c7e6428f2c6f298e74cf24b70668f29d45c249507a723ff3782/ty-0.0.74-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:dc086db5367d912c31c0cc872deb7387290e779a4b9b54fcb944673a7cd52c7b", size = 12395272, upload-time = "2026-08-22T15:05:34.702Z" }, + { url = "https://files.pythonhosted.org/packages/b1/dd/b23a5b6b35d37df89dc8dc5daa09efd9245a668b50c4c81c25de21567dc1/ty-0.0.74-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0314d7b391cf684e47c2fa093d2ce4c597cfc9b01d9a315fe204aed6359b271b", size = 12573079, upload-time = "2026-08-22T15:05:37.683Z" }, + { url = "https://files.pythonhosted.org/packages/23/c5/ccba16239d6129533c8b3603458d0f4dd2ba69478e47059073968e74261d/ty-0.0.74-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4a45dd2e991e8bdae82ba78c8cd051b253f60bc71a6536598fa3ef580b4fc9b", size = 12832506, upload-time = "2026-08-22T15:05:40.505Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1c/2390912634dff4f341f97b397f2aee341ff062be0a66cda37d59375454f2/ty-0.0.74-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:210e2eac6b018fb934e2b8dac3956a0ba076a3fb1fa6f135058c825e5b759b81", size = 13154752, upload-time = "2026-08-22T15:05:43.355Z" }, + { url = "https://files.pythonhosted.org/packages/c4/33/a8c12188227e6f74f91853a7374e01ed81d6ad21c16c8b70e92dbebfe46a/ty-0.0.74-py3-none-win32.whl", hash = "sha256:db0bb6a8f098ef9bd1be861f73b4f7c0320d40d4c05c7ae0a8677d4e7aa4f6e5", size = 12130002, upload-time = "2026-08-22T15:05:46.058Z" }, + { url = "https://files.pythonhosted.org/packages/21/5c/064f28ccb9c234cfce5a2f7aa69a256663d5ae5bb0290b3a9706cc4d1e4c/ty-0.0.74-py3-none-win_amd64.whl", hash = "sha256:bebff181515255b3c78bd2e7693ae66fab6064ad4feea2065c68bc01022aa678", size = 12771435, upload-time = "2026-08-22T15:05:48.811Z" }, + { url = "https://files.pythonhosted.org/packages/fe/06/d6becdaca0315346c26b6df97cb0eafa81de4f870945d6989e88704374ed/ty-0.0.74-py3-none-win_arm64.whl", hash = "sha256:1a3469eaaf8c85b1c0a15bede25d36daea4b09fce1d913e965b24e24b3f1d6c6", size = 12558299, upload-time = "2026-08-22T15:05:51.543Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.52.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/0f/3f86e61397dd33bf2ccf28188c40db6a740658aeebbbf6e7dbc101a1f487/uvicorn-0.52.4.tar.gz", hash = "sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86", size = 100627, upload-time = "2026-08-19T06:27:41.821Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/79/4a20b54ab0491485ccd8c077db2d39187c7f12b3e15485d38a7be37c81b4/uvicorn-0.52.4-py3-none-any.whl", hash = "sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1", size = 79871, upload-time = "2026-08-19T06:27:40.36Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.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.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, + { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, + { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, + { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, + { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, + { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, + { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, + { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, + { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, + { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, +] + +[[package]] +name = "websockets" +version = "17.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/72/fba934cb3dff7a85d811820efffcd141ddd52b5a2a01637f64551373ff4d/websockets-17.1.tar.gz", hash = "sha256:acfea4c20bf54384883ea33b1240fc1db4f52e190823a4e2b334bc3e8bfca96a", size = 187520, upload-time = "2026-08-26T17:25:33.063Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/31/5f6450a7879f4f063ef08897cc385ea3ce3f1fe17f08b11e3fd959abdf27/websockets-17.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a0162a6372110a5601cb5c9fd826635cedf69f3e110c545dd19774e040b970e", size = 217006, upload-time = "2026-08-26T14:56:10.509Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2a/c1b006fc861695d2aa4e35327b842015ce1d98cf8f99241829b3d6460bfc/websockets-17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:829dba1bc049779de9b332088c1a6a9858e96bd67e50b6b644a95e02b67836bc", size = 214690, upload-time = "2026-08-26T14:56:11.681Z" }, + { url = "https://files.pythonhosted.org/packages/46/69/66e5b7d01445e0eeb1d4ab419c30315f2c90cf7a8a8cd4ecc47f894dba54/websockets-17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd8f47dbf2e8adb15c847215f83436de3fdb120b51fdae0fbbdf69fd97a3ad80", size = 214947, upload-time = "2026-08-26T14:56:12.923Z" }, + { url = "https://files.pythonhosted.org/packages/07/ce/033cafe2d2538562efa876b9149a2c7a0f7787870a4b1bb6e28adc9ceb6b/websockets-17.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9f4c0377a83e163a303514fdfab501dbe379bdc13e5b9312a91d112658b29dce", size = 224329, upload-time = "2026-08-26T14:56:14.212Z" }, + { url = "https://files.pythonhosted.org/packages/34/c7/e1c2e8a67f6cc0aa43abe0046fb3b7a020980649e6a843751dc7ce9eb170/websockets-17.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c3241d684a76eaaef8b2dc789afde4343cd3aad55ea81e4e8ab3605b529bae51", size = 224611, upload-time = "2026-08-26T14:56:15.702Z" }, + { url = "https://files.pythonhosted.org/packages/be/de/07c6d48eb3d2069709410c851e7de10ab83d752c4bd09862899627c2729b/websockets-17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5f5c7a893507d0e83a80b88aefd6522f7e882cd53f9722c6f23f5a020c9557c", size = 225848, upload-time = "2026-08-26T14:56:16.962Z" }, + { url = "https://files.pythonhosted.org/packages/f3/dd/3c68572d20509648cc2fb6f50ccf3deeb4b87270f2c8966e99476e278ea3/websockets-17.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00bf34b64501e3477e81fc281532ff3cbf4da26633c10b63979d5085d46602d3", size = 227290, upload-time = "2026-08-26T14:56:18.204Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4a/8f6651c8a22093539c9215af0c5bbf217b87b382c99d2112039b92d593c2/websockets-17.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ce0305b702b20d1e1d60a9aaace6bc89970e1753565543f310d549eab22c2435", size = 226476, upload-time = "2026-08-26T14:56:19.459Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/f6fc33cea86b1127fd1297b18c107e81580ab55a73a39f9a934441ef321f/websockets-17.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29176d8b429cfa0fa443c473878d37a5c06cfd0cb36b71ba4314accc71e05906", size = 225233, upload-time = "2026-08-26T14:56:20.939Z" }, + { url = "https://files.pythonhosted.org/packages/cb/83/65edaf05f7c9b1dea82f4d252fdc37706a84571646f06119a27b0a16fe19/websockets-17.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3709a1ab30b4b922027d22f68d2b61a0656a91680ac894a537624e6be7dd7f7c", size = 222488, upload-time = "2026-08-26T14:56:22.208Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/d1169c2f7f1f0032b0d4b0c00f0711a070cd7c735de37bfeb876bc0f9606/websockets-17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:43bd0c1ceb924d67f5c1a5254d8361dd9d94246e6331a726064dfa2917880780", size = 225295, upload-time = "2026-08-26T14:56:23.445Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f4/64e2a386c3899b917c2933225c9b47887874229d159797f3bf1a11c20d51/websockets-17.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:1fce0f43e0d41422e0b2cad6561e1970df22f212f4c7e884967df7cf591b031c", size = 223891, upload-time = "2026-08-26T14:56:24.647Z" }, + { url = "https://files.pythonhosted.org/packages/26/b3/dfb5c482f7e310a3432fdbb045ddfe6d34114680e89a233d4ff900a32961/websockets-17.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4031152769179ab8dcdeafc7b0e58052a49117560a28671700b47b2c7b717aad", size = 224661, upload-time = "2026-08-26T14:56:26.027Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cf/94865130a336029f46412adc127c4fbe380f46172b90ce251369e35c4302/websockets-17.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a06f3b5085176763182449559e20391d7ce616a8972a9f7a33deda87ea6d4f3c", size = 225766, upload-time = "2026-08-26T14:56:27.455Z" }, + { url = "https://files.pythonhosted.org/packages/96/34/eb8c658f86dfe562ed49a887a27424bfe9e618c26ea6f865b093d075d3a6/websockets-17.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:77b37cceca17291897c3c73bd30a7c7c7909593554b5da574ec852af83c1742a", size = 223323, upload-time = "2026-08-26T14:56:28.807Z" }, + { url = "https://files.pythonhosted.org/packages/1b/7e/2629609652ece5ca0c7ac235927dd4511b08131e3a5d53439b798fddf002/websockets-17.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d8e83333385cac6030a5167fd18bf96cc6c58b914c308e683f05b0cf94bc8dd0", size = 224276, upload-time = "2026-08-26T14:56:29.991Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/8525737fe840b38e5f40956c198fb586a4fac1e07144d41a5b949b989cf8/websockets-17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:073c5c3f7e127041fa9d34a9e29ceefee8c3cafbd267ed2927318f425144380d", size = 224558, upload-time = "2026-08-26T14:56:31.184Z" }, + { url = "https://files.pythonhosted.org/packages/74/ab/3a958c6cbcf74b118f601c20a80ac8bd5e8dfec0bcf7345116feaeefb121/websockets-17.1-cp313-cp313-win32.whl", hash = "sha256:2afb58c7ba48b329d56769f8dfd89f394efe587b65ef806bae810a484d6d3608", size = 217475, upload-time = "2026-08-26T14:56:32.431Z" }, + { url = "https://files.pythonhosted.org/packages/22/36/fb521f0f2994c25509651f169efe5582dddd8713d57a0757ba87859372ef/websockets-17.1-cp313-cp313-win_amd64.whl", hash = "sha256:0340bbef6bfbe16da888b3983d666a4db4954ac3253c38f13bc7aba0c7db5a2f", size = 217784, upload-time = "2026-08-26T14:56:33.608Z" }, + { url = "https://files.pythonhosted.org/packages/68/92/9b8419584681a12a7534b746dfb2737c466efe2455483e2fbf8b941a04ec/websockets-17.1-cp313-cp313-win_arm64.whl", hash = "sha256:7a72efa3bf4fa3a6669a54420a472ad056da3973d827f10e3a536da463f926c2", size = 217715, upload-time = "2026-08-26T14:56:34.865Z" }, + { url = "https://files.pythonhosted.org/packages/90/0d/500cf5daea09d4669dff3a7d67159094a0bd6c4ef130381404f6edd3eb5f/websockets-17.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0c9982938980e086da59f70d05f9418cd143401a601a0faac10fa48f7bb1cd3e", size = 217048, upload-time = "2026-08-26T14:56:36.03Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/5b12c6168aa269cffbfd24d177cd492b130120403a418c7e89462e27b4ac/websockets-17.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:57b39dc8541cf7ed3f639da82bf7451060483967f9e733da1f8173e4095f0642", size = 214737, upload-time = "2026-08-26T14:56:37.43Z" }, + { url = "https://files.pythonhosted.org/packages/0c/36/e453e5106e4e2416f008ac222837c2f1637a063b08008afcd1088889b631/websockets-17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:96abdecbaae746851b87c3a36cb4a661df93ca3d92f114270f79228bf1d00de6", size = 214955, upload-time = "2026-08-26T14:56:38.71Z" }, + { url = "https://files.pythonhosted.org/packages/dd/30/0204bb86176db02cdfc678ce65ed808a66fab87d250ce61a8790800a60b0/websockets-17.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9fc873e239c5abeb150bc24dbd1a7af23a9254526383ce0a077f5e20adbeb19", size = 224331, upload-time = "2026-08-26T14:56:39.924Z" }, + { url = "https://files.pythonhosted.org/packages/46/c8/d8372256e00c4e3cab1115c45075d1eeedb642a3f2b42bd70c4deae03f06/websockets-17.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f42912fa9eb4cb7c7ec9fde9b3332ba339eb8a8811981043d4029599f3d950b", size = 224685, upload-time = "2026-08-26T14:56:41.169Z" }, + { url = "https://files.pythonhosted.org/packages/12/7d/650355b8f67f908ff99603351d4458d1a0b787d627950a47c38db7e25308/websockets-17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f98bf378d7a5be047a044a1a27c987a8f355e10e3b5754617dbe756248cbc5ce", size = 225927, upload-time = "2026-08-26T14:56:42.359Z" }, + { url = "https://files.pythonhosted.org/packages/34/6c/a9ffa5b903579eed76017870f055d75ecc73988d9d0c9b65a92ba0bf2a27/websockets-17.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d334d11398086bb5559606cb42d51c013ea7c061c7db701521392373d3c087f5", size = 227300, upload-time = "2026-08-26T14:56:43.538Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5d/4551c2269066af7481ee44605a0813770961615b5b5da3e87a8f5cb859ea/websockets-17.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c27336b1a0ac56569493e858497870347854372395f50483725f8cdacc5a45c", size = 226533, upload-time = "2026-08-26T14:56:44.669Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/237a99233e5c445759a613831b3a92e91905afc064dc3bd0ad33c35fd1e2/websockets-17.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67258b00302a5aaf0b267771c7014b13429abd7ea17eebc4c55bd935ff101555", size = 225280, upload-time = "2026-08-26T14:56:45.83Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b5/e9407a91613d1d1cd932414143a1012096b26674a782fc55a0bd23217ee4/websockets-17.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:455ffeea0879d313205df1e745e5883e1feb7f31ecd26be882f5f0babd3db04f", size = 222540, upload-time = "2026-08-26T14:56:47.053Z" }, + { url = "https://files.pythonhosted.org/packages/db/d2/db76628db0577b783205d9779f64d8e373416b04c62d1546be4b75dc8540/websockets-17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f7233eaf441a345a5943a929fd4b5ea3278f11aed35a9ed0f3106b8cb3ca846a", size = 225354, upload-time = "2026-08-26T14:56:48.32Z" }, + { url = "https://files.pythonhosted.org/packages/a9/4c/2174181c067b89a74ae18e2650c2ac29959f4b796afe876ab3f4d30d642c/websockets-17.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c65da239a5ad553619804c1f9d65c1a0b3005381c6158ee14da2c7444cbd0c78", size = 223867, upload-time = "2026-08-26T14:56:49.579Z" }, + { url = "https://files.pythonhosted.org/packages/df/75/274decb9a8253561b5be3261e02a6676fc8ecdf31e95b722e53d5bfb8fd2/websockets-17.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9fa1ffa08c81a4f809cdab6129f8e55bee4650b9d6d3461019dda73aacd146b6", size = 224652, upload-time = "2026-08-26T14:56:50.885Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e6/49824f1fb4db7656d2f7492b1d8be16147b759d909490e32f4776843ee64/websockets-17.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:406b8107943a43ef4649b1e0cb0cdc052bbf08fe1c8905a623c4af9586e5cebb", size = 225822, upload-time = "2026-08-26T14:56:52.356Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6a/5dc43838c0b02a95f42c47a0de33c5ddd7767a9feeb4d0d8777ac1cfefe4/websockets-17.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:4e8ffcb486c8490a34a4cef5e4409d8da5a1cb1681e5bf7d786ce5e84aa8540d", size = 223379, upload-time = "2026-08-26T14:56:53.699Z" }, + { url = "https://files.pythonhosted.org/packages/c2/62/585637cf06d6b321232f79c55dc14d65518d12cf87c94c44f5864068810e/websockets-17.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fb88076df585b69c5761c387c0081aa87d7b9eb1b205a6535ca4777e25650d81", size = 224330, upload-time = "2026-08-26T14:56:55.184Z" }, + { url = "https://files.pythonhosted.org/packages/de/68/c3b234a6a1366b6ab5bbfaa4434a1b946e1dc4e8ddd6824bfd93a8835b7f/websockets-17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5d4724255fb8398acd9e583b97eb2279cec20e0bd0f9a94bf75f6056ef9f13da", size = 224622, upload-time = "2026-08-26T14:56:56.393Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d4/84cf3d1376f5d8207f55f43c1c818babd6b89447f5dcd01f18a6d5526796/websockets-17.1-cp314-cp314-win32.whl", hash = "sha256:be3f0129c5654517b2abf07dcb75bb1d9479759a4ccfb569e8293579e9fc029a", size = 217036, upload-time = "2026-08-26T14:56:57.652Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0f/9e7ac63c5d7cb642952200814f584318e65146df008b7d375d5d9c6b2c97/websockets-17.1-cp314-cp314-win_amd64.whl", hash = "sha256:2a4dc6ef83f4559e0d05f313a375cb38f63c986096a9da99fe94fdd779d313e5", size = 217382, upload-time = "2026-08-26T14:56:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/1ae6b91f7f3ac05f5c9f14a72dc2181c115ff370bcd8a7f10f02c174adfd/websockets-17.1-cp314-cp314-win_arm64.whl", hash = "sha256:46c0331c9eaaf73a559f3a9e388466be0df96eb83d40f06f1ca6ab6613b35c82", size = 217268, upload-time = "2026-08-26T14:57:00.654Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f0/f65644d0e0b2b90918a8c41503841cc4072a58f2bf76c09bc36e751fc0dd/websockets-17.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d411ea5ca18ac1b12c0c94be88b60c18ca641ac43bcdfdf1c9f79d46cdbe1603", size = 217379, upload-time = "2026-08-26T14:57:02.181Z" }, + { url = "https://files.pythonhosted.org/packages/ff/35/4c46d1f620ac1a30f92b6eae78ee40a772a93f568647ca7ccdc5ea283cf8/websockets-17.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:07fa3e7c30e2c577928d359b56bf872a3e0cbcc15553eaa0907c1ee86344b56f", size = 214911, upload-time = "2026-08-26T14:57:03.478Z" }, + { url = "https://files.pythonhosted.org/packages/04/6e/4587e8406d7c1188e97b9cf466c081e93399380d447f885bfce81626cd37/websockets-17.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6de9acef07e3a78e9567fcd26c29011a4da8f050b13004bbf880a0fd82a6eea5", size = 215115, upload-time = "2026-08-26T14:57:04.692Z" }, + { url = "https://files.pythonhosted.org/packages/ec/06/1381c8fff525041025909eb80ace32489194a00ba22a0a8d428030afcc84/websockets-17.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ea0ed9373b880115911d9d39634bccc95b8ce590c9c42e8589f5cacc3ef3cee2", size = 224696, upload-time = "2026-08-26T14:57:05.899Z" }, + { url = "https://files.pythonhosted.org/packages/36/9d/9034e867dc85340be058619751742b895f722326e83100d110063461ca07/websockets-17.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50903d335bfda026c2fa11dd9aed09d8cbee0c451e3a85122a9acb041b7dc69b", size = 224975, upload-time = "2026-08-26T14:57:07.262Z" }, + { url = "https://files.pythonhosted.org/packages/40/eb/ed03aa3cae748ebf6397e5d44028f433f746bad09dc568ff754fda3a3c9b/websockets-17.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a74531ce81af587f906ab42f194032388fcff8fc7938402e5917c9147a39441", size = 226151, upload-time = "2026-08-26T14:57:08.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c9/cc1964a096d16f3b73cb1ee5f14f277f5a3bcac07c6e8f9a1dcded99f4c8/websockets-17.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8fbf28e639544503b7d1c96452a5e5e043e4108d89b1f3fa02910603622d19db", size = 228292, upload-time = "2026-08-26T14:57:09.846Z" }, + { url = "https://files.pythonhosted.org/packages/1a/26/46da6dd0363c2db2e4876fd59a40fd40c1943a82d7018d0a33afbce47d52/websockets-17.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f612dc57f00c07cf4aa2673f7cbceabd654ad2457b7e639f061b794d6e11f9fd", size = 226722, upload-time = "2026-08-26T14:57:11.118Z" }, + { url = "https://files.pythonhosted.org/packages/78/98/ecd8f5e1c5d0e54c08ebc5c66852271112166db68107cb0e17ca1bf25009/websockets-17.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c7ac77401227212dc6e849182feee50d57cf456ec6329ffd6979c94bb136c5c", size = 225451, upload-time = "2026-08-26T14:57:12.601Z" }, + { url = "https://files.pythonhosted.org/packages/65/4d/da8d2760db53e17aae763738b6ba834b1fcf16813d3632f3edb6951e1ec8/websockets-17.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32a2a68d989d6e5b74a9d5095415c51189ebae29fceb7cf2b64a1c0318a81256", size = 223003, upload-time = "2026-08-26T14:57:13.875Z" }, + { url = "https://files.pythonhosted.org/packages/a4/40/ea401c141a79c5b1d0021a0dab9d0df2051c108f1620fbb39a6e7c714c3b/websockets-17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:aec00f018d34c67500ff0438dc314b40277be4a1b983cbacbf53ccf7db63e257", size = 225704, upload-time = "2026-08-26T14:57:15.091Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8e/07ab3f44215d89840d5385fdcaaab1fed8caeffa67c6899e15062957c12c/websockets-17.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0014eaff8ad5b3b43feda2279f9d34bf2eaae040720b9fbbb55944b10f40b14d", size = 224192, upload-time = "2026-08-26T14:57:16.3Z" }, + { url = "https://files.pythonhosted.org/packages/58/93/ccf1af0a23e5748d4e22292a377d78d15cf294d7e707bbb11a8990ae6bd5/websockets-17.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:db9d7ee47f3ba531e278be539af39e2c7c7d28fb94897b6cd1120d63b0ef5922", size = 225082, upload-time = "2026-08-26T14:57:17.531Z" }, + { url = "https://files.pythonhosted.org/packages/e2/db/e32200f99ce282e728d2929f2c429db353cf3282db7d0eba99eb32c9fec1/websockets-17.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:ff3e2ba7a9f0a110b0555452e9b5a03a34e11662544e01beea15f144b48ba7b7", size = 226101, upload-time = "2026-08-26T14:57:18.802Z" }, + { url = "https://files.pythonhosted.org/packages/28/3d/e7a6e9777b29433620167c98f3caaff0d6b08b1239a273ef7f7fd1393349/websockets-17.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6da17fc94bd270f5987b10bee113461ac36a36a98b0481ddcc98056e5a90001a", size = 223794, upload-time = "2026-08-26T14:57:20.313Z" }, + { url = "https://files.pythonhosted.org/packages/48/05/ac569090726dedd6656f3ee28b0c02dfb1ba76e898dceaccc2987a237cef/websockets-17.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:e8dc3fa6d6b7ead3f9de57895f41b116a28787548e066365d9d90f7356bcaad2", size = 224567, upload-time = "2026-08-26T14:57:21.634Z" }, + { url = "https://files.pythonhosted.org/packages/14/50/4ef62941111db6b31193f4fabbb65f845a5177579040cb8fe0d774d25034/websockets-17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b65d5fe48219dc2d5e158de9e6514e75600f379cc7e37108d35f31764c155566", size = 224993, upload-time = "2026-08-26T14:57:22.86Z" }, + { url = "https://files.pythonhosted.org/packages/28/42/2b95ada4ea19bf3a2072b68669ce4f4afb212690b727d31640576287fd68/websockets-17.1-cp314-cp314t-win32.whl", hash = "sha256:2cce251f3e2469b99b6802b55435bcdd07123b41870f54c87b336183af9d7e68", size = 217168, upload-time = "2026-08-26T14:57:24.466Z" }, + { url = "https://files.pythonhosted.org/packages/32/0a/67d5ee08dd8060a37d612fd40a625b5376ad19ae48fe1c8ad428c278b817/websockets-17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f6c38cdcaf98a911d7acc25577f2f9e710f3a2fc2bde1563556784320196b51", size = 217508, upload-time = "2026-08-26T14:57:25.983Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/822005d0c674451d2411027b878cdc128a2b7ea5a30d337d9e279da22eba/websockets-17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:d1e2f5fa2b6d01f0d85b4f223fea7ed1d504be282a02a81bd2be4817ef7a2f03", size = 217425, upload-time = "2026-08-26T14:57:27.324Z" }, + { url = "https://files.pythonhosted.org/packages/de/d5/99a6c6a1eb5d5ae9f45f59a3c97f4e3b21f310eb404a547fb3e7d2fc054c/websockets-17.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:88381602e379165b66244b2ebc29f9b23ea0851fbe63ae157f91ca324f072d6f", size = 216970, upload-time = "2026-08-26T14:57:28.575Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0e/1e7f6e833728193958d3ed3d67b5d57c3c7cfa948abf94d4bc553257c954/websockets-17.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:88bc5138e53903a85c354e59df7ba73ce306f7b09724cef74dba121e60a88ce2", size = 214699, upload-time = "2026-08-26T14:57:29.862Z" }, + { url = "https://files.pythonhosted.org/packages/07/00/95d39549f86e34425a0412bcbe61708dd1fc46af654e2134a6c4389102ad/websockets-17.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3546ef55b3a074494106508bc6505c73825970d2d9505f7bf53882b3e88b0d1e", size = 214927, upload-time = "2026-08-26T14:57:31.148Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ff/b442415fc4f7f9943b0fc8e8eebaa13923ca73361e167c439ba634eecbd9/websockets-17.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9ae55d24241fc055f22aea3ac924559069848bd0ad4ea065fdd72d2194685fe8", size = 224373, upload-time = "2026-08-26T14:57:32.833Z" }, + { url = "https://files.pythonhosted.org/packages/a8/dd/b83537aae4cf61615b9d8b2dbb235c0030ba85457a6d934798273814600f/websockets-17.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d7b349265fad6244013eecd99df8d83c12bf3013943e431f4fadd5bffc37db42", size = 224801, upload-time = "2026-08-26T14:57:34.041Z" }, + { url = "https://files.pythonhosted.org/packages/76/83/5ab0abed58454909e8dbab45086ac68ee4556d7a8ada26735addc909b903/websockets-17.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc5789e5ea182b77a38881383ada5347202a6c66f4857d054e075290e80b604b", size = 225967, upload-time = "2026-08-26T14:57:35.292Z" }, + { url = "https://files.pythonhosted.org/packages/4b/26/e2412f2b998a8c1dfc00c0709ff6ee0c634dd0b0b4f92bdfe9667876b71c/websockets-17.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce13c7d233239e739600a57d4a73c1192ad8259e655a4d55aa1a454242bc809d", size = 227664, upload-time = "2026-08-26T14:57:36.493Z" }, + { url = "https://files.pythonhosted.org/packages/ec/25/0dd4495df3c0e02f6db705312ba85ab9b2dd42257dc23eb0da10066e4844/websockets-17.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1036189bd34b0bc1b10a4679321e2c7968af317efe6e8e4c1c5141c4254fb5bb", size = 226447, upload-time = "2026-08-26T14:57:37.781Z" }, + { url = "https://files.pythonhosted.org/packages/be/67/6df3f63ffc48f08126ed0cd2fd2a41092967c3e364f8ec100deae90b6d77/websockets-17.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e78fd4b7b2c5086a38671c9c882c1e643385eccea360b5b1fda4a105e590087e", size = 225343, upload-time = "2026-08-26T14:57:39.133Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8d/a8479bbb09ff054907d141123d8f52fb6ae5ac39c6dbe39e6a02a8408309/websockets-17.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:46e7a10bf04318c7b0c0273791925ae5e1cbe4a11e34aa934d2ef27862058a80", size = 222748, upload-time = "2026-08-26T14:57:40.478Z" }, + { url = "https://files.pythonhosted.org/packages/40/fb/4c3d2a3269cde3f3087916de9c3d9fc5d7196b46846d8c3a9ae59ad0a884/websockets-17.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:33e45c7ea38428e740a7f233555d71df0b875cef7fc080acebc9654475e35335", size = 225453, upload-time = "2026-08-26T14:57:41.859Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1c/6467b401d19408f34e1c7389c222c2c7e1dfdf08c551190269b5eabc726c/websockets-17.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:6e63c01803be425ff062b7f7fc201a74def1d49fc94a2410dd17375df75936e9", size = 224112, upload-time = "2026-08-26T14:57:43.136Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5f/744e032ac80e11039a7447657ebabb46e9b5c2dbcec83be571335212932f/websockets-17.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:722ec21717eec6477bce582147a28acdfe034e604239466a6a95daedb863e774", size = 224646, upload-time = "2026-08-26T14:57:44.871Z" }, + { url = "https://files.pythonhosted.org/packages/9f/47/bcb9128d9afc4d0934d9192e2a24897ca2f7a63df2654904915349c6c46d/websockets-17.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:e74e41f0ad12ff1e8983e349daef79d37cc8280c743ce9d134d6c74c18dab5d6", size = 225797, upload-time = "2026-08-26T14:57:46.338Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e0/b058047b7cf565e1105b10ef6b6b24a6ebe3575678c7dc75a645334705a7/websockets-17.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:12fe8984a32dbfd084e0603f1a8d740c0180cb85b3174585c54a80d2455a8394", size = 223605, upload-time = "2026-08-26T14:57:48.175Z" }, + { url = "https://files.pythonhosted.org/packages/b9/69/fc1555bff884de363f1bf9eebf2836dbeb29fa7e4f957debb7bbcf43abba/websockets-17.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:01dcb47deebc40b38fd4a493b9b9f4d0a704b7bec6f35e4d34085b329abce71a", size = 224508, upload-time = "2026-08-26T14:57:49.407Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/648d4e68621688b19093b06f7b497d520952e68cdea1c1b54371fe9491de/websockets-17.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f4c45ee2512d3757b5e6c67c5a34e435143f2ecb7df3324f9fd888688c45c0f4", size = 224767, upload-time = "2026-08-26T14:57:50.799Z" }, + { url = "https://files.pythonhosted.org/packages/58/93/f8342b55864f71df13eb8e9ef7dce691b87a87f04f75bb8a1385b3336e7c/websockets-17.1-cp315-cp315-win32.whl", hash = "sha256:0f4f50dfe2cc810fc4e2de979b35e83bf8bb4bccdc6fe472d93762ea7b1d5927", size = 217003, upload-time = "2026-08-26T14:57:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f0/7b5fdb774c245e0b6217009e2a24d2105c1a64923949f33be41aa7959302/websockets-17.1-cp315-cp315-win_amd64.whl", hash = "sha256:4af784f3e436f65b355c117c6497320f2b5cf6a559295cb1c4c7338e335d45cc", size = 217300, upload-time = "2026-08-26T14:57:53.492Z" }, + { url = "https://files.pythonhosted.org/packages/76/33/1fe6ed1b5087516115ca451b2c240314b010647071f8fc3bd78a21e4dddb/websockets-17.1-cp315-cp315-win_arm64.whl", hash = "sha256:d58159af7835fde09c462394293c0d7aaf8fb4557d8f8e5699f5e722ccae013d", size = 217214, upload-time = "2026-08-26T14:57:54.88Z" }, + { url = "https://files.pythonhosted.org/packages/94/ca/ed02e75996a266d76c5fcb5dd9b930db4cf2b388ca5fa3d2a72086f81568/websockets-17.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:1a5cf4e7bbe3ca499e6a289206cb4fcb7444b09919e129bd517f57d5fa192c13", size = 217282, upload-time = "2026-08-26T14:57:56.108Z" }, + { url = "https://files.pythonhosted.org/packages/bd/7d/d536f5bc89ea5b52fd1c1727c59fabafee6bc41f5ce92c3bd2f83047908c/websockets-17.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:416b4bc8789a1865a3ff643ec4ee073a5f52402d0dbeafd27b1798d5dd6b6a51", size = 214863, upload-time = "2026-08-26T14:57:57.355Z" }, + { url = "https://files.pythonhosted.org/packages/37/37/944cf17bad668e9be1247e6314f88a48b9faf7c250e383410db8b38af0b9/websockets-17.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:259f45358c76d3b18489e3e80636cdbe807e05ecf1b10fdf1a779106d23d0c8e", size = 215073, upload-time = "2026-08-26T14:57:58.719Z" }, + { url = "https://files.pythonhosted.org/packages/74/bf/3267966cc1bbc2b8fa62fd329651b0af502df1f5d1c0eed027ff339d6aa8/websockets-17.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9d01e8ede41fea4f5a847dad9d628355f74905f437a5b6856d67aa66d193800", size = 225229, upload-time = "2026-08-26T14:58:00.235Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d8/85ea722f483510abb39fc71aafb4465d17cf9051a275ab036874ff3c300c/websockets-17.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7b35181a14cbfcae163b4de545d22abfd07d06c2c41ca69cfcd99251d6888ab", size = 225500, upload-time = "2026-08-26T14:58:01.994Z" }, + { url = "https://files.pythonhosted.org/packages/50/ce/64c7d00005bd0d15ecb5c5fcb7fb2597b6b92ddd16c4fa6bbc3d2835ad63/websockets-17.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a8e768a048c2220697477ce2e67e4345dc9f693d0ee6af53945b5e30227c6a7", size = 226829, upload-time = "2026-08-26T14:58:03.327Z" }, + { url = "https://files.pythonhosted.org/packages/b4/dc/096c67940fb957e667ca3c542818150434eb0388c6fdc90b3a502f3c3e96/websockets-17.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:880069d21cc33a558dcf180924a546d1ecf8ada5be3e4e70acee87019d706a24", size = 228457, upload-time = "2026-08-26T14:58:04.78Z" }, + { url = "https://files.pythonhosted.org/packages/51/fe/f2331b6b7ccc67589891da354fa46a5cb79e95f83b9fd0e734d77f1f2140/websockets-17.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cec1bb8f22abccc8d20f8ca63df9be41600c26c190f4b97ee86c675fd4a863a6", size = 227265, upload-time = "2026-08-26T14:58:06.102Z" }, + { url = "https://files.pythonhosted.org/packages/47/a5/fb1642302f8ec77ca922203074f155a9831a5128ad75e725059a476d1227/websockets-17.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f3a1d577e081667dda7f8e5b4796e6e32f9713c93e2a3d930669519840a3c623", size = 226143, upload-time = "2026-08-26T14:58:07.464Z" }, + { url = "https://files.pythonhosted.org/packages/d7/41/7133fcfb63f5562750b269d6a845c689dde6a2c6407286da395beea19ddd/websockets-17.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc053f9e95a76213c5eb7ed95779f7daf0d2bf0e4e03073629ebfa43a033f151", size = 223501, upload-time = "2026-08-26T14:58:08.766Z" }, + { url = "https://files.pythonhosted.org/packages/64/b1/82b36bfabc79ff2d383a1fc043cee6a13f794ef4f6bf1b4810ad6988cf6f/websockets-17.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:bb0efe019480a1c93e168ce96479273aaebd672fc8c350d5eed1e507ababb1b8", size = 226330, upload-time = "2026-08-26T14:58:09.987Z" }, + { url = "https://files.pythonhosted.org/packages/41/7d/5b511b9bf6e9ad331e6ff902fcbcc71c3794d10ef3b5efe80ccb8f0a7861/websockets-17.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:615746b12b26a3fd4077bc6fbeb277a1c192a45dd57b531d07ad9ed5c52a9a7a", size = 224980, upload-time = "2026-08-26T14:58:11.303Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/aed08f25301f8eef23be903ff9319fcf35630ca2bdec9d226f7d804dd5b3/websockets-17.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:1a20136d61f9ca3a31493732762661fafc2c20e8861930214e21afc6a8a692a2", size = 225478, upload-time = "2026-08-26T14:58:12.543Z" }, + { url = "https://files.pythonhosted.org/packages/3e/47/0d63d4168536b4682c9d19b7399443b1176f25dbb68878374fa716670230/websockets-17.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:2786cbd273ab69c22612db8a41229ddf2c158060b17b5928884bf388d07887f3", size = 226588, upload-time = "2026-08-26T14:58:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/844bd0b6386fc81ed6a55f4b6dd26f01c6987eda205afa10175ea12b2164/websockets-17.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:b1c323fc3be1dc3f87f6c59458cb7d9e13dcbbf971d6c3f3e2bbaf58d3bfcdfe", size = 224336, upload-time = "2026-08-26T14:58:15.778Z" }, + { url = "https://files.pythonhosted.org/packages/96/18/03709c84bc88ec4dcea68d4be4ccd07d611073dec111203a5bf45af8809d/websockets-17.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:12c8e2b25df59755954a04dfa09c990b96691025aaf7eafd19ed6da24b09c18d", size = 225197, upload-time = "2026-08-26T14:58:17.141Z" }, + { url = "https://files.pythonhosted.org/packages/27/cf/0d1c694b6466c89e875b85b32b51312c472cf6708eee91914866f5087dde/websockets-17.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f58f58b4b29bbea2a3635e2c56eff4a3adab011fe383802a9e542e31b97085fc", size = 225493, upload-time = "2026-08-26T14:58:18.521Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f5/99857c3dd9676749f33e3668665a34ad6099505fb8d75eb084f49f7807a9/websockets-17.1-cp315-cp315t-win32.whl", hash = "sha256:f78a3ffb1994304db2c0c4588e4d1a518079b557054fa3bb985a6f5e50ff49a3", size = 217130, upload-time = "2026-08-26T14:58:20.037Z" }, + { url = "https://files.pythonhosted.org/packages/2c/84/77599922ab441bfe61508f97dab2c71f8e114d31793993ea54011db16199/websockets-17.1-cp315-cp315t-win_amd64.whl", hash = "sha256:ad68c28a27246fed109a4409393d677b7e1388345cbbd2f5aee5c182d8506110", size = 217448, upload-time = "2026-08-26T14:58:21.382Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3c/8b9a225b523f06a9389be81f1b0ab07c49bec6014742e6aa359c1f920f1f/websockets-17.1-cp315-cp315t-win_arm64.whl", hash = "sha256:e552e0037230ac16e5f568de7012041344d1b18c9feed30ec2891b8eba55af81", size = 217372, upload-time = "2026-08-26T14:58:22.807Z" }, + { url = "https://files.pythonhosted.org/packages/41/63/23572870e01836a98346075b9e17a8bc24a6ddd9800a3204ceee58677f3c/websockets-17.1-py3-none-any.whl", hash = "sha256:f221081107b8c48184d99f7019604486376e7ef826037e70aad6b02540732c23", size = 211134, upload-time = "2026-08-26T17:25:31.397Z" }, +] + +[[package]] +name = "wrapt" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/b0/c1f5a970721f06b85c0cd5142e0ff8fe067708abd779b0c4f4be7d61d09f/wrapt-2.3.0.tar.gz", hash = "sha256:681a2d0eefd721998f90642762b8e75c2159ec531b20ad5e437245ea7b06a107", size = 131509, upload-time = "2026-07-28T06:06:14.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/6e/0f88a072483e76b881e3fdcd6b6ffb4a5791002514fe541e72b1b73c859a/wrapt-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d3fb71e65b001adfc42684522eeccd9c21d8ba679945abc993439567b66e59f", size = 81960, upload-time = "2026-07-28T06:04:49.622Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ff/b7e2776e7c294075eb712cc9ef573d1b818f393006d09787262b8fc871c4/wrapt-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51a7a4181c1295774812271fbcd7c909df372bc25579d4ed9eb875caaf0ae86f", size = 82435, upload-time = "2026-07-28T06:04:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/d8/90/343bb5d0f1f9669bc252a6073f085b4abf862511bd5c9c9eaec754341f1d/wrapt-2.3.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9045917809c63fdf7abe3a2ceaed3d670b8ee4500ddd9291192d30aeb34467c5", size = 170350, upload-time = "2026-07-28T06:04:52.187Z" }, + { url = "https://files.pythonhosted.org/packages/59/f8/13b79a392930bd0dd6b86cbfbfe1c40944110456e1dc6d809e5c46ece904/wrapt-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54ca1d5573f69b5fe1d74f1f65799c68015e82f685efec9fd8cfa40a094c44d0", size = 170022, upload-time = "2026-07-28T06:04:53.599Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fc/4f1b6918f5290db959d6e0c07f77385d87cede29c39c9cf8f145e9c82954/wrapt-2.3.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:242b60c21e30866e6a2fa606c612b47c553fa60c0eaeeeb7797fb842ac0ce609", size = 161043, upload-time = "2026-07-28T06:04:54.936Z" }, + { url = "https://files.pythonhosted.org/packages/01/e1/45d3cf74414780bdff6d0380467e003f6eb0f028b6c9403db868dbc7209c/wrapt-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3f3d7ec0a51fbfe00d3aef047641ff2c58b25565b4717fc1f90e050be01cba8", size = 168576, upload-time = "2026-07-28T06:04:56.261Z" }, + { url = "https://files.pythonhosted.org/packages/f3/73/2fa58dd97f191c997755e2c6d569a68f0c433db4e4b36099bdd7227b6cac/wrapt-2.3.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:261f53870cd4fb2bf38f9f972c56c728fd224cb7c65721307de59d9e7e6741ae", size = 159140, upload-time = "2026-07-28T06:04:57.754Z" }, + { url = "https://files.pythonhosted.org/packages/29/a8/08a56e2000a8816d449dcbad8c8b081697acbbd490821ceca0f9d8e8d20c/wrapt-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8159ec0b0cb7608175eb150de94c19e34f4d47ac655f5ca9baf45df6b688ffd3", size = 169263, upload-time = "2026-07-28T06:04:59.161Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d4/354e1725e35a73b2af4fa70a3e024c7a5d1bf1802dfb862dcb668aae0253/wrapt-2.3.0-cp313-cp313-win32.whl", hash = "sha256:10461884b3014fbfc8eb7d09a93c5f246363e6711d9d881f95eb8c27fdef049f", size = 78241, upload-time = "2026-07-28T06:05:00.507Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7e/34c87fa2174848dfee820322aaa318bab08913998ccecc8d2f57b4ad4639/wrapt-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:ac870cc97b73bb00ac353329e9559a4bebc47c4c86792ed9b23b58c15b6ad838", size = 81113, upload-time = "2026-07-28T06:05:01.839Z" }, + { url = "https://files.pythonhosted.org/packages/11/86/fcc9a530579e008c9478bb565a6cdfbfd33536660f069c8b91a6607c5050/wrapt-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:a65e8db2b4e90c2e7ade931086351c98ef420bf7a94ee08c95ac8a3cbbc43579", size = 80182, upload-time = "2026-07-28T06:05:03.152Z" }, + { url = "https://files.pythonhosted.org/packages/96/50/3864848b95b28ef73e17551fc8dccbff2628a834f52cf26a57f9c419fb83/wrapt-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:fd1f2f557dd3491fe75905e578f4db967393d40d1a8f468edc4d40ac7f2d5944", size = 83921, upload-time = "2026-07-28T06:05:04.476Z" }, + { url = "https://files.pythonhosted.org/packages/3b/4c/3d1921a60c3e8c71c540ff136e6a47a1fbccf7f671e818394889f7871d9c/wrapt-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9f5d2aec29dfc76c37e23897dee92766a3fd4f3bff3ae7fc9c6b4bf37d8c1360", size = 84412, upload-time = "2026-07-28T06:05:05.921Z" }, + { url = "https://files.pythonhosted.org/packages/fa/1a/4a796ff7adb26ada6d4b758c94d47a38320b085e7099afc088efbbcdb006/wrapt-2.3.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:646d20d413ffcd1b0a2f700076e2d0252d872dcb7754860a73e45a59ea883614", size = 207168, upload-time = "2026-07-28T06:05:07.256Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3e/d7777776806c579b761bac2f91721dda9f04c7a1b380213c5935cc750ae6/wrapt-2.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:379f670f45b7bb8993edd9f6fc36c6cc65edb81cffa0b504be34acb0303fff0a", size = 214351, upload-time = "2026-07-28T06:05:08.945Z" }, + { url = "https://files.pythonhosted.org/packages/63/27/2d64d394df7bf181955b3bb562bf33c4492fb4be113f53071106d43ad8b5/wrapt-2.3.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6208f302f110295d64b22a7ac96500c791bf492dce4366e622e4912b077c9687", size = 199020, upload-time = "2026-07-28T06:05:10.418Z" }, + { url = "https://files.pythonhosted.org/packages/3e/3d/fb31d3db7d9834d265fb1a27a2adf0ddf51557c67458c97b22439ad6ae3d/wrapt-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ed635a9ca4f3a5a2b900c10c69e823373bc00ebc114b459383596d3487da3570", size = 209969, upload-time = "2026-07-28T06:05:11.983Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d1/8724b5da582e62070dc9bf4d8bf1972f317297eefd7ba1f2b5c6393ccf6c/wrapt-2.3.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e3b9eaa742ae7a0aaaaad4ca4b69469d757af2d6e6663ef1dadc47adec0aeb41", size = 196324, upload-time = "2026-07-28T06:05:13.557Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5c/3d9ef411149543016ee6bcf3af707f787cebd946527452b94bf122e9b7b4/wrapt-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0f7284f88f4833705132d06d3b425a43095c2cbd07c58166aac3ab646ba12a4", size = 202610, upload-time = "2026-07-28T06:05:15.048Z" }, + { url = "https://files.pythonhosted.org/packages/13/9b/4fc042ceb757866dd4a5fc057b3b736f2b360d3703ce9f830d83dc9226e0/wrapt-2.3.0-cp313-cp313t-win32.whl", hash = "sha256:7ebb274aba688b043429eb1500ff8a76ce0cb8ac0812ca3e301f06247b8722b3", size = 79178, upload-time = "2026-07-28T06:05:16.469Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ff/b94878f8eed809ca042685276bcea9f24e8c2ca7c9653bb80bbb920a68a5/wrapt-2.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c4bded758ad6f03b965830944a2f0bc5b2eb3767fe5a7310134315d1a6610e98", size = 82634, upload-time = "2026-07-28T06:05:18.026Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/663e1de5332a71685a729754312d327d4cada767c36e1c5a2db4c8de49e6/wrapt-2.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:d2cc64539da63e39ffb9c7ede849b6e8ddaaf7b3876b5cfb04efd85a5f3f4eb6", size = 81387, upload-time = "2026-07-28T06:05:19.417Z" }, + { url = "https://files.pythonhosted.org/packages/58/10/b073beaea89bc0d3670a75ff51139430a54b6af7ba7796507730634536dd/wrapt-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea52a0d0f08c584943d5764be0e84efa912c8da23c23e1e285ff2f5641c18fcc", size = 81978, upload-time = "2026-07-28T06:05:21.133Z" }, + { url = "https://files.pythonhosted.org/packages/b3/31/0916d9cebf848ed3f1a0c1888faee421747df77331e4db2bc527a9a85988/wrapt-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd85b0aa88efdb189d6ae2f35f4526943a8f091c38599c9c31478241c819e6a1", size = 82518, upload-time = "2026-07-28T06:05:22.562Z" }, + { url = "https://files.pythonhosted.org/packages/f5/73/31c1bf0f3384062751c2094dadb314916d70aa9b6bfd26d994b4a7b393fa/wrapt-2.3.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:141ed6211286a9660d8d6702de598b43f0934b4f0eda16393f100a80f501d945", size = 170187, upload-time = "2026-07-28T06:05:23.904Z" }, + { url = "https://files.pythonhosted.org/packages/ed/25/fce087d54b79b8905f3c3c9dd5f454bbd8d8acb80b960c4a6aee5b4659b3/wrapt-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e49885a62ec4ee854d1b9e6371fda6afd219917225752abf729a3f36d4df9a5", size = 169288, upload-time = "2026-07-28T06:05:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/c7/30/0d09e6dddc6b7a7230ac77f50254b5980ab4fcd22976f72f8cc8a0404458/wrapt-2.3.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d6159c9b2fefec02314e1332dbbbfaf960e369dfd26bcf7f8b258b5732065b3", size = 160932, upload-time = "2026-07-28T06:05:27.022Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ca/0913af0d2ec0c43865d32d615f518fea66c13c5c930e489e9b0de248e9a8/wrapt-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24da48596326ef8e448cfa837b454f638713d3531262375f00e5a9681682fc07", size = 169017, upload-time = "2026-07-28T06:05:28.501Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f2/3d1e47ea81b822210f5df1bf942fd90780a75c055243d569b664529dea88/wrapt-2.3.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cd3a2edf0427013736b8127955cec62608c56e53ea47e82812ea32059cda407f", size = 159065, upload-time = "2026-07-28T06:05:30.01Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/ef2066ced8e5fca204e2b361e9708e36555b40949c583d997ea3b590817d/wrapt-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fa0df3bff4e7ce45759f33fd39335fe2f60477bb9ecf7b8aa41e7d07ee36a23", size = 168821, upload-time = "2026-07-28T06:05:31.649Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/016104650d4e572fa91506eb396b3dd8efbccc9284fdc1c9479c3d21db28/wrapt-2.3.0-cp314-cp314-win32.whl", hash = "sha256:2935d5454b3f179a29b12cf390ee47246740ba2c3a7545b1b46ba31a5f2a4a0b", size = 78700, upload-time = "2026-07-28T06:05:33.391Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/6fdc20a9f2ca304748b3f0819cbf377d55260562777bf0b615431bc3c181/wrapt-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:cc2cea812e5cb179a796b766747e7d3b21088760d8deb95676d482b8c8e6fa7d", size = 81422, upload-time = "2026-07-28T06:05:34.774Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a4/9cbd53bf05746bea2c392af39cb052427a8ec95cbd494d930733d8f44681/wrapt-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:22cc5c0a717bd4da87018ae0bffd4c19c6fb679d3ff357216ba566ab26c76cab", size = 80639, upload-time = "2026-07-28T06:05:36.228Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/6c5e4a0f66ea0d2b2dd267e8dd05a0014eea56840b3c8595d40b0a5d1f91/wrapt-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a6b5984cd65dd639546f0eb4b8eacf1c31cb2fe9fb5c27bffe240987cdb2cf84", size = 84030, upload-time = "2026-07-28T06:05:37.714Z" }, + { url = "https://files.pythonhosted.org/packages/6a/eb/a1aedf03283bc9cbf8a1783995ddc54e3c5a86878f19002d2c428494f4c5/wrapt-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c88abcf53daef80e01a75c7530e727fa6e2c1888fe83e3dcdba4c96216a1f5c7", size = 84419, upload-time = "2026-07-28T06:05:39.131Z" }, + { url = "https://files.pythonhosted.org/packages/63/61/50d511c0dc5105563849e86daa3e16ac7feef699f79fb05af45ea70107d5/wrapt-2.3.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:85de890ff968196e92dd1ae73a9fb8970495e7650a457b1c9ef0ac3dd550bce2", size = 207171, upload-time = "2026-07-28T06:05:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/3f/59/9b538cf7795217e810699d16bc88b96a830d9b5c403eb2ec2db6b5f2ae81/wrapt-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50f416b74d092bb9f41b424e90dd457f365f7ba4b11de62a23679769a21bd85c", size = 214329, upload-time = "2026-07-28T06:05:42.287Z" }, + { url = "https://files.pythonhosted.org/packages/b3/28/9935d62b1499e5c8b3d191e99ba4eb31ca237a0b699142011a837e9dc7ea/wrapt-2.3.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39febbee6d77301d31da6996b152ce52452da7c7ef72aba10c2fa976dff9c295", size = 199079, upload-time = "2026-07-28T06:05:43.958Z" }, + { url = "https://files.pythonhosted.org/packages/2b/01/4446b80fa2ffa47a3449b250d004ba1c1937f07f64a179608fec735df866/wrapt-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:93513bec052c6cd987f9f580c3df068c8bc4ebae6543736be3ca7ec5959cafcd", size = 209992, upload-time = "2026-07-28T06:05:45.677Z" }, + { url = "https://files.pythonhosted.org/packages/d4/07/56f26c9f9979586a021e8148747004aba4498f49458c90b0502969b904e1/wrapt-2.3.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:729126e667da34d251b8ebf8a45ef0c5ddadc21542b3d6e1abf4259ece6508df", size = 196334, upload-time = "2026-07-28T06:05:47.608Z" }, + { url = "https://files.pythonhosted.org/packages/8b/41/6d7bcc895b0f28b2250e10908f060687b9165429dcd7f22ddb3d4c031b74/wrapt-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:626b69db2021aa01671ec7bbc9740e558522bd44c18cf2ce69bf3d666a014109", size = 202644, upload-time = "2026-07-28T06:05:49.183Z" }, + { url = "https://files.pythonhosted.org/packages/cd/25/7860927edba06b758b8852a6f02e832be715563c67a6795d94350bc81099/wrapt-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:629d73378082c00a8173031f9fb30a3ac6abbc894a5bfdfae71fabc60642d501", size = 79685, upload-time = "2026-07-28T06:05:50.976Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0f/270bafe92fde3b069a39bc01e39ee79340895b335640df861d43d2a51885/wrapt-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:42869085687f0aefd57c0f636c3f9354f8ffb321a8ba9cb52d19beb796e561c5", size = 83104, upload-time = "2026-07-28T06:05:52.405Z" }, + { url = "https://files.pythonhosted.org/packages/55/b3/af176d79a8515a8a720eccdad9a96f6e31a30abf2865430c8c42adf2fd13/wrapt-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b1e5aa486e269b00ed35e64771c7d0ab8096cfd2643405ca8cd60ebedc099a51", size = 81774, upload-time = "2026-07-28T06:05:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/00/39/3daf9f47be208606586de4568ba6713db53ebc8fd7a575aea1fe57983b69/wrapt-2.3.0-py3-none-any.whl", hash = "sha256:d8c7ed08477429752b8c44991f40ad7838b18332a160698740a6bfbc10d998a2", size = 61866, upload-time = "2026-07-28T06:06:12.9Z" }, +] diff --git a/rewrite_architect.md b/rewrite_architect.md new file mode 100644 index 0000000..93c1ce7 --- /dev/null +++ b/rewrite_architect.md @@ -0,0 +1,2682 @@ +# Strict AI-Native Repository Profile + +> **SSOT:** This document is the canonical architecture and repository profile. `runtime-research.md` is supporting research, not an authority. When the two conflict, this document wins; adopted decisions must be recorded here. + +## 0. Stack + +This repository describes a Python backend with a separate SvelteKit frontend. + +### Backend + +- Language and runtime: Python 3.13 +- Package and project manager: `uv` +- HTTP API: FastAPI +- ASGI server: Uvicorn +- API schemas: Pydantic 2 and OpenAPI + - Configuration: `pydantic-settings` (`BaseSettings`) +- Password hashing: `pwdlib[argon2]` using Argon2id +- Database: PostgreSQL +- Data access: SQLAlchemy 2.x async engine and ORM +- Database migrations: Alembic +- PostgreSQL driver: `psycopg` 3 async implementation +- Database URL scheme: `postgresql+psycopg://` + +### Frontend + +- Framework: SvelteKit +- Production adapter: `@sveltejs/adapter-node` +- UI runtime: Svelte 5 +- Language: TypeScript +- Styling: Tailwind CSS v4 +- UI components: shadcn-svelte +- Accessible primitives: Bits UI +- Icons: Lucide Svelte + +### Verification + +- Backend tests: pytest +- Frontend tests: Vitest +- Browser tests: Playwright +- Python quality: Ruff, Pyright, Import Linter +- Frontend quality: Prettier, ESLint, `svelte-check` + +Redis, message brokers, distributed tracing, Sentry, Loki, Datadog, and Kubernetes are not part of the default stack. Add them only when an explicit operational requirement justifies them. + +## 1. Objective + +Repository được tối ưu cho AI coding agents theo nguyên tắc: + +```text +minimize decision entropy +maximize structural signal +maximize mechanical enforcement +``` + +Agent được quyền quyết định **cách giải bài toán**. + +Agent không được tự quyết lại: + +```text +naming grammar +test taxonomy +verification workflow +``` + +Repository phải làm cho: + +```text +valid decisions → obvious +invalid decisions → mechanically difficult +``` + +--- + +# 2. Repository Structure + +```text +project/ +├── AGENTS.md +├── CLAUDE.md +├── README.md +├── ARCHITECTURE.md +├── CONTEXT.md +│ +├── pyproject.toml +├── uv.lock +├── .python-version +├── .importlinter +├── .pre-commit-config.yaml +├── .env.example +├── alembic.ini +├── Makefile +├── Dockerfile.backend +├── .dockerignore +├── compose.yaml +│ +├── docs/ +│ ├── specs/ +│ ├── adr/ +│ └── agents/ +│ ├── domain.md +│ ├── issue-tracker.md +│ └── triage-labels.md +│ +├── migrations/ +│ ├── env.py +│ ├── script.py.mako +│ └── versions/ +│ +├── apps/ +│ └── web/ +│ ├── src/ +│ │ ├── routes/ +│ │ ├── lib/ +│ │ │ ├── components/ +│ │ │ │ ├── ui/ +│ │ │ │ └── shared/ +│ │ │ ├── features/ +│ │ │ ├── generated/ # API types/client when Web consumes the API +│ │ │ ├── server/ +│ │ │ ├── styles/ +│ │ │ └── utils/ +│ │ ├── hooks.server.ts +│ │ └── app.html +│ ├── static/ +│ ├── tests/ +│ │ └── unit/ +│ ├── Dockerfile +│ ├── package.json +│ ├── pnpm-lock.yaml +│ ├── svelte.config.js +│ └── vite.config.ts +│ +├── src/ +│ └── / +│ ├── domain/ +│ │ ├── entities/ +│ │ ├── value_objects/ +│ │ ├── policies/ +│ │ ├── events/ +│ │ └── errors/ +│ │ +│ ├── application/ +│ │ ├── use_cases/ +│ │ └── ports/ +│ │ ├── inbound/ +│ │ └── outbound/ +│ │ +│ ├── infrastructure/ +│ │ ├── adapters/ +│ │ │ ├── persistence/ +│ │ │ ├── clients/ +│ │ │ └── messaging/ +│ │ └── config/ +│ │ +│ ├── presentation/ +│ │ ├── http/ +│ │ ├── cli/ +│ │ └── consumers/ +│ │ +│ └── bootstrap/ +│ ├── app.py +│ ├── entrypoint.py +│ └── wiring.py +│ +├── tests/ +│ ├── unit/ +│ │ ├── domain/ +│ │ └── application/ +│ ├── integration/ +│ │ └── infrastructure/ +│ ├── functional/ +│ │ └── presentation/ +│ └── architecture/ +│ +├── e2e/ +│ ├── playwright.config.ts +│ └── tests/ +│ +├── .artifacts/ +│ └── verification/ +│ ├── screenshots/ +│ ├── traces/ +│ └── videos/ +│ +└── .github/ + └── workflows/ + └── ci.yml +``` + +Container ownership: + +```text +Dockerfile.backend +→ builds the Python/FastAPI backend image + +apps/web/Dockerfile +→ builds the SvelteKit/Node web image + +compose.yaml +→ orchestrates backend, web, and PostgreSQL locally +→ does not define image build logic itself +``` + +Each deployable runtime has its own image and Dockerfile. Multi-stage builds remain inside each Dockerfile. Do not combine backend and web into one production image. + +Makefile ownership: + +```text +Makefile +→ canonical repository command interface for developers, agents, and CI +→ owns recipes such as db-up, db-down, backend-dev, web-dev, check, test, migrate, admin-create, and build +→ calls existing tools such as uv, pnpm, Docker Compose, Alembic, and pytest +→ contains no business logic or duplicated implementation logic +``` + +Makefile conventions: + +```text +→ command recipes are `.PHONY` +→ `make help` lists supported commands +→ recipes remain simple and POSIX-shell compatible +→ CI calls the same recipes documented for local use +→ do not add a second task runner or duplicate command catalog +``` + +Ownership: + +```text +.env.example +→ the single canonical environment variable list with dummy values. +→ Checked in. It is the only env template file. +→ Secrets have no real defaults. App fails fast at startup if missing. +→ `PUBLIC_*` is exposed to the browser. Never put secrets in `PUBLIC_*`. + +Env files: + +```text +.env.example +→ checked in, documents every variable dev and test need +→ dev and test share the same shape, only values differ + +.env +→ local-only dev values, never committed +→ copy from the example: `cp .env.example .env` + +.env.test +→ local-only test values, never committed +→ points at an isolated test database and credentials +→ test runner loads `.env.test` instead of `.env` + +staging / prod +→ no env file shipped with the artifact +→ values injected by the deployment environment or secrets manager +``` + +Variable categories: + +```text +runtime +→ APP_ENV, LOG_LEVEL + +database +→ DATABASE_URL + +auth / external services +→ API_KEY_ENCRYPTION_KEY for re-displaying existing API keys; keep it outside the database +→ credentials required by integrations actually used +→ server-only, never PUBLIC_* + +backend integrations +→ AI provider keys, base URLs +→ messaging brokers, webhook secrets +→ only the integrations the service actually uses + +web public +→ PUBLIC_APP_NAME and other explicitly browser-safe runtime values +→ browser calls SvelteKit by default; `PUBLIC_API_URL` is added only if browser-to-FastAPI calls are explicitly adopted + +web server-only +→ `BACKEND_INTERNAL_URL` and backend credentials used by `$lib/server/` +→ never `PUBLIC_*` +``` + +Environment separation: + +```text +dev +→ local `.env`, dummy values allowed except real-looking secrets + +test +→ isolated test database and credentials +→ never points at dev or prod data + +staging / prod +→ injected by the deployment environment or secrets manager +→ no `.env` file shipped with the artifact +→ production secrets never appear in repo, logs, or screenshots +``` + +Why dev and test are separated: + +```text +dev +→ where a human works, dirty data is acceptable + +test +→ where machines verify, data must be clean and predictable + +Sharing one database lets manual work break tests +and lets test fixtures wipe manual work. +``` + +Backend-Web HTTP contract +→ FastAPI/Pydantic schema owns the definition. +→ Do not commit generated OpenAPI or create a contract pack by default. +→ Once Web consumes the API, one Makefile recipe exports transient OpenAPI and generates TypeScript types/client into apps/web/src/lib/generated/ for local setup, CI, and builds. +→ Generated outputs are ignored by Git, never manually edited, and produced before Web checks and image builds. + +migrations/ +→ canonical database migration history. +→ `env.py` uses the SQLAlchemy async engine configuration. +→ Alembic revisions live in `versions/` and are reviewed before merge. +→ `alembic check` runs in CI to detect model/migration drift. +→ `alembic upgrade head` runs once as a release step before application startup. +→ FastAPI lifespan must not run database migrations. + +apps/web/tests/ +→ isolated web unit/component tests. +→ No backend, no network, no browser. +→ Uses Vitest. + +e2e/ +→ single full-system browser regression suite. +→ backend + web + database running together. +→ Uses Playwright Test. +→ The only browser E2E entry point. + +apps/web/src/lib/utils/ +→ generic presentation utilities only. +→ Examples: class merging, date formatting, numeric clamping. +→ Must be pure, reused in at least two places, + and must not import domain or feature concepts. +``` + +--- + +# 3. Documentation Ownership + +Every type of knowledge has exactly one canonical owner. + +```text +AGENTS.md / CLAUDE.md +→ HOW TO WORK + +README.md +→ WHAT THIS PROJECT IS + +ARCHITECTURE.md +→ WHERE CODE GOES + +CONTEXT.md +→ WHAT WORDS MEAN + +docs/specs/ +→ WHAT THE SYSTEM SHOULD DO + +docs/adr/ +→ WHY WE CHOSE THIS + +docs/agents/ +→ HOW EXTERNAL AGENT SKILLS INTEGRATE +``` + +## AGENTS.md + +Contains only repository-wide agent workflow and hard operational rules. + +It should route agents toward the appropriate source of truth instead of duplicating those sources. + +Example responsibility: + +```text +Before coding: +1. Read relevant CONTEXT.md terms. +2. Read ARCHITECTURE.md. +3. Read relevant approved spec. +4. Read relevant ADR when architectural decisions are involved. +5. Inspect an existing exemplar in the target layer. + +Before handoff: +1. Ruff +2. Pyright +3. Import Linter +4. Relevant tests +5. Browser verification when user-facing behavior changes +``` + +## CLAUDE.md + +Claude-specific entry point. + +Prefer: + +```text +Follow AGENTS.md for repository-wide instructions. +``` + +Do not duplicate architecture or domain rules. + +## README.md + +Contains: + +```text +project purpose +scope +basic architecture overview +installation +development setup +run commands +deployment entry points +``` + +It does not define architectural rules. + +## CONTEXT.md + +Canonical ubiquitous language. + +Defines: + +```text +term +precise meaning +relationships +allowed usage +forbidden synonyms +``` + +Example: + +```text +ScoringRun + +One execution of one ScoringPolicy against one canonical input snapshot. + +Avoid: +- ScoringJob +- ScoreExecution +- RunJob +``` + +Rule: + +```text +One concept → one canonical term. +``` + +## ARCHITECTURE.md + +Defines: + +```text +layers +placement rules +dependency rules +naming grammar +test taxonomy +transaction ownership +configuration boundaries +error boundaries +composition rules +``` + +## docs/specs/ + +Approved behavioral requirements. + +Specs describe: + +```text +inputs +behavior +outputs +constraints +acceptance criteria +``` + +Specs must not redefine architecture. + +## docs/adr/ + +Only architectural decisions with meaningful trade-offs. + +Use ADR when a choice is: + +```text +cross-cutting +non-obvious +expensive to reverse +architecturally significant +``` + +Do not create ADRs for routine implementation details. + +## docs/agents/ + +Configuration/integration for external agent skills. + +It is not a project knowledge source of truth. + +--- + +# 4. Architecture + +## Domain + +```text +domain/ +``` + +Owns business truth. + +Allowed: + +```text +entities +value objects +domain policies +domain events +domain errors +invariants +pure domain calculations +``` + +Forbidden dependencies: + +```text +FastAPI +SQLAlchemy +HTTPX +Postgres +Redis +environment variables +presentation +infrastructure +application orchestration +``` + +--- + +## Application + +```text +application/ +``` + +Owns system use cases. + +### use_cases/ + +```text +application/use_cases/ +``` + +Represents: + +```text +what the application does +``` + +Examples: + +```text +CreateScoringRun +ExecuteScoringRun +GetScoringRun +ReviewScoringRun +``` + +### ports/inbound/ + +Stable contracts through which external actors may invoke application capabilities. + +Conceptually: + +```text +outside +→ inbound port +→ use case +``` + +Python note: + +An explicit inbound Protocol is optional when the use case itself already forms a clear callable application boundary. + +Do not create an inbound abstraction purely to mirror an existing function without adding an invariant. + +### ports/outbound/ + +Capabilities that the application requires from the outside world. + +Examples: + +```text +ScoringRunRepository +AIProvider +EventPublisher +Clock +IdGenerator +``` + +Conceptually: + +```text +use case +→ outbound port +→ adapter +``` + +Application does not know concrete technologies. + +--- + +# 5. Infrastructure + +```text +infrastructure/ +``` + +Owns technical implementations. + +## adapters/persistence/ + +Examples: + +```text +PostgresScoringRunRepository +SQLAlchemy models +database sessions +transaction implementation +``` + +## adapters/clients/ + +Examples: + +```text +AIHubClient +OpenAIClient +CRMClient +``` + +## adapters/messaging/ + +Examples: + +```text +OutboxPublisher +KafkaPublisher +WebhookDispatcher +``` + +## config/ + +Examples: + +```text +environment settings +runtime configuration loading +secret references +``` + +Configuration contract: + +```text +pydantic-settings BaseSettings +→ the only backend configuration loader and validator + +canonical variables +→ APP_ENV +→ LOG_LEVEL +→ DATABASE_URL +→ API_KEY_ENCRYPTION_KEY + +Session tokens are stored only as digests. API keys retain a verifier for authentication and an encrypted copy for authorized re-display; the encryption key is a server-only secret kept outside the database. Do not add a session signing secret unless the selected implementation requires one. + +DATABASE_URL +→ uses postgresql+psycopg:// + +infrastructure/config/ +→ defines Settings + +bootstrap/ +→ loads validated Settings and constructs concrete dependencies + +domain/ and application/ +→ do not read environment variables directly +``` + +Required secrets and production connection settings have no insecure defaults. Missing or invalid configuration fails application startup. PostgreSQL container variables such as `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` configure the database image; `DATABASE_URL` is the canonical application connection setting. + +--- + +# 6. Presentation + +```text +presentation/ +``` + +Owns inbound transport. + +## http/ + +```text +FastAPI routes +request schemas +response schemas +HTTP dependency wiring +HTTP error mapping +middleware +``` + +HTTP layer translates: + +```text +HTTP +→ application input +→ use case +→ application result +→ HTTP response +``` + +Operational endpoints: + +```text +/healthz +→ liveness probe +→ checks that the process responds +→ must not query the database +→ returns 200 when the app is alive + +/readyz +→ readiness probe +→ checks critical dependencies such as PostgreSQL +→ returns 200 when the app can accept traffic +→ returns 503 when the app is not ready +``` + +Health responses must not expose secrets or infrastructure details. Health endpoints contain no business logic. + +No business rules. + +## cli/ + +Command-line entry points. + +## consumers/ + +Message/event consumers. + +--- + +# 7. Bootstrap + +```text +bootstrap/ +``` + +Composition root. + +Owns: + +```text +dependency construction +concrete adapter selection +application wiring +application startup +``` + +Rule: + +```text +Construction belongs at the edge. +Usage depends on abstractions. +``` + +Bootstrap may know every layer. + +Inner layers may not know bootstrap. + +### Dependency Injection (DI) + +Use manual dependency injection by default. Do not add a DI container without a demonstrated requirement. + +```text +bootstrap/wiring.py +→ constructs concrete adapters and application services +→ is the only composition root + +use case +→ receives outbound ports through explicit parameters + +presentation/http/ +→ may use FastAPI Depends for transport-bound wiring only +→ must pass already-wired application services into the use case + +domain/ and application/ +→ must not import FastAPI or Depends +``` + +Application and domain code must not construct infrastructure dependencies. Tests replace concrete adapters at the composition root with fakes or test implementations. App-lifetime resources such as engines and clients are created once; transaction semantics remain owned by the application use case. + +### Application Factory and Lifespan + +```text +bootstrap/app.py +→ defines create_app(settings, dependencies) +→ creates and configures the FastAPI instance +→ registers routers, middleware, handlers, and lifespan + +bootstrap/entrypoint.py +→ loads runtime Settings +→ calls create_app() +→ exposes app for the ASGI server + +bootstrap/wiring.py +→ builds the concrete dependency graph +``` + +The application factory is the project application-construction pattern, not a DI container. It is used to create isolated development, test, and production application instances. FastAPI `Depends` remains limited to presentation/http transport wiring; application and domain code do not depend on it. + +The lifespan manages application-lifetime resources such as database engines and external clients. It must not run database migrations or contain business rules. The production ASGI entrypoint is: + +```bash +uvicorn src..bootstrap.entrypoint:app --host 0.0.0.0 --port 8000 +``` + +--- + +# 8. Dependency Direction + +```text + Domain + ▲ + │ + Application + ▲ ▲ + │ │ + Infrastructure Presentation + \ / + Bootstrap +``` + +Hard rules: + +```text +Domain +X Application +X Infrastructure +X Presentation + +Application +✓ Domain +X Infrastructure +X Presentation + +Infrastructure +✓ Application +✓ Domain +X Presentation + +Presentation +✓ Application +✓ Domain when mapping requires it +X Infrastructure concrete implementation + +Bootstrap +✓ all layers +``` + +These rules should be machine-enforced. + +--- + +# 9. Naming Model + +Naming has three owners. + +```text +CONTEXT.md +→ vocabulary + +ARCHITECTURE.md +→ grammar + +PEP 8 + Ruff +→ spelling/style +``` + +## Vocabulary + +Domain decides canonical nouns. + +Example: + +```text +ScoringRun +ScoringPolicy +EngineProfile +Rule +ScoreBand +``` + +## Grammar + +### Domain type + +```text +ScoringRun +ScoringPolicy +Rule +Score +``` + +Avoid: + +```text +ScoringRunEntity +ScoringPolicyModel +RuleData +``` + +unless technical distinction genuinely requires it. + +### Use case + +Verb + noun: + +```text +CreateScoringRun +ExecuteScoringRun +ReviewScoringRun +``` + +### Outbound port + +Capability/role: + +```text +ScoringRunRepository +AIProvider +EventPublisher +``` + +### Adapter + +Technology + role when useful: + +```text +PostgresScoringRunRepository +AIHubProvider +KafkaEventPublisher +``` + +### Domain event + +Past tense: + +```text +ScoringRunCreated +ScoringRunCompleted +PolicyRejected +``` + +### Functions + +Controlled verb grammar: + +```text +calculate_ +compute_ +derive_ +validate_ +normalize_ +select_ +build_ +create_ +load_ +save_ +find_ +list_ +run_ +execute_ +``` + +Meaning: + +```text +calculate / compute +→ deterministic calculation + +derive +→ obtain information from known facts + +validate +→ check validity + +build +→ construct value/object + +create +→ create resource/entity + +load +→ external read + +save +→ persistence + +find/list +→ query + +run/execute +→ workflow +``` + +Avoid generic names by default: + +```text +Manager +Helper +Utils +Common +Processor +Misc +BaseService +``` + +--- + +# 10. Test Taxonomy + +```text +tests/ +├── unit/ +├── integration/ +├── functional/ +└── architecture/ +``` + +## Unit + +Tests pure domain/application behavior. + +```text +no network +no database +no filesystem unless explicitly part of unit +``` + +Examples: + +```text +rule evaluation +score calculation +policy validation +state transitions +``` + +## Integration + +Tests real technical boundaries. + +Examples: + +```text +repository + PostgreSQL +Alembic + PostgreSQL +AI client + mocked HTTP transport +outbox + database +``` + +Prefer real production technology where practical. + +Do not substitute SQLite for PostgreSQL when PostgreSQL semantics matter. + +```text +local integration tests +→ PostgreSQL provided by compose.yaml + +CI integration tests +→ isolated PostgreSQL service container + +Testcontainers +→ not a default dependency; add only when test isolation requirements justify it +``` + +## Functional + +Tests system behavior from an external boundary. + +Examples: + +```text +HTTP request +→ FastAPI +→ application +→ persistence +→ HTTP response +``` + +Use HTTPX for FastAPI. + +## Architecture + +Tests structural invariants. + +Examples: + +```text +domain cannot import infrastructure + +application cannot import presentation + +FastAPI cannot appear in domain/application + +SQLAlchemy cannot appear in domain + +bootstrap is the only composition root +``` + +Use Import Linter plus custom AST/path checks where necessary. + +--- + +# 11. Browser E2E + +```text +e2e/ +``` + +Uses Playwright Test. + +Purpose: + +```text +deterministic user-facing regression tests +``` + +Examples: + +```text +login +create lead +score lead +review result +``` + +E2E tests must be repeatable locally and in CI. + +--- + +# 12. Agent Browser Verification + +After automated checks pass, an agent may perform exploratory browser verification. + +Preferred tooling: + +```text +Playwright CLI +or +agent-browser + +Chrome DevTools +for deep diagnostics +``` + +Agent verification flow: + +```text +open application +→ execute affected user flow +→ inspect visible state +→ inspect console +→ inspect network failures +→ screenshot final state +→ collect trace when useful +→ report evidence +``` + +Artifacts: + +```text +.artifacts/verification/ +├── screenshots/ +├── traces/ +└── videos/ +``` + +Rule: + +```text +Browser agent = discover +Automated test = preserve +``` + +If exploratory verification discovers a reproducible bug: + +```text +reproduce +→ add deterministic regression test +→ fix +→ rerun verification +``` + +--- + +# 13. Static Quality Toolchain + +## Ruff + +Owns: + +```text +formatting +linting +imports +dead code +common Python correctness checks +complexity guardrails +``` + +Ruff is not a test framework. + +## Pyright + +Owns static type correctness. + +Recommended: + +```text +strict or near-strict project configuration +``` + +Hard target: + +```text +Pyright errors = 0 +``` + +## Import Linter + +Owns dependency topology. + +Hard target: + +```text +architecture violations = 0 +``` + +--- + +# 14. Quantitative Guardrails + +Hard invariants: + +```text +Ruff errors = 0 +Pyright errors = 0 +Architecture violations = 0 +Forbidden dependency edges = 0 +Failed tests = 0 +``` + +Initial complexity guardrails: + +```text +McCabe complexity <= 10 +function arguments <= 5 +branches <= 12 +nested blocks <= 5 +returns <= 6 +statements/function <= 50 +``` + +These values are guardrails, not design objectives. + +Rule: + +```text +Do not refactor solely to game a metric. +``` + +A justified exception should remain explicit. + +--- + +# 15. Coverage + +Measure: + +```text +line coverage +branch coverage +``` + +Prefer branch coverage. + +Coverage is evidence of exercised code, not proof of correctness. + +Rule: + +```text +100% coverage is not a project objective by default. +``` + +Critical domain/application behavior matters more than raw percentage. + +--- + +# 16. Hooks + +Use: + +```text +.pre-commit-config.yaml +``` + +Compatible runners: + +```text +pre-commit +or +prek +``` + +## Fast local feedback + +On save / agent iteration: + +```text +ruff format +ruff check +pyright on affected code +``` + +## Pre-commit + +Run cheap deterministic checks: + +```text +ruff format --check +ruff check +syntax/config validation +secret checks +merge-conflict checks +cheap structure checks +``` + +## Pre-push + +Run: + +```text +pyright +lint-imports +unit tests +``` + +Hooks improve feedback latency. + +Hooks are not authoritative because they can be bypassed. + +--- + +# 17. CI + +CI is the final enforcement layer. + +```text +CI provider +→ GitHub Actions +→ workflow lives in .github/workflows/ci.yml +→ workflow invokes canonical Makefile recipes +→ migration to Gitea Actions is a repository/deployment handoff; keep shared checks in Makefile +``` + +```text +Ruff +↓ +Pyright +↓ +Import Linter +↓ +Unit tests +↓ +Integration tests +↓ +Functional tests +↓ +Architecture tests +↓ +Coverage +↓ +E2E when relevant +``` + +Required invariant: + +```text +local hook rules ⊆ CI rules +``` + +Anything important enough to block development locally must also be enforced in CI. + +## Canonical Delivery Workflow + +```text +Development +→ native backend and web processes +→ PostgreSQL from Docker Compose +→ Makefile provides the canonical commands +→ fast local feedback + +Pull request CI +→ invokes the same Makefile recipes +→ backend quality checks +→ frontend quality checks +→ isolated PostgreSQL integration tests +→ `alembic check` +→ generate the Web API client from the current FastAPI schema and type-check Web once it consumes the API +→ relevant functional and architecture tests + +Build +→ build `Dockerfile.backend` +→ build `apps/web/Dockerfile` +→ tag images with the Git commit SHA +→ test the built artifacts + +Release +→ publish the immutable backend and web images +→ run `alembic upgrade head` once as a release step +→ deploy the same images +→ check `/healthz` and `/readyz` +→ run smoke tests and relevant E2E tests + +Failure +→ stop the release or roll back to the previous image SHA +→ do not automatically downgrade production schema +``` + +```text +build once +→ test that artifact +→ deploy that artifact +``` + +The deployment platform, registry, reverse proxy, and provider-specific manifests remain deferred until the target infrastructure is selected. + +--- + +# 18. Rule Classification + +Every architectural/project rule should be classified. + +## Hard invariant + +Binary and mechanically checkable. + +Examples: + +```text +domain must not import infrastructure +ruff errors = 0 +pyright errors = 0 +tests pass +``` + +These should block CI. + +## Guardrail + +Quantitative smell indicator. + +Examples: + +```text +complexity > 10 +arguments > 5 +function > 50 statements +``` + +May block or warn depending on project maturity. + +## Semantic guidance + +Cannot reliably be reduced to a static rule. + +Examples: + +```text +Does this policy belong in Domain? +Is this name faithful to the ubiquitous language? +Is this abstraction genuinely useful? +``` + +Enforce through: + +```text +CONTEXT.md +ARCHITECTURE.md +existing exemplars +review +agent reasoning +``` + +--- + +# 19. Rule Escalation + +For every documented rule: + +```text +Can it be machine checked? + │ + yes + ↓ +lint / test / CI + │ + no + ↓ +documentation + exemplar +``` + +Do not leave mechanically enforceable architecture invariants only in Markdown. + +--- + +# 20. Agent Workflow + +Canonical implementation loop: + +```text +1. Understand task + +2. Read: + - AGENTS.md + - relevant CONTEXT terms + - relevant ARCHITECTURE rules + - relevant spec + - relevant ADR + +3. Locate canonical destination + +4. Inspect an existing exemplar + +5. Implement + +6. Run: + - Ruff + - Pyright + - Import Linter + +7. Run relevant tests + +8. Run broader affected test suite + +9. Run browser/E2E verification when applicable + +10. Inspect: + - console + - network + - screenshots/traces when useful + +11. Report: + - what changed + - tests/checks performed + - verification evidence +``` + +--- + +# 21. Placement Decision Grammar + +Agent should classify, not design architecture. + +```text +Business truth? +→ domain/ + +Application workflow? +→ application/use_cases/ + +Stable inbound contract? +→ application/ports/inbound/ + +Capability application requires externally? +→ application/ports/outbound/ + +Database implementation? +→ infrastructure/adapters/persistence/ + +External API/service implementation? +→ infrastructure/adapters/clients/ + +Messaging implementation? +→ infrastructure/adapters/messaging/ + +Environment/runtime configuration? +→ infrastructure/config/ + +HTTP transport? +→ presentation/http/ + +CLI transport? +→ presentation/cli/ + +Message/event consumer? +→ presentation/consumers/ + +Dependency construction? +→ bootstrap/ +``` + +There should be exactly one obvious answer. + +--- + +# 22. Abstraction Rule + +Code generation cost is cheap. + +Understanding and verification cost are not. + +Therefore: + +```text +Extra structure that buys semantic information +→ good + +Extra abstraction that only buys indirection +→ reject +``` + +Good: + +```text +application/ports/outbound/scoring_run_repository.py +``` + +Bad: + +```text +core/services/base_manager.py +``` + +For every new abstraction ask: + +```text +From PATH + NAME can an agent determine: + +1. what this code does? +2. which dependency directions are legal? +3. where another thing of the same kind belongs? +``` + +If not, reconsider the abstraction. + +--- + +# 23. Documentation Duplication Rule + +Canonical information must not be duplicated. + +```text +Domain definitions +→ CONTEXT.md only + +Dependency topology +→ ARCHITECTURE.md only + +Project instructions +→ AGENTS.md only + +Historical decision rationale +→ ADR only + +Behavioral requirement +→ spec only +``` + +Other documents may link to the canonical owner but should not restate it in detail. + +--- + +# 24. Programming Model + +The repository uses: + +```text +Functional Core ++ +Object-Oriented Domain where identity/state matters ++ +Imperative Shell at the edges +``` + +This is not a choice between functional programming and object-oriented programming. + +Use this decision grammar: + +```text +stateless deterministic transformation +→ function + +identity + invariant + lifecycle +→ object/entity + +immutable domain concept +→ value object + +rule spanning multiple concepts +→ pure domain policy/function + +application workflow +→ use case + +external state/effect +→ adapter +``` + +Functions are the default in Python. + +Use classes only when they provide at least one meaningful property: + +```text +identity +encapsulated invariant +state/lifecycle +resource ownership +dependency grouping +polymorphic boundary +``` + +Example: + +```python +# Deterministic transformation +def calculate_score(evidence: Evidence, policy: ScoringPolicy) -> Score: + ... + + +# Identity, lifecycle, and invariant +class ScoringRun: + def complete(self, score: Score) -> None: + if self.status is not RunStatus.RUNNING: + raise InvalidRunTransition(...) + ... + + +# Rule spanning multiple concepts +def requires_review( + score: Score, + confidence: Confidence, + policy: ReviewPolicy, +) -> bool: + ... +``` + +Do not introduce classes solely as namespaces. + +Avoid: + +```python +class ScoreCalculator: + def calculate(self, evidence: Evidence, policy: ScoringPolicy) -> Score: + ... +``` + +Prefer: + +```python +def calculate_score(evidence: Evidence, policy: ScoringPolicy) -> Score: + ... +``` + +## Functional Core / Imperative Shell + +```text +PURE CORE + +domain/ + deterministic + explicit input/output + no I/O + no clock access + no global randomness + no environment access + +ORCHESTRATION + +application/ + coordinates domain logic + requests effects through ports + +EFFECTFUL SHELL + +infrastructure/ +presentation/ +bootstrap/ + database + HTTP + filesystem + queues + environment + clock implementation + randomness implementation +``` + +Rules: + +```text +Prefer functions for stateless deterministic behavior. +Use objects for identity, invariants, lifecycle, state, +resource ownership, or meaningful polymorphism. + +Prefer immutable data. +Keep side effects at explicit boundaries. +Prefer composition over inheritance. +Do not introduce classes solely as namespaces. +``` + +--- + +# 25. Model Boundaries + +Models belong to their architectural boundary. + +```text +HTTP schema +≠ Application command/result +≠ Domain model +≠ ORM model +``` + +Canonical flow: + +```text +RequestSchema + ↓ mapping +Command + ↓ +UseCase + ↓ +Domain + ↓ +Repository Port + ↓ +Persistence Mapper + ↓ +ORM Model +``` + +Rules: + +```text +Presentation maps transport data to/from application data. +Application maps workflow inputs/results to/from domain concepts when needed. +Infrastructure maps domain data to/from persistence or external-service models. +``` + +FastAPI, Pydantic, SQLAlchemy, and provider-specific models must not leak across their owning boundaries. + +A model may be reused across a boundary only when it is already owned by the inner layer and introduces no framework or transport coupling. + +--- + +# 26. State and Mutation + +Prefer immutable values. + +Mutation is allowed only where state or lifecycle is part of the concept. + +Example immutable value object: + +```python +@dataclass(frozen=True) +class Score: + value: int +``` + +A lifecycle-bearing entity such as `ScoringRun` may be stateful, but state changes must: + +```text +occur through explicit operations +preserve invariants +reject invalid transitions +remain observable in tests +``` + +Never mutate shared state implicitly. + +Do not use module-level mutable state for domain behavior or application workflow state. + +--- + +# 27. Composition and Inheritance + +Default: + +```text +composition > inheritance +``` + +Avoid generic inheritance structures such as: + +```text +BaseService +BaseManager +AbstractRepositoryBase +BaseUseCase +``` + +Inheritance is justified only when there is a genuine substitutable abstraction with stable shared semantics. + +For outbound ports, prefer Python `Protocol` over an inheritance hierarchy created only for code reuse. + +Use composition to share implementation behavior. + +--- + +# 28. Effects and Reliability Semantics + +Effects must be explicit and owned by the correct layer. + +## Transaction ownership + +```text +Use case owns transaction semantics. +Infrastructure provides the transaction mechanism. +``` + +A transaction boundary should normally align with one application use case. + +Domain code must not open, commit, or roll back transactions. + +## Time and randomness + +Domain and application behavior must not read global time or randomness directly when those values affect behavior. + +Use explicit inputs or outbound ports such as: + +```text +Clock +IdGenerator +RandomSource +``` + +Infrastructure provides their runtime implementations. + +## External calls + +External calls belong behind outbound ports and infrastructure adapters. + +Every external operation must define the applicable semantics: + +```text +timeout +retry +idempotency +cancellation +error mapping +``` + +Rules: + +```text +Timeouts are explicit. +Retries are bounded and live at the technical boundary. +Business logic does not contain transport retries. +Retryable operations must be idempotent or carry an idempotency strategy. +Cancellation must propagate where the runtime supports it. +Provider errors are translated before crossing into application/domain code. +``` + +Do not spread retry or transaction logic across domain behavior. + +--- + +# 29. Exceptions and Waivers + +A rule suppression is an explicit, local exception—not an unnamed escape hatch. + +Every suppression must: + +```text +target a specific rule +use the narrowest possible scope +include rationale when the reason is not obvious +remain visible to review and tooling +``` + +Avoid anonymous suppressions such as: + +```python +# noqa +``` + +Prefer a targeted suppression: + +```python +result = legacy_call() # noqa: S307 -- trusted migration input; remove after ADR-012 +``` + +Use the tool-specific rule code where supported. + +An architectural exception that changes dependency direction, layer ownership, or another cross-cutting invariant requires an ADR. It must not be hidden with `# noqa`, `type: ignore`, excluded paths, or disabled checks. + +--- + +# 30. Web Architecture + +The web frontend is a separate SvelteKit application: + +```text +apps/web/ +``` + +It is not placed inside the backend `presentation/` layer. + +The backend owns business capabilities and HTTP APIs. The web application owns user-facing composition, interaction, navigation, and presentation state. + +Production runtime: + +```text +@sveltejs/adapter-node +→ builds the SvelteKit application as a Node.js server + +pnpm build +→ creates the production output in `build/` + +node build +→ runs the production web server +``` + +`adapter-node` is the default production adapter for the Docker web image. `adapter-auto` and `adapter-static` are not production defaults for this repository. + +Runtime binding: + +```text +containerized backend and web +→ bind to 0.0.0.0 + +native local process +→ bind to 127.0.0.1 by default +→ use 0.0.0.0 when another container or host needs access + +backend port +→ 8000 + +web port +→ 3000 + +PostgreSQL port +→ 5432 +``` + +`HOST` controls the network interfaces on which a process listens; it is not a public domain name. Port values use these defaults unless a deployment target requires an explicit mapping. + +Web runtime configuration: + +```text +ORIGIN +→ public origin used by SvelteKit for request/origin validation +→ local default: http://localhost:3000 +→ production value is injected by the deployment environment + +$env/dynamic/private +→ runtime server-only values such as BACKEND_INTERNAL_URL and secrets +→ may be imported only by server-side modules + +$env/dynamic/public +→ runtime values safe to expose to the browser +→ only variables with the PUBLIC_ prefix + +$env/static/* +→ reserved for values genuinely invariant across deployments +``` + +The web server calls the backend through `BACKEND_INTERNAL_URL` from server-only code by default. Do not expose backend credentials or internal service addresses through `PUBLIC_*`. Build images without environment-specific secrets or URLs; inject runtime configuration when each container starts. + +Use official SvelteKit conventions as architectural signals: + +```text +src/routes/ +→ routes, layouts, navigation, and page boundaries + +src/lib/ +→ reusable web application code + +src/lib/server/ +→ secrets, privileged I/O, sessions, and server-only backend communication + +hooks.server.ts +→ request lifecycle concerns +``` + +Do not replace these conventions with framework vocabulary such as: + +```text +pages/ +views/ +controllers/ +``` + +Do not mirror the backend's Clean Architecture directory structure inside the web application. + +The web structure is: + +```text +apps/web/src/ +├── routes/ +├── lib/ +│ ├── components/ +│ │ ├── ui/ +│ │ └── shared/ +│ ├── features/ +│ ├── server/ +│ ├── styles/ +│ └── utils/ +├── hooks.server.ts +└── app.html +``` + +Rules: + +```text +route +→ composition, navigation, and data boundary + +feature +→ user capability and product-facing behavior + +component +→ rendering and interaction + +server module +→ secrets, privileged I/O, session handling, + and backend communication +``` + +Route-specific components may remain colocated with the route. Move code into `$lib` only when it is reused or belongs to a stable feature/component category. + +Avoid large route components that combine: + +```text +API calls +business rules +state synchronization +validation +markup +styling +``` + +Business truth remains in the backend Domain/Application layers. The web may implement presentation rules and immediate input feedback, but it must not independently redefine authoritative business rules. + +--- + +# 31. Web Programming and State Model + +Use Svelte 5 runes for new reactive code. + +State ownership follows this order: + +```text +URL state +→ shareable, bookmarkable, or navigation state + +server/page data +→ authoritative remote state + +local $state +→ component-local mutable UI state + +$derived +→ deterministic computed state + +context +→ state shared within one bounded component subtree + +global state +→ last resort +``` + +Rules: + +```text +derived value +→ $derived + +external side effect +→ $effect + +ordinary application-state synchronization +→ not $effect +``` + +Keep state as close as possible to its owner. + +Do not copy server data into local state unless the user is editing a draft or the local copy has another explicit lifecycle. + +Do not introduce a global state library until URL state, page data, local state, and context have been shown insufficient. + +Every user-facing flow must define: + +```text +loading +empty +error +success +``` + +Interactive state must remain keyboard-accessible and observable in tests. + +--- + +# 32. Web Data, Forms, and Server Boundaries + +Use SvelteKit's built-in mechanisms before adding data or form libraries. + +Default decision grammar: + +```text +route/page data +→ SvelteKit load + +mutating form +→ SvelteKit form action + +progressive enhancement +→ enhance the native form + +privileged operation +→ server-only module + +experimental remote function +→ explicit project decision, not a per-task choice +``` + +Form actions are the stable default for mutations. + +Remote functions may be adopted when the project explicitly accepts their current stability and standardizes their use. Agents must not mix remote functions, direct `fetch`, custom wrappers, and third-party query libraries without a documented reason. + +Rules: + +```text +Secrets never enter client code. +Privileged backend calls run in server-only modules. +Native forms remain the baseline where practical. +Validation at the trust boundary remains authoritative. +Client validation improves feedback but does not replace server validation. +``` + +Do not introduce TanStack Query, a custom API abstraction, or a form framework without demonstrated requirements that SvelteKit does not satisfy. + +--- + +# 33. Backend-Web Contracts + +Cross-application contracts must have one source of truth. + +Canonical HTTP contract flow: + +```text +FastAPI schema + ↓ +OpenAPI + ↓ +generated TypeScript types/client + ↓ +SvelteKit web application +``` + +Hard invariant: + +```text +The web application must not manually redefine backend DTOs +when the backend publishes those DTOs through OpenAPI. +``` + +FastAPI/Pydantic remains the only source of HTTP contract definitions. Once Web consumes an API, a Makefile recipe exports OpenAPI from the current backend and generates TypeScript types/client into `apps/web/src/lib/generated/`; neither output is committed or manually edited. Local setup, CI, and the Web image build run that recipe before Web type-check/build. A generated client transports the backend contract into Web; Web feature models own only presentation-specific state. CI verifies compatibility by generating from the current backend and type-checking/building Web, not by comparing checked-in generated files. + +Do not create parallel contract packs, standalone copies of the same schemas, or fixture/manifest catalogs without an actual consumer requirement. Event contracts require explicit ownership and compatibility rules only when events are consumed across applications. + +## HTTP errors + +Use conventional HTTP statuses (401 unauthenticated, 403 forbidden, 404 not found, 409 conflict, 422 invalid input, 500 unexpected failure) and FastAPI's default error responses in v1. Domain and application errors remain HTTP-independent; `presentation/http/` maps expected errors to safe HTTP responses. Unexpected failures return a generic 500 without stack traces or secrets; details go to operational logs. Add a custom error envelope or stable business error codes only when a real client needs to distinguish cases that status alone cannot express. + +## Authentication and machine-to-machine access + +The v1 application has two caller types with separate credentials: + +```text +Human administrators +→ email/password login +→ FastAPI is the authentication and authorization authority +→ one role: admin; multiple admin accounts are allowed +→ no public self-registration; initial accounts are provisioned through `make admin-create` +→ password hashes use `pwdlib[argon2]` with Argon2id +→ server-side opaque session in PostgreSQL; generate a high-entropy token with Python `secrets`, store only its SHA-256 digest +→ session lifetime: 8 hours absolute; logout revokes the session; no sliding refresh in v1 +→ cookie attributes: HttpOnly, SameSite=Lax, Path=/, host-only (omit Domain), Secure in production + +SvelteKit Admin Web +→ acts as the server-side frontend boundary +→ forwards the session to FastAPI for protected operations +→ does not make final authorization decisions + +External applications (CRM, workers, automation) +→ call FastAPI directly using X-API-Key +→ each key identifies an application/client +→ keys cannot access admin, account, or key-management endpoints +→ admins can reveal and copy an existing key again, including after returning to the page +→ store a verifier/hash for authentication and an encrypted copy for authorized re-display +→ keys can be revoked or rotated +``` + +FastAPI validates the session and authorization on every protected admin operation. API-key authentication is separate from admin sessions; FastAPI validates each key and authorizes access to machine-facing AI endpoints. Do not trust a user ID or role supplied by the browser or an unverified internal header. + +V1 uses a minimal FastAPI-owned email/password and database-session flow: `pwdlib[argon2]` handles password hashing; Python `secrets` generates opaque session tokens; only session token digests are stored. SQLAlchemy models and Alembic own user, session, and API-key metadata. API-key management in Admin Web supports create, list metadata, reveal/copy an existing key on demand, and revoke; normal key authentication uses the verifier, not decryption. Encrypt the retrievable copy with a vetted library and a server-only `API_KEY_ENCRYPTION_KEY` kept outside the database; never expose secrets in list responses or logs, and mark reveal responses `Cache-Control: no-store`. Only an authenticated admin may request a key reveal. Do not add JWT, Redis, an authentication framework, or an external auth service by default. Do not implement cryptographic primitives manually. Add detailed API-key scopes only when distinct client permissions are required. + +Tests must cover valid/invalid login, expired/revoked sessions, protected admin access, cookie attributes, CSRF rejection for cross-origin mutations, and API-key creation, admin-only re-display, and revocation. + +Company email is a login identifier, not SSO. OIDC/SSO is deferred until the company provides an identity provider or requires centralized sign-in. Email-based verification/reset flows require an email service and are not part of the initial default. + +Browser-to-backend topology: + +```text +Browser +→ calls the same-origin SvelteKit server + +SvelteKit server +→ calls FastAPI server-to-server through BACKEND_INTERNAL_URL + +External applications +→ call FastAPI directly with X-API-Key +``` + +Session handoff: FastAPI owns credential verification, opaque token issuance, session validation, and revocation. SvelteKit uses server-side form actions and its built-in cookie API to set the browser's host-only session cookie after a server-to-server login and delete it on logout; the raw token must not appear in browser-facing JSON or logs. For protected backend calls, SvelteKit forwards only the session cookie to FastAPI, which validates it independently. Do not assume cross-domain server-side fetch forwards cookies automatically or blindly forward all browser headers. Test login → authenticated request → logout across both servers, including cookie attributes and revocation. Keep cookie-authenticated mutation endpoints behind the SvelteKit boundary; any browser-reachable FastAPI mutation that accepts cookies must enforce its own CSRF protection. + +The default browser flow does not configure CORS because the browser does not call FastAPI directly. Server-to-server calls and non-browser API clients are not governed by browser CORS. If a future requirement makes browsers call FastAPI across origins, configure an exact trusted-origin allowlist; never use a wildcard origin with credentials. + +Browser-initiated mutations use SvelteKit form actions by default. Keep SvelteKit's built-in Origin-based CSRF check enabled; configure `ORIGIN` to the public web origin when it cannot be inferred reliably. Do not rely on `SameSite=Lax` alone. Any custom browser-facing mutation endpoint outside form actions must validate the trusted Origin or use a CSRF token. Tests must verify that cross-origin mutations are rejected. + +V1 audit records live in PostgreSQL and cover important admin actions only: login attempts, configuration changes, and API-key creation, reveal, and revocation. Record the actor when known, action, target, timestamp, and outcome; never record passwords or key secrets. Successful state changes and their audit record share a database transaction where practical. Keep technical diagnostics in Python `logging` to stdout, separate from audit; do not add an audit framework or centralized logging service by default. + +AI job history is a future concern, separate from administrator audit events and operational logs. PostgreSQL is a possible starting point, not a committed `ai_jobs` table or schema; define what to record, how long to retain it, and how to display it when AI job requirements are concrete. + +--- + +# 34. Web UI System + +The web UI uses one fixed foundation: + +```text +SvelteKit +├── Svelte 5 +├── Tailwind CSS v4 +├── Bits UI +├── shadcn-svelte +├── Lucide Svelte +└── native Svelte transitions, animate, and svelte/motion +``` + +Responsibilities: + +```text +Tailwind CSS v4 +→ styling and executable design tokens + +Bits UI +→ accessible interaction primitives and behavior + +shadcn-svelte +→ project-owned component implementations + +Lucide Svelte +→ canonical icon family + +Svelte transition / animate / svelte/motion +→ UI motion +``` + +Do not mix multiple component systems or icon families. + +## Component ownership + +```text +components/ui/ +→ domain-free UI primitives + +components/shared/ +→ reusable compositions without one feature owner + +features// +→ product semantics and user capabilities +``` + +Examples: + +```text +Button +Input +Dialog +Table +Badge +FormField +→ components/ui/ + +ScoreBadge +ScoringRunTable +PolicyEditor +ReviewPanel +→ features/scoring/ or features/review/ +``` + +Hard rule: + +```text +ui/ +→ knows no business domain + +features/ +→ may compose ui/ with product semantics +``` + +Do not use a generic `components/` directory as an unclassified component store. + +## Basic component foundation + +The initial UI foundation should cover these primitives before feature-specific components: + +```text +Button, Link, IconButton +Input, Textarea, Select +Checkbox, Radio, Switch +Label, FormField, HelperText, ErrorMessage +Card, Badge, Alert +Dialog, Dropdown, Popover, Tooltip, Tabs +Table, Pagination +Skeleton, EmptyState +``` + +This is a foundation, not a requirement to build every component upfront. Add a primitive when a real feature needs it; do not create speculative components. + +Each interactive component must define: + +```text +semantic HTML and component anatomy +variants and size scale +default, hover, focus, active, disabled, loading, and error states +keyboard behavior and visible focus +accessible name, description, and validation feedback +responsive behavior and long-content handling +``` + +Prefer native HTML for simple behavior. Use Bits UI for complex accessible interactions, and keep project-owned styling and composition in `components/ui/`. + +## Design tokens + +Design tokens are semantic and executable: + +```text +color +typography +spacing +radius +shadow +breakpoints +z-index +motion duration +easing +``` + +Prefer semantic tokens: + +```text +background +foreground +surface +primary +muted +destructive +border +focus-ring +``` + +Avoid hard-coded arbitrary colors, radii, spacing, and z-index values when an existing token expresses the intent. + +Changing a token or primitive should consistently update every consumer. + +## Motion + +Use the smallest native mechanism that fits: + +```text +CSS transition +→ simple hover, focus, color, and transform feedback + +Svelte transition +→ enter and exit + +animate:flip +→ list reordering + +svelte/motion Spring or Tween +→ value-based physical or timed motion +``` + +Do not add another animation framework without a demonstrated requirement. + +Motion must communicate feedback, hierarchy, or state change. It must respect `prefers-reduced-motion`. + +--- + +# 35. Web Accessibility and UX Invariants + +Svelte accessibility warnings are zero by default. + +A suppression must follow the repository waiver rules and target a specific warning. + +Required invariants: + +```text +semantic HTML is preferred +interactive controls support keyboard operation +form controls have accessible labels +focus is visible +focus behavior is correct for dialogs and menus +color contrast meets the project accessibility target +icon-only controls have accessible names +reduced-motion preferences are respected +``` + +Do not recreate complex interactive primitives when Bits UI already provides their accessibility behavior. + +Every component and feature must account for: + +```text +long content +small viewport +keyboard navigation +disabled state +loading state +empty state +error state +``` + +Accessibility is part of implementation correctness, not a final visual review step. + +--- + +# 36. Web Verification and Dependency Policy + +Frontend verification stack: + +```text +Prettier +→ formatting + +ESLint +→ JavaScript/TypeScript linting + +svelte-check +→ Svelte and TypeScript static analysis + +Vitest +→ unit and component behavior + +Playwright +→ browser regression and complete user flows + +Playwright CLI or agent-browser +→ exploratory agent verification +``` + +Suggested verification flow: + +```text +format +→ lint +→ svelte-check +→ unit/component tests +→ build +→ Playwright when user-facing behavior changes +→ exploratory browser verification +``` + +Hard targets: + +```text +TypeScript errors = 0 +svelte-check errors = 0 +ESLint errors = 0 +Svelte accessibility warnings = 0 +failed web tests = 0 +``` + +Vitest should cover meaningful primitive states. Playwright should preserve critical assembled flows rather than duplicate every component test. + +## Dependency rule + +The approved stack is the default answer. + +A new frontend dependency is allowed only when: + +```text +the platform does not already provide the capability +Svelte/SvelteKit does not already provide the capability +an installed dependency does not already provide the capability +the requirement is demonstrated, not speculative +maintenance and compatibility cost are acceptable +``` + +Before adding a dependency, record: + +```text +capability required +existing options considered +reason the approved stack is insufficient +ownership and removal implications +``` + +A routine dependency does not require an ADR. A dependency that changes the programming model, state model, contract model, component system, or build architecture does. + +## Web boundary concerns + +The web architecture must explicitly account for: + +```text +secret handling +sensitive logging +responsive behavior +performance budgets +build and deployment target +``` + +Request/correlation IDs are deferred; add them only if tracing failures across Web and FastAPI logs becomes difficult. The authentication/session and caller model is defined in `# 33. Backend-Web Contracts`. Remaining browser/security and operational concerns must be assigned to canonical documents and enforcement mechanisms, not left as per-feature decisions. + +--- + +# 37. Central Principle + +```text +Canonical vocabulary + × +explicit repository rules + × +filesystem structure + × +mechanical enforcement + × +self-verification +``` + +The filesystem acts as semantic metadata. + +The documentation acts as explicit project knowledge. + +The tooling acts as executable constraints. + +The tests verify behavior. + +The browser gives the agent runtime visibility. + +Together they form an agent-native development environment in which: + +```text +the agent spends reasoning on the problem, +not on rediscovering how the repository works. +``` diff --git a/stack.md b/stack.md index 63966bd..c53e213 100644 --- a/stack.md +++ b/stack.md @@ -1,113 +1,12 @@ # Technology Stack -> This document records technology decisions, default candidates, deferred options, and trade-offs. It does not override the approved Lead Scoring Customer Pilot specification or released executable contracts. +This records adopted rebuild decisions, not proof that deployment or integration has passed. See `docs/specs/ai-platform-lead-scoring.md` for behavior and `ARCHITECTURE.md` for placement. -## Project Context and Boundaries +- Backend: Python 3.13, uv, FastAPI/Pydantic at HTTP boundary, PostgreSQL 16+ with SQLAlchemy async + **asyncpg**, Alembic. Domain uses Python values and pure functions; no transport/ORM models inside it. +- Runtime: one backend process, ephemeral asyncio tasks; PostgreSQL owns run durability, unique idempotency and transactional outbox. Startup terminalizes lost work before readiness. No broker/Redis or distributed leases in v1. +- AIHub: V-Key supplied per request in RAM, Bearer forwarding, medium bounded two-pass when AI is required. Actual provider protocol/model/timeout and quotas require sandbox verification; no server-wide tenant V-Key. +- Admin Web: SvelteKit 5, TypeScript 5, Bun, adapter-node after runtime verification; generated types from FastAPI OpenAPI, server-only session/backend access. DotB-Omni owns business UI. +- Tooling: pytest, Ruff, Pyright, Import Linter, Alembic lifecycle, Svelte check/Vitest and one browser flow; pre-commit opt-in, GitHub CI authoritative. uv/Bun lockfiles required. Dockerfiles separate for backend and web; Compose local only. +- `rewrite_architect.md` is a draft: its proposed psycopg/pnpm, key re-display, dynamic subscription and speculative catalog are **not** adopted. Tenant key raw is returned once; rotate if lost. -- Purpose: Build the first Python-based Lead Scoring service in AI Platform for a controlled Customer Pilot. -- Shape: Workspace containing specifications, documentation, and the initialized baseline for the Lead Scoring service (`src/lead_scoring/`). -- Material constraints: - - DotB EMS remains the CRM source of truth. - - Omni owns EMS integration, identity resolution, conversation enrichment, Lead Scoring configuration, and HITL UI. - - Lead Scoring owns scoring logic, versioned configuration, scoring-run history, and domain events. - - AIHub owns model access, V-Key validation, quota, credit, and LLM audit. - - Global runtimes and the shared `mcpls` configuration remain outside project bootstrap scope. - -## Active Decisions - -### Service runtime and HTTP framework - -- Status: Implemented baseline in `src/lead_scoring/`. -- Primary decision owner: Lead Scoring service. -- Technology: Python 3.13, `uv`, FastAPI, and Pydantic. -- Rationale: The service is Python-first, Pydantic provides executable validation, and FastAPI derives the API contract from the same models. -- Baseline implementation: `src/lead_scoring/` package, `GET /health` endpoint, async lifespan managing database engine and session factory. - -### API contract and generated client - -- Status: Implemented baseline CLI for OpenAPI generation. -- Primary decision owner: Lead Scoring API contract. -- Technology: Pydantic models as the API source of truth, FastAPI-generated OpenAPI, CLI generator with `--check` stale detection. -- Rationale: One executable backend contract avoids handwritten DTO and frontend client drift. -- Constraints: Generated artifacts (`openapi.json`) are ignored derived artifacts produced deterministically by `uv run generate-openapi` and verified for drift via `--check`. Never edit manually. - -### Shared executable Contract Pack - -- Status: Selected pre-release baseline for the Customer Pilot. -- Primary decision owner: AI Platform, with DotB-Omni review and consumer validation. -- Technology: Versioned committed directory at `contracts/lead-scoring-contract-pack/v1/`, generated from Pydantic models, OpenAPI and shared JSON fixtures. -- Rationale: One pinned artifact corpus lets provider and consumer repositories validate the same exact wire shapes without maintaining competing payload definitions. -- Versioning: Consumers pin `contract_pack_version` together with the AI Platform Git commit. Additive changes preserve the API major and existing required fields/enums; breaking changes publish a new major directory and retain the old pack. -- Verification: `uv run generate-contract-pack --validate-fixtures` validates positive and negative provider fixtures, while `uv run generate-contract-pack --check` detects missing, stale or unexpected artifacts. - -### Persistence engine and migration tooling - -- Status: Adopted and implemented baseline. -- Primary decision owner: Lead Scoring persistence and delivery boundary. -- Technology: PostgreSQL 16+, SQLAlchemy 2 async (`asyncpg`), Alembic async migration lifecycle. -- Rationale: Production requires PostgreSQL-native features (transactional outbox, jsonb, concurrency safety). SQLite fallback is strictly prohibited to prevent dialect divergence. -- Verification: Database integration tests execute real Alembic `upgrade head` and `downgrade base` lifecycle against disposable PostgreSQL 16 containers via Docker testcontainers. SQLite and ambient database fallbacks are strictly prohibited. - -### Verification and repository command surface - -- Status: Implemented baseline command surface. -- Primary decision owner: Root-level generation and verification workflow. -- Selected baseline: `uv` lockfile, Ruff, Pyright, `ty` for fast feedback, pytest (with `pytest-asyncio` and `testcontainers[postgres]`). -- Rationale: A small command surface serves local development, agents, hooks, and CI without adding a task orchestrator. -- Constraints: Pyright remains the authoritative type-checking reference. `ty` is non-authoritative fast feedback. - -### AI integration and credential boundary - -- Status: Adopted by the approved specification. -- Primary decision owner: Lead Scoring and AIHub service boundary. -- Technology: Bounded fixed-pass scoring pipelines; caller-provided `X-Tenant-VKey` held in memory by Lead Scoring and forwarded to AIHub as a Bearer token. -- Rationale: AIHub remains the single owner of model access and quota while Lead Scoring avoids persistent raw V-Key storage and an extra resolver service. -- Constraints: Raw V-Keys must not enter databases, queues, caches, logs, crash dumps, or browser code. Autonomous tools and unbounded agent loops are outside the pilot. - -### Pilot durability and event delivery - -- Status: Selected pattern; persistence baseline adopted with PostgreSQL. -- Primary decision owner: Lead Scoring persistence and delivery boundary. -- Technology: Durable scoring runs plus transactional outbox and registered webhook delivery on PostgreSQL; no dedicated message broker for the pilot. -- Rationale: This preserves run durability and retryable delivery without adding Kafka, RabbitMQ, or another operational subsystem before the workload requires one. -- Open work: Implement retry worker, outbox table schema, and webhook delivery on the adopted PostgreSQL persistence baseline. - -### Pilot scoring execution topology - -- Status: Adopted and implemented for rule-only execution. -- Primary decision owner: Lead Scoring runtime lifecycle. -- Technology: In-process `asyncio` tasks with PostgreSQL conditional claims and expiring run leases. -- Rationale: This is the smallest topology that keeps the raw tenant V-Key in the live attempt only, prevents duplicate claims, and lets database recovery terminalize orphaned work without adding a broker. -- Constraints: A process or task loss cannot resume the attempt because the V-Key is not durable. Recovery commits `worker_interrupted` with `retryable=true` and a terminal outbox event; retry is a new admission action. - -### Frontend toolchain - -- Status: Selected for the internal Dev Console; not implemented. -- Primary decision owner: AI Platform internal console, with Omni owning customer-facing configuration and HITL UI. -- Technology: Svelte 5, TypeScript, Bun, Vite, Vitest, `@sveltejs/vite-plugin-svelte`, `svelte-check`, `@sveltejs/mcp`, ESLint, and Prettier Svelte tooling. -- Rationale: This provides a small client for generated OpenAPI types without adding SvelteKit before routing, SSR, or deployment requires it. -- Constraints: The console must not duplicate Omni business UI. It uses a backend-only internal test V-Key separate from production Omni tenant credentials; raw V-Key never reaches browser code. Add a Svelte language server only if `mcpls` must serve `.svelte` files semantically. - -### Pilot trigger boundary - -- Status: Adopted by the approved specification. -- Production initiation: Manual Omni action and Captain `create_and_score_ems_lead` tool invocation. -- Internal initiation: AI Platform Dev Console using a separate backend-only test V-Key. -- Deferred: Generic automatic input-event triggers such as message-created, conversation-resolved, or EMS lifecycle events. -- Constraint: Completion events from Lead Scoring to Omni remain required and are distinct from deferred input triggers. - -## Evaluated Options - -- TypeScript 7 native compiler and LSP: `candidate` - Run project-locally as `tsc --lsp --stdio` or `tsgo --lsp --stdio`. Do not change global TypeScript, Node, or `mcpls` defaults before a project-local pilot. -- `typescript-language-server`: `current` - Existing `mcpls` backend for TypeScript and JavaScript. Its push-only diagnostics behavior does not satisfy the current `mcpls` pull-diagnostics path; upgrading the wrapper alone is not the fix. -- `svelte-language-server`: `deferred` - Add only if `mcpls` must provide semantic LSP support for `.svelte`. -- SvelteKit: `deferred` - Add only when production routing, SSR, or deployment requires it. -- `oasdiff`: `deferred` - Adopt as a blocking compatibility check after the first reviewed and versioned OpenAPI baseline exists. -- Schemathesis: `deferred` - Adopt when the real ASGI app, declared error responses, deterministic authentication, and external-service overrides exist. -- Dedicated broker such as Kafka or RabbitMQ: `deferred` - Transactional outbox and registered webhooks are sufficient for the Customer Pilot boundary. -- Monorepo orchestrator, second formatter or linter, generic agent framework, or custom build system: `rejected` - No current requirement justifies the maintenance and duplicate source-of-truth cost. - -## Unresolved Decisions - -- Production deployment and service-to-service security: Pilot caller authentication is the approved per-app opaque Bearer service-token pattern; define hosting, network exposure, per-environment secret delivery, rotation/revocation, and operational ownership before Customer Pilot deployment. -- TypeScript 7 adoption: Decide whether TS7 is the project compiler only or also the `mcpls` language-server backend after a project-local pilot verifies build, typecheck, diagnostics, and compatibility with the selected Svelte tooling. +Before production choose hosting, TLS/proxy, secret manager, DB backup/restore and retention, alerts/on-call and deployment verification. Never imply local Compose alone establishes production readiness.