From 0e8978f48c9838a396541ba938d41265f6886981 Mon Sep 17 00:00:00 2001 From: ANDREW HOSFORD Date: Tue, 7 Apr 2026 11:59:48 -0500 Subject: [PATCH] Update project files Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 3 +- .planning/MILESTONES.md | 25 + .planning/PROJECT.md | 84 ++ .planning/STATE.md | 23 + .planning/config.json | 17 + .planning/research/ARCHITECTURE.md | 1305 +++++++++++++++++ .planning/research/FEATURES.md | 492 +++++++ .planning/research/PITFALLS.md | 443 ++++++ .planning/research/STACK.md | 302 ++++ .planning/research/SUMMARY.md | 321 ++++ backend/app/database.py | 11 + backend/app/main.py | 3 +- backend/app/models.py | 4 +- backend/app/quality.py | 2 +- backend/app/routers/databases.py | 135 ++ backend/app/routers/layouts.py | 40 +- backend/app/routers/requirements.py | 73 +- backend/app/routers/stats.py | 5 + backend/app/schemas.py | 1 + backend/pyproject.toml | 1 + backend/uv.lock | 812 ++++++++++ data/demo.db | Bin 0 -> 53248 bytes data/planner - 20263103.db | Bin 0 -> 172032 bytes data/planner.db | Bin 0 -> 86016 bytes data/planner_backup_2026-03-18.db | Bin 0 -> 172032 bytes data/planner_midi-neo-pixel_archive.db | Bin 0 -> 188416 bytes data/planner_s11_complete_2026-03-17.db | Bin 0 -> 172032 bytes data/planner_state_mgmt_2026-03-17.db | Bin 0 -> 172032 bytes data/planner_stories_wip_2026-03-17.db | Bin 0 -> 172032 bytes data/planner_tech_stack_2026-03-17.db | Bin 0 -> 172032 bytes docs/neopixel-command-calculator-plan.md | 119 ++ frontend/angular.json | 3 +- frontend/src/app/app.component.html | 61 + frontend/src/app/app.component.scss | 161 ++ frontend/src/app/app.component.ts | 99 +- .../src/app/core/models/requirement.model.ts | 9 + .../app/core/services/requirement.service.ts | 46 +- .../dashboard/dashboard.component.html | 6 + .../dashboard/dashboard.component.scss | 5 + .../detail/detail-panel.component.html | 176 ++- .../detail/detail-panel.component.scss | 269 +++- .../features/detail/detail-panel.component.ts | 201 ++- .../features/graph/graph-view.component.html | 57 +- .../features/graph/graph-view.component.scss | 29 + .../features/graph/graph-view.component.ts | 178 ++- .../src/app/shared/node-picker.component.ts | 235 +++ launch.bat | 16 + launch.sh | 36 + seed_npc.py | 185 +++ update_esp32.py | 126 ++ 50 files changed, 5878 insertions(+), 241 deletions(-) create mode 100644 .planning/MILESTONES.md create mode 100644 .planning/PROJECT.md create mode 100644 .planning/STATE.md create mode 100644 .planning/config.json create mode 100644 .planning/research/ARCHITECTURE.md create mode 100644 .planning/research/FEATURES.md create mode 100644 .planning/research/PITFALLS.md create mode 100644 .planning/research/STACK.md create mode 100644 .planning/research/SUMMARY.md create mode 100644 backend/app/routers/databases.py create mode 100644 backend/uv.lock create mode 100644 data/demo.db create mode 100644 data/planner - 20263103.db create mode 100644 data/planner.db create mode 100644 data/planner_backup_2026-03-18.db create mode 100644 data/planner_midi-neo-pixel_archive.db create mode 100644 data/planner_s11_complete_2026-03-17.db create mode 100644 data/planner_state_mgmt_2026-03-17.db create mode 100644 data/planner_stories_wip_2026-03-17.db create mode 100644 data/planner_tech_stack_2026-03-17.db create mode 100644 docs/neopixel-command-calculator-plan.md create mode 100644 frontend/src/app/shared/node-picker.component.ts create mode 100644 launch.bat create mode 100644 launch.sh create mode 100644 seed_npc.py create mode 100644 update_esp32.py diff --git a/.gitignore b/.gitignore index 9f8719d..1e01cd7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,7 @@ __pycache__/ dist/ build/ -# Database -data/*.db +# Database journals (temp files) data/*.db-journal # IDE diff --git a/.planning/MILESTONES.md b/.planning/MILESTONES.md new file mode 100644 index 0000000..474fc47 --- /dev/null +++ b/.planning/MILESTONES.md @@ -0,0 +1,25 @@ +# Milestones + +## v1.0 — Core Requirements Management (Complete) + +**Released:** 2026-03-18 +**Tag:** v1.0.0 +**Commits:** 33 + +### What Shipped +- Interactive DAG graph with semantic zoom (4 levels) +- TMCS quality gates with enforcement +- Hierarchical decomposition (5 node types) +- Cross-pillar satisfaction links +- Edit history with version restore +- Named graph layouts +- Canvas undo (Ctrl+Z) +- Dashboard statistics +- Password auth with JWT +- Focus mode (shallow + deep) +- Gitea badge on graph nodes +- Full API with Swagger docs + +### Phases +- No formal phase tracking (pre-GSD) +- Last phase number: 0 diff --git a/.planning/PROJECT.md b/.planning/PROJECT.md new file mode 100644 index 0000000..621a2f9 --- /dev/null +++ b/.planning/PROJECT.md @@ -0,0 +1,84 @@ +# req-planner + +## What This Is + +A standalone requirements management tool with interactive graph visualization, hierarchical decomposition, TMCS quality gates, and code traceability. Built as a general-purpose planning tool that connects to any repository. Used by a single developer to decompose project goals into traceable, testable requirements organized as a DAG. + +## Core Value + +Every requirement is traceable from abstract goal to code-level leaf with quality validation at every level — the requirements graph IS the code graph. + +## Current Milestone: v2.0 Multi-Tenancy & User Accounts + +**Goal:** Replace single shared password with individual user accounts, per-user workspaces, role-based access control, and project sharing. + +**Target features:** +- User registration and login (individual accounts) +- Per-user project ownership and workspaces +- Role-based access control (owner, editor, viewer) +- Project sharing and collaboration + +## Requirements + +### Validated + + + +- ✓ Hierarchical requirement decomposition (pillars → requirements → sub-requirements → leaves) — v1.0 +- ✓ Interactive DAG graph with Cytoscape.js + dagre layout — v1.0 +- ✓ Semantic zoom (4 detail levels) — v1.0 +- ✓ TMCS quality gates with auto/manual validation — v1.0 +- ✓ Measurable gate enforcement (forced false without test spec) — v1.0 +- ✓ Cross-pillar satisfaction links — v1.0 +- ✓ Password-based authentication with JWT — v1.0 +- ✓ Full CRUD for requirements, projects, links, layouts — v1.0 +- ✓ Edit history with version snapshots and restore — v1.0 +- ✓ Named graph layouts (save/load/default) — v1.0 +- ✓ Canvas undo (Ctrl+Z, 20-state stack) — v1.0 +- ✓ Dashboard with statistics — v1.0 +- ✓ Gitea issue integration fields — v1.0 +- ✓ Focus mode (shallow + deep) with state preservation — v1.0 +- ✓ Multi-shortcut graph interactions (Click, Shift, Alt, Ctrl, Right-click) — v1.0 +- ✓ Gitea badge on graph nodes — v1.0 +- ✓ Detail panel with inline editing, history, links — v1.0 + +### Active + + + +(Defining requirements) + +### Out of Scope + +- Real-time collaborative editing — complexity too high for v2, consider v3 +- OAuth/SSO providers — email/password sufficient initially +- Mobile app — web-first + +## Context + +- Backend: Python 3.12+, FastAPI, SQLAlchemy 2.0, SQLite +- Frontend: Angular 17, Cytoscape.js, RxJS, SCSS dark theme +- Database: SQLite at data/planner.db with 5 tables (requirement_nodes, cross_pillar_links, projects, requirement_history, graph_layouts) +- Current auth: single shared password with JWT tokens (24h expiry) +- Gitea instance at theres-a-git-in-this-tea.andrewawesomo.net for issue tracking +- Currently single-user — all data shared, no user isolation + +## Constraints + +- **Tech stack**: Must stay FastAPI + Angular + SQLite — no database migration +- **Backwards compatible**: Existing data must survive the migration +- **Self-hosted**: Runs on user's own infrastructure, no cloud dependencies + +## Key Decisions + +| Decision | Rationale | Outcome | +|----------|-----------|---------| +| SQLite over PostgreSQL | Single-user simplicity, zero config | ✓ Good | +| JWT over sessions | Stateless, works with SPA | ✓ Good | +| Cytoscape.js for graph | Mature, supports semantic zoom | ✓ Good | +| localStorage for transient state | Fast, no backend round-trips | ✓ Good | +| Backend DB for durable state | Survives cache clear, device-independent | ✓ Good | +| Single password auth | Quick to implement for v1 | ⚠️ Revisit — replacing in v2 | + +--- +*Last updated: 2026-03-18 after v1.0 release* diff --git a/.planning/STATE.md b/.planning/STATE.md new file mode 100644 index 0000000..d8e7d35 --- /dev/null +++ b/.planning/STATE.md @@ -0,0 +1,23 @@ +# State + +## Project Reference + +See: .planning/PROJECT.md (updated 2026-03-18) + +**Core value:** Every requirement is traceable from abstract goal to code-level leaf with quality validation at every level +**Current focus:** Defining requirements for v2.0 + +## Current Position + +Phase: Not started (defining requirements) +Plan: — +Status: Defining requirements +Last activity: 2026-03-18 — Milestone v2.0 started + +## Accumulated Context + +- v1.0 released and tagged (33 commits) +- Backend runs on port 8100, frontend on 4200 +- Current auth is single shared password ("what_is_this_2") with JWT +- SQLite DB has 5 tables, auto-created via create_all() +- Gitea milestone #15 created for v2.0 diff --git a/.planning/config.json b/.planning/config.json new file mode 100644 index 0000000..737bdb7 --- /dev/null +++ b/.planning/config.json @@ -0,0 +1,17 @@ +{ + "version": "1.0.0", + "workflow": { + "research": true, + "commit_docs": true, + "auto_verify": false + }, + "models": { + "researcher": "sonnet", + "synthesizer": "sonnet", + "planner": "sonnet", + "plan_checker": "sonnet", + "executor": "sonnet", + "verifier": "sonnet", + "roadmapper": "sonnet" + } +} diff --git a/.planning/research/ARCHITECTURE.md b/.planning/research/ARCHITECTURE.md new file mode 100644 index 0000000..473de0a --- /dev/null +++ b/.planning/research/ARCHITECTURE.md @@ -0,0 +1,1305 @@ +# Architecture Research: Multi-Tenancy & User Accounts Integration + +**Domain:** Requirements management system (FastAPI + SQLAlchemy 2.0 + SQLite) +**Researched:** 2026-03-18 +**Confidence:** HIGH + +## Executive Summary + +Adding user accounts and multi-tenancy to an existing FastAPI + SQLAlchemy + SQLite application requires four architectural layers: **Authentication** (user identity), **Authorization** (permissions), **Tenant Isolation** (data separation), and **Migration Strategy** (backwards compatibility). The recommended approach uses shared-schema multi-tenancy with tenant-scoped queries, JWT tokens with refresh tokens, RBAC via junction tables, and FastAPI's dependency injection for context management. + +**Key decision:** Use shared-schema multi-tenancy (single database, user_id + tenant_id filtering) rather than database-per-tenant, because SQLite's single-writer lock makes multiple active databases perform worse than filtered queries on a single database. + +## Existing Architecture Analysis + +### Current State + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Angular 17 Frontend │ +│ - AuthGuard + HTTP Interceptor for token injection │ +│ - localStorage for token storage │ +└─────────────────────┬───────────────────────────────────────┘ + │ HTTP/JSON +┌─────────────────────┴───────────────────────────────────────┐ +│ FastAPI Backend │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ auth.py: Single password → JWT (24h, no refresh) │ │ +│ │ verify_token() dependency on all protected routes │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ Routers: requirements, projects, links, layouts, │ │ +│ │ stats (all protected by verify_token) │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ SQLAlchemy 2.0 ORM with get_db() dependency │ │ +│ └────────────────────────────────────────────────────────┘ │ +└─────────────────────┬───────────────────────────────────────┘ + │ +┌─────────────────────┴───────────────────────────────────────┐ +│ SQLite Database │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ requirement_nodes│ │ projects │ │ +│ │ (no user_id) │ │ (no user_id) │ │ +│ └──────────────────┘ └──────────────────┘ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ cross_pillar_links│ │ requirement_history│ │ +│ │ │ │ (no user_id) │ │ +│ └──────────────────┘ └──────────────────┘ │ +│ ┌──────────────────┐ │ +│ │ graph_layouts │ │ +│ │ (no user_id) │ │ +│ └──────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Architecture Gaps + +| Component | Current State | Required for v2.0 | +|-----------|---------------|-------------------| +| **Authentication** | Single password, 24h JWT | User accounts with email/password, refresh tokens | +| **User Model** | None | Users table with hashed passwords | +| **Tenant Isolation** | None (all data shared) | Per-user project ownership, user_id filtering | +| **Authorization** | Binary (authenticated or not) | Role-based access (owner/editor/viewer per project) | +| **Data Attribution** | No edit tracking | User attribution in history | +| **Invite System** | N/A | Invite-only registration with tokens | +| **Session Management** | Stateless JWT (24h only) | Refresh tokens (long-lived, revocable) | + +## Recommended Architecture + +### Target State with Multi-Tenancy + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Angular 17 Frontend │ +│ - AuthGuard with user profile │ +│ - HTTP Interceptor with token refresh logic │ +│ - localStorage: access_token + refresh_token │ +└─────────────────────┬───────────────────────────────────────┘ + │ HTTP/JSON +┌─────────────────────┴───────────────────────────────────────┐ +│ FastAPI Backend │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ Authentication Layer (auth.py + new users.py) │ │ +│ │ - POST /auth/register (invite token required) │ │ +│ │ - POST /auth/login (email + password → tokens) │ │ +│ │ - POST /auth/refresh (refresh_token → new access) │ │ +│ │ - POST /auth/logout (revoke refresh_token) │ │ +│ │ - GET /auth/me (current user profile) │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ Dependency Injection Context │ │ +│ │ get_current_user() → UserContext (user_id, email) │ │ +│ │ require_project_access(role) → enforces RBAC │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ Tenant-Scoped Query Middleware │ │ +│ │ Auto-inject user_id filter on all queries │ │ +│ │ (via SQLAlchemy session events) │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ Routers (modified for user context) │ │ +│ │ - requirements: add created_by, updated_by │ │ +│ │ - projects: add owner_id, check permissions │ │ +│ │ - links, layouts, stats: scope to accessible projects │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ SQLAlchemy 2.0 ORM │ │ +│ │ get_db_with_tenant_filter() → scoped session │ │ +│ └────────────────────────────────────────────────────────┘ │ +└─────────────────────┬───────────────────────────────────────┘ + │ +┌─────────────────────┴───────────────────────────────────────┐ +│ SQLite Database │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ users │ │ projects │ │ +│ │ (NEW) │ │ + owner_id (FK) │ │ +│ └──────────────────┘ └──────────────────┘ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ project_members │ │ requirement_nodes│ │ +│ │ (NEW - RBAC) │ │ + created_by_id │ │ +│ │ │ │ + updated_by_id │ │ +│ └──────────────────┘ └──────────────────┘ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ invite_tokens │ │ requirement_history│ │ +│ │ (NEW) │ │ + user_id │ │ +│ └──────────────────┘ └──────────────────┘ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ refresh_tokens │ │ graph_layouts │ │ +│ │ (NEW) │ │ (no change) │ │ +│ └──────────────────┘ └──────────────────┘ │ +│ ┌──────────────────┐ │ +│ │ cross_pillar_links│ (no change - inherits access from │ +│ │ │ parent requirement) │ +│ └──────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## New Database Schema + +### New Tables + +#### 1. users (Core Identity) + +```python +class User(Base): + __tablename__ = "users" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True) + hashed_password: Mapped[str] = mapped_column(String(255), nullable=False) + full_name: Mapped[str | None] = mapped_column(String(255), nullable=True) + is_active: Mapped[bool] = mapped_column(Boolean, default=True) + is_admin: Mapped[bool] = mapped_column(Boolean, default=False) # Global admin flag + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow) + last_login: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + + # Relationships + owned_projects: Mapped[list["Project"]] = relationship("Project", back_populates="owner") + project_memberships: Mapped[list["ProjectMember"]] = relationship( + "ProjectMember", back_populates="user", cascade="all, delete-orphan" + ) + created_requirements: Mapped[list["RequirementNode"]] = relationship( + "RequirementNode", foreign_keys="RequirementNode.created_by_id", back_populates="created_by" + ) + refresh_tokens: Mapped[list["RefreshToken"]] = relationship( + "RefreshToken", back_populates="user", cascade="all, delete-orphan" + ) + +__table_args__ = ( + Index("idx_users_email_active", "email", "is_active"), # Fast login lookup +) +``` + +**Why this design:** +- `email` as primary identity (unique, indexed for fast login) +- `is_admin` for global operations (e.g., invite token generation) +- `is_active` for soft account suspension without data loss +- `last_login` for monitoring and security auditing + +#### 2. project_members (RBAC Junction Table) + +```python +class ProjectMemberRole(str, enum.Enum): + owner = "owner" # Full control, can delete project + editor = "editor" # Can modify requirements, cannot manage members + viewer = "viewer" # Read-only access + +class ProjectMember(Base): + __tablename__ = "project_members" + + project_id: Mapped[int] = mapped_column( + Integer, ForeignKey("projects.id", ondelete="CASCADE"), primary_key=True + ) + user_id: Mapped[int] = mapped_column( + Integer, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True + ) + role: Mapped[ProjectMemberRole] = mapped_column( + Enum(ProjectMemberRole), nullable=False, default=ProjectMemberRole.viewer + ) + added_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow) + added_by_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + + # Relationships + project: Mapped["Project"] = relationship("Project", back_populates="members") + user: Mapped["User"] = relationship("User", back_populates="project_memberships") + +__table_args__ = ( + Index("idx_project_members_user", "user_id"), # Find all projects for a user + Index("idx_project_members_project", "project_id"), # Find all members of a project +) +``` + +**Why this design:** +- Composite primary key (project_id, user_id) prevents duplicate memberships +- Three-tier role model (owner/editor/viewer) covers most collaboration patterns +- `added_by_id` for audit trail (who invited whom) +- Cascade delete ensures cleanup when projects or users are deleted + +#### 3. invite_tokens (Invite-Only Registration) + +```python +class InviteToken(Base): + __tablename__ = "invite_tokens" + + token: Mapped[str] = mapped_column(String(64), primary_key=True) # UUID or secure random + email: Mapped[str] = mapped_column(String(255), nullable=False, index=True) + created_by_id: Mapped[int] = mapped_column( + Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False + ) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow) + expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) + used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + used_by_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + +__table_args__ = ( + Index("idx_invite_tokens_email_used", "email", "used_at"), # Check if email already invited + Index("idx_invite_tokens_expires", "expires_at"), # Cleanup expired tokens +) +``` + +**Why this design:** +- Token as primary key (opaque, unpredictable) +- `email` scoped invites prevent multiple registrations with same token +- `expires_at` for time-limited invites (e.g., 7 days) +- `used_at` / `used_by_id` track invite consumption (one-time use) +- Created by existing users (only authenticated users can invite) + +#### 4. refresh_tokens (Session Management) + +```python +class RefreshToken(Base): + __tablename__ = "refresh_tokens" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + user_id: Mapped[int] = mapped_column( + Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True + ) + token_hash: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True) + expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow) + revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + + # Relationships + user: Mapped["User"] = relationship("User", back_populates="refresh_tokens") + +__table_args__ = ( + Index("idx_refresh_tokens_user_active", "user_id", "revoked_at"), # Active tokens per user + Index("idx_refresh_tokens_expires", "expires_at"), # Cleanup expired tokens +) +``` + +**Why this design:** +- `token_hash` stores hashed refresh token (never store plaintext tokens in DB) +- `expires_at` for long-lived but not permanent tokens (e.g., 30 days) +- `revoked_at` enables explicit logout (revoke token) +- `last_used_at` for security monitoring (detect stolen tokens via unusual usage patterns) +- Multiple refresh tokens per user allowed (multi-device support) + +### Modified Existing Tables + +#### projects (Add Ownership) + +```python +# ADD to existing Project model: +owner_id: Mapped[int] = mapped_column( + Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False +) + +owner: Mapped["User"] = relationship("User", back_populates="owned_projects") +members: Mapped[list["ProjectMember"]] = relationship( + "ProjectMember", back_populates="project", cascade="all, delete-orphan" +) +``` + +**Migration strategy:** +- New column `owner_id` with `server_default=1` (assumes first admin user has id=1) +- Run data migration to assign existing projects to the admin user +- After migration, make `owner_id` NOT NULL + +#### requirement_nodes (Add Attribution) + +```python +# ADD to existing RequirementNode model: +created_by_id: Mapped[int] = mapped_column( + Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True +) +updated_by_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True +) + +created_by: Mapped["User | None"] = relationship( + "User", foreign_keys=[created_by_id], back_populates="created_requirements" +) +updated_by: Mapped["User | None"] = relationship( + "User", foreign_keys=[updated_by_id] +) +``` + +**Migration strategy:** +- Both columns nullable (existing data has NULL creator) +- `SET NULL` on user deletion preserves history even if user deleted +- Display "Unknown User" in UI when created_by_id is NULL + +#### requirement_history (Add User Attribution) + +```python +# ADD to existing RequirementHistory model: +user_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True +) + +user: Mapped["User | None"] = relationship("User") +``` + +**Migration strategy:** +- Nullable because historical edits have no user attribution +- New history entries always have user_id (enforced in router logic) + +## Integration Patterns + +### Pattern 1: User Context Injection (Dependency Injection) + +**What:** Extract authenticated user from JWT and provide as dependency to route handlers. + +**Implementation:** + +```python +# In auth.py +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer +from jose import JWTError, jwt +from pydantic import BaseModel + +security = HTTPBearer() + +class UserContext(BaseModel): + user_id: int + email: str + is_admin: bool + +async def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(security), + db: Session = Depends(get_db) +) -> UserContext: + """Extract user from JWT access token.""" + try: + payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM]) + user_id: int = payload.get("sub") + if user_id is None: + raise HTTPException(status_code=401, detail="Invalid token") + except JWTError: + raise HTTPException(status_code=401, detail="Invalid or expired token") + + # Verify user exists and is active + user = db.query(User).filter(User.id == user_id, User.is_active == True).first() + if not user: + raise HTTPException(status_code=401, detail="User not found or inactive") + + return UserContext(user_id=user.id, email=user.email, is_admin=user.is_admin) + +# Usage in routes: +@router.get("/api/projects") +def list_projects( + db: Session = Depends(get_db), + current_user: UserContext = Depends(get_current_user) +): + # current_user available here + pass +``` + +**Why this works:** +- FastAPI's dependency injection runs get_current_user before route handler +- Raises HTTPException if token invalid (automatic 401 response) +- UserContext is immutable (Pydantic model) preventing tampering +- No middleware needed—explicit dependencies are clearer + +### Pattern 2: RBAC Authorization (Permission Checking) + +**What:** Check user's role on a project before allowing operations. + +**Implementation:** + +```python +# In auth.py +from functools import wraps +from typing import Callable + +class PermissionDenied(HTTPException): + def __init__(self): + super().__init__(status_code=403, detail="Insufficient permissions") + +def require_project_access( + project_id: int, + required_role: ProjectMemberRole, + db: Session, + current_user: UserContext +) -> ProjectMember: + """ + Check if user has required role on project. + Returns ProjectMember if authorized, raises 403 otherwise. + + Role hierarchy: owner > editor > viewer + """ + # Global admins bypass all checks + if current_user.is_admin: + return ProjectMember( + project_id=project_id, + user_id=current_user.user_id, + role=ProjectMemberRole.owner + ) + + # Check ownership + project = db.query(Project).filter(Project.id == project_id).first() + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + if project.owner_id == current_user.user_id: + return ProjectMember( + project_id=project_id, + user_id=current_user.user_id, + role=ProjectMemberRole.owner + ) + + # Check membership + member = db.query(ProjectMember).filter( + ProjectMember.project_id == project_id, + ProjectMember.user_id == current_user.user_id + ).first() + + if not member: + raise PermissionDenied() + + # Role hierarchy check + role_order = { + ProjectMemberRole.viewer: 1, + ProjectMemberRole.editor: 2, + ProjectMemberRole.owner: 3, + } + + if role_order[member.role] < role_order[required_role]: + raise PermissionDenied() + + return member + +# Usage in routes: +@router.delete("/api/requirements/{req_id}") +def delete_requirement( + req_id: str, + db: Session = Depends(get_db), + current_user: UserContext = Depends(get_current_user) +): + req = db.query(RequirementNode).filter(RequirementNode.id == req_id).first() + if not req: + raise HTTPException(status_code=404) + + # Check editor access on project + require_project_access( + project_id=req.project_id, + required_role=ProjectMemberRole.editor, + db=db, + current_user=current_user + ) + + # Authorized—proceed with deletion + db.delete(req) + db.commit() +``` + +**Why this works:** +- Explicit permission checks at operation level (not blanket middleware) +- Role hierarchy encoded in single function (DRY principle) +- Global admins bypass for administrative operations +- Project owners inherit full access automatically + +### Pattern 3: Tenant-Scoped Queries (Data Isolation) + +**What:** Automatically filter queries to only return data user has access to. + +**Implementation (Option A: Manual Filtering - Recommended for SQLite):** + +```python +# In routers/projects.py +@router.get("/api/projects", response_model=list[ProjectResponse]) +def list_projects( + db: Session = Depends(get_db), + current_user: UserContext = Depends(get_current_user) +): + """List all projects accessible to current user.""" + if current_user.is_admin: + # Admins see all projects + projects = db.query(Project).all() + else: + # Users see owned + member projects + owned = db.query(Project).filter(Project.owner_id == current_user.user_id) + member = ( + db.query(Project) + .join(ProjectMember) + .filter(ProjectMember.user_id == current_user.user_id) + ) + projects = owned.union(member).all() + + return projects +``` + +**Implementation (Option B: SQLAlchemy Session Event Hooks - Advanced):** + +```python +# In database.py +from sqlalchemy import event +from sqlalchemy.orm import Session + +class TenantSession(Session): + """Session subclass that auto-filters queries by user_id.""" + def __init__(self, user_id: int | None = None, **kwargs): + super().__init__(**kwargs) + self.user_id = user_id + +@event.listens_for(TenantSession, "do_orm_execute") +def receive_do_orm_execute(execute_state): + """Intercept queries and inject tenant filters.""" + if execute_state.is_select and execute_state.session.user_id: + # Auto-add user_id filter to all queries on tables with user_id column + # (Complex implementation—see SQLAlchemy docs) + pass + +# In dependency: +def get_tenant_db(current_user: UserContext = Depends(get_current_user)): + db = TenantSession(user_id=current_user.user_id, bind=engine) + try: + yield db + finally: + db.close() +``` + +**Recommendation:** Use **Option A (Manual Filtering)** for this project because: +- SQLite doesn't support Row Level Security like PostgreSQL +- Explicit filters are easier to understand and debug +- Session event hooks add complexity and can break with ORM updates +- Project has moderate query count—manual filters are manageable + +### Pattern 4: Refresh Token Rotation (Session Security) + +**What:** Short-lived access tokens (15min) with long-lived refresh tokens (30 days). + +**Implementation:** + +```python +# In auth.py +ACCESS_TOKEN_EXPIRE_MINUTES = 15 +REFRESH_TOKEN_EXPIRE_DAYS = 30 + +def create_access_token(user_id: int) -> str: + """Short-lived token for API requests.""" + expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + payload = {"sub": user_id, "exp": expire, "type": "access"} + return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM) + +def create_refresh_token(db: Session, user_id: int) -> str: + """Long-lived token for obtaining new access tokens.""" + token = secrets.token_urlsafe(32) # Cryptographically secure random + token_hash = hashlib.sha256(token.encode()).hexdigest() + + expires_at = datetime.now(timezone.utc) + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS) + + db_token = RefreshToken( + user_id=user_id, + token_hash=token_hash, + expires_at=expires_at + ) + db.add(db_token) + db.commit() + + return token # Return plaintext (only time it's visible) + +@router.post("/auth/refresh", response_model=TokenResponse) +def refresh_access_token( + refresh_token: str, + db: Session = Depends(get_db) +): + """Exchange refresh token for new access token.""" + token_hash = hashlib.sha256(refresh_token.encode()).hexdigest() + + db_token = db.query(RefreshToken).filter( + RefreshToken.token_hash == token_hash, + RefreshToken.revoked_at.is_(None), + RefreshToken.expires_at > datetime.now(timezone.utc) + ).first() + + if not db_token: + raise HTTPException(status_code=401, detail="Invalid or expired refresh token") + + # Update last used timestamp + db_token.last_used_at = datetime.now(timezone.utc) + db.commit() + + # Generate new access token + access_token = create_access_token(db_token.user_id) + + return TokenResponse(access_token=access_token) + +@router.post("/auth/logout") +def logout( + refresh_token: str, + db: Session = Depends(get_db), + current_user: UserContext = Depends(get_current_user) +): + """Revoke refresh token (logout).""" + token_hash = hashlib.sha256(refresh_token.encode()).hexdigest() + + db_token = db.query(RefreshToken).filter( + RefreshToken.token_hash == token_hash, + RefreshToken.user_id == current_user.user_id + ).first() + + if db_token: + db_token.revoked_at = datetime.now(timezone.utc) + db.commit() + + return {"message": "Logged out"} +``` + +**Frontend integration (Angular):** + +```typescript +// In auth.interceptor.ts +intercept(req: HttpRequest, next: HttpHandler): Observable> { + return next.handle(this.addToken(req)).pipe( + catchError((error: HttpErrorResponse) => { + if (error.status === 401 && this.getRefreshToken()) { + return this.handle401Error(req, next); + } + return throwError(() => error); + }) + ); +} + +private handle401Error(req: HttpRequest, next: HttpHandler) { + if (!this.isRefreshing) { + this.isRefreshing = true; + this.refreshTokenSubject.next(null); + + return this.authService.refreshToken().pipe( + switchMap((token: any) => { + this.isRefreshing = false; + this.refreshTokenSubject.next(token.access_token); + return next.handle(this.addToken(req)); + }), + catchError((err) => { + this.isRefreshing = false; + this.authService.logout(); + return throwError(() => err); + }) + ); + } + + return this.refreshTokenSubject.pipe( + filter(token => token !== null), + take(1), + switchMap(() => next.handle(this.addToken(req))) + ); +} +``` + +**Why this works:** +- Access tokens short-lived (minimal damage if stolen) +- Refresh tokens stored in httpOnly cookies or secure storage (harder to steal) +- Token rotation prevents replay attacks +- Explicit revocation enables secure logout + +### Pattern 5: Invite-Only Registration Flow + +**What:** Only users with valid invite tokens can register. + +**Implementation:** + +```python +# In auth.py +@router.post("/auth/invites", dependencies=[Depends(get_current_user)]) +def create_invite( + email: str, + db: Session = Depends(get_db), + current_user: UserContext = Depends(get_current_user) +): + """Create invite token (authenticated users only).""" + # Check if email already registered + if db.query(User).filter(User.email == email).first(): + raise HTTPException(status_code=400, detail="User already exists") + + # Check for unused invite + existing = db.query(InviteToken).filter( + InviteToken.email == email, + InviteToken.used_at.is_(None), + InviteToken.expires_at > datetime.now(timezone.utc) + ).first() + + if existing: + raise HTTPException(status_code=400, detail="Invite already sent") + + # Generate token + token = secrets.token_urlsafe(32) + expires_at = datetime.now(timezone.utc) + timedelta(days=7) + + invite = InviteToken( + token=token, + email=email, + created_by_id=current_user.user_id, + expires_at=expires_at + ) + db.add(invite) + db.commit() + + # TODO: Send email with registration link + # send_invite_email(email, f"https://app.example.com/register?token={token}") + + return {"token": token, "expires_at": expires_at} + +@router.post("/auth/register", response_model=TokenResponse) +def register( + invite_token: str, + email: str, + password: str, + full_name: str | None, + db: Session = Depends(get_db) +): + """Register new user with invite token.""" + # Validate invite + invite = db.query(InviteToken).filter( + InviteToken.token == invite_token, + InviteToken.email == email, + InviteToken.used_at.is_(None), + InviteToken.expires_at > datetime.now(timezone.utc) + ).first() + + if not invite: + raise HTTPException(status_code=400, detail="Invalid or expired invite") + + # Check if user exists + if db.query(User).filter(User.email == email).first(): + raise HTTPException(status_code=400, detail="User already exists") + + # Create user + hashed_password = get_password_hash(password) + user = User( + email=email, + hashed_password=hashed_password, + full_name=full_name + ) + db.add(user) + + # Mark invite as used + invite.used_at = datetime.now(timezone.utc) + invite.used_by_id = user.id + + db.commit() + + # Generate tokens + access_token = create_access_token(user.id) + refresh_token = create_refresh_token(db, user.id) + + return TokenResponse( + access_token=access_token, + refresh_token=refresh_token + ) +``` + +**Why this works:** +- Email scoped invites prevent token reuse +- Expiration limits token lifespan +- Audit trail tracks who invited whom +- One-time use prevents invite sharing + +## Data Flow Changes + +### Before (v1.0 - Single Password) + +``` +User enters password + ↓ +POST /api/auth/login {password} + ↓ +Backend validates against single PASSWORD env var + ↓ +If valid: JWT created with {exp} only + ↓ +Frontend stores token in localStorage + ↓ +All API requests include: Authorization: Bearer + ↓ +verify_token() checks JWT signature and expiration + ↓ +If valid: route handler executes (no user context) +``` + +### After (v2.0 - User Accounts) + +``` +User enters email + password + ↓ +POST /api/auth/login {email, password} + ↓ +Backend queries users table by email + ↓ +Verify hashed_password with bcrypt + ↓ +If valid: Generate access_token + refresh_token + ↓ +Frontend stores both tokens (localStorage or httpOnly cookie) + ↓ +API requests include: Authorization: Bearer + ↓ +get_current_user() extracts user_id from JWT + ↓ +Queries users table to verify user still active + ↓ +Returns UserContext(user_id, email, is_admin) + ↓ +Route handler receives current_user dependency + ↓ +Permission checks against project_members table + ↓ +Queries auto-filtered by user's accessible projects + ↓ +Write operations tag created_by_id / updated_by_id +``` + +### New: Token Refresh Flow + +``` +Access token expires (15min) + ↓ +Frontend receives 401 on API request + ↓ +Interceptor catches 401, calls POST /api/auth/refresh {refresh_token} + ↓ +Backend validates refresh_token (hash lookup in DB) + ↓ +Checks: not revoked, not expired, user active + ↓ +If valid: Generate new access_token (refresh_token unchanged) + ↓ +Updates last_used_at timestamp on refresh_token + ↓ +Returns new access_token + ↓ +Frontend retries original request with new token + ↓ +(Transparent to user—no re-login needed) +``` + +## Migration Strategy (Backwards Compatibility) + +### Phase 1: Add New Tables (Non-Breaking) + +```python +# Alembic migration: add users, project_members, invite_tokens, refresh_tokens +# No existing data affected—tables are empty initially +``` + +### Phase 2: Add Columns with Nullable (Non-Breaking) + +```python +# Alembic migration: +# - ALTER TABLE projects ADD COLUMN owner_id INTEGER REFERENCES users(id) +# - ALTER TABLE requirement_nodes ADD COLUMN created_by_id INTEGER REFERENCES users(id) +# - ALTER TABLE requirement_nodes ADD COLUMN updated_by_id INTEGER REFERENCES users(id) +# - ALTER TABLE requirement_history ADD COLUMN user_id INTEGER REFERENCES users(id) + +# All columns nullable initially—existing data has NULL +``` + +### Phase 3: Seed Default Admin User + +```python +# Data migration script: +default_user = User( + id=1, + email="admin@localhost", + hashed_password=get_password_hash(os.environ.get("ADMIN_PASSWORD", "changeme")), + full_name="System Admin", + is_admin=True +) +db.add(default_user) + +# Assign all existing projects to admin +db.execute( + update(Project).values(owner_id=1) +) + +# Mark all existing requirements as created by admin +db.execute( + update(RequirementNode) + .where(RequirementNode.created_by_id.is_(None)) + .values(created_by_id=1) +) + +db.commit() +``` + +### Phase 4: Deploy New Auth Endpoints (Dual Auth) + +```python +# Keep old /api/auth/login {password} working +# Add new /api/auth/login {email, password} +# Frontend updated to use new endpoint +# Old endpoint deprecated but functional (gradual migration) +``` + +### Phase 5: Make owner_id NOT NULL (Breaking) + +```python +# After confirming all projects have owner_id: +# Alembic migration: +# - ALTER TABLE projects ALTER COLUMN owner_id SET NOT NULL +``` + +### Rollback Plan + +If issues discovered after deployment: +1. **Revert code:** Old auth.py still validates tokens (SECRET_KEY unchanged) +2. **Database intact:** New columns nullable—old queries unaffected +3. **No data loss:** All existing data preserved with owner_id=1 + +## Build Order (Recommended Sequence) + +### Milestone 1: Authentication Foundation + +**Goal:** Users can register and login with email/password. + +1. Create `User` model and table +2. Create `InviteToken` model and table +3. Create `RefreshToken` model and table +4. Implement password hashing (passlib + bcrypt) +5. Add `/auth/register` endpoint (with invite token validation) +6. Add `/auth/login` endpoint (email + password → tokens) +7. Add `/auth/refresh` endpoint (refresh token → new access token) +8. Add `/auth/logout` endpoint (revoke refresh token) +9. Update `get_current_user()` dependency +10. Seed default admin user in migration + +**Test:** User can register with invite, login, and receive valid tokens. + +### Milestone 2: Project Ownership + +**Goal:** Projects belong to users, users see only their projects. + +1. Add `owner_id` column to `projects` (nullable) +2. Update project creation to set `owner_id = current_user.user_id` +3. Update `GET /api/projects` to filter by ownership +4. Update `GET /api/requirements` to filter by project ownership +5. Data migration: Assign existing projects to admin +6. Make `owner_id` NOT NULL + +**Test:** User sees only projects they own; other users' projects hidden. + +### Milestone 3: RBAC (Project Sharing) + +**Goal:** Project owners can invite collaborators with roles. + +1. Create `ProjectMember` model and table +2. Add `POST /api/projects/{id}/members` (owner adds member) +3. Add `GET /api/projects/{id}/members` (list members) +4. Add `DELETE /api/projects/{id}/members/{user_id}` (remove member) +5. Implement `require_project_access()` dependency +6. Update all requirement routes to check RBAC +7. Add role field to project list response (user's role per project) + +**Test:** Owner invites editor; editor can modify requirements but not delete project. + +### Milestone 4: Edit Attribution + +**Goal:** History tracks who made each change. + +1. Add `created_by_id`, `updated_by_id` to `requirement_nodes` +2. Add `user_id` to `requirement_history` +3. Update requirement creation to set `created_by_id` +4. Update requirement updates to set `updated_by_id` +5. Update history creation to set `user_id` +6. Update UI to display editor names in history + +**Test:** History panel shows "Edited by Alice" instead of generic timestamps. + +### Milestone 5: Invite System + +**Goal:** Users can invite others to join the platform. + +1. Add `POST /api/auth/invites` (create invite token) +2. Add `GET /api/auth/invites` (list my sent invites) +3. Add invite email template +4. Integrate email sending (SMTP or service like Mailgun) +5. Update registration flow to send welcome email + +**Test:** User creates invite; recipient registers successfully; both users see each other's projects if shared. + +## Anti-Patterns to Avoid + +### Anti-Pattern 1: Trusting Client-Provided user_id + +**What people do:** +```python +@router.post("/api/requirements") +def create_requirement(data: RequirementCreate, user_id: int): # ❌ DANGEROUS + node = RequirementNode(**data.dict(), created_by_id=user_id) + # Attacker can impersonate anyone by sending any user_id +``` + +**Why it's wrong:** Clients can lie. Attacker sends `user_id=1` to impersonate admin. + +**Do this instead:** +```python +@router.post("/api/requirements") +def create_requirement( + data: RequirementCreate, + current_user: UserContext = Depends(get_current_user) # ✅ Server extracts from JWT +): + node = RequirementNode(**data.dict(), created_by_id=current_user.user_id) +``` + +### Anti-Pattern 2: Global Query Filters in Middleware + +**What people do:** +```python +# Middleware that silently filters all queries by user_id +@app.middleware("http") +async def tenant_filter_middleware(request, call_next): + # Inject user_id filter into all SQLAlchemy queries + # (Complex session hook magic) +``` + +**Why it's wrong:** +- Hidden behavior—hard to debug ("why is this query returning nothing?") +- Breaks when you need cross-tenant operations (e.g., admin views) +- SQLAlchemy session state leaks between requests +- Fails silently if middleware doesn't run (security vulnerability) + +**Do this instead:** +```python +# Explicit filtering in route handlers +@router.get("/api/projects") +def list_projects(current_user: UserContext = Depends(get_current_user), db: Session = Depends(get_db)): + return db.query(Project).filter( + (Project.owner_id == current_user.user_id) | + (Project.members.any(ProjectMember.user_id == current_user.user_id)) + ).all() +``` + +### Anti-Pattern 3: Storing Refresh Tokens in localStorage + +**What people do:** +```typescript +// Frontend stores refresh token in localStorage +localStorage.setItem('refresh_token', refreshToken); // ❌ VULNERABLE TO XSS +``` + +**Why it's wrong:** +- XSS attacks can read localStorage and steal refresh tokens +- Refresh tokens are long-lived (30 days)—high-value targets + +**Do this instead:** +```typescript +// Store refresh token in httpOnly cookie (set by backend) +// Backend response: +Set-Cookie: refresh_token=; HttpOnly; Secure; SameSite=Strict + +// OR store in secure storage with encryption (mobile apps) +// Access tokens in memory only (SessionStorage acceptable) +sessionStorage.setItem('access_token', accessToken); // ✅ Cleared on tab close +``` + +### Anti-Pattern 4: No Token Expiration Validation + +**What people do:** +```python +def verify_token(token: str): + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + return payload["sub"] # ❌ Doesn't check if user still exists/active +``` + +**Why it's wrong:** +- Deleted users can still authenticate (token valid until expiration) +- Deactivated accounts remain accessible +- No way to revoke access immediately + +**Do this instead:** +```python +def get_current_user(credentials = Depends(security), db: Session = Depends(get_db)): + payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM]) + user_id = payload["sub"] + + user = db.query(User).filter(User.id == user_id, User.is_active == True).first() # ✅ Verify user active + if not user: + raise HTTPException(status_code=401, detail="User not found or inactive") + + return UserContext(user_id=user.id, email=user.email, is_admin=user.is_admin) +``` + +### Anti-Pattern 5: CASCADE DELETE Without Considering Foreign Keys + +**What people do:** +```python +# Delete user without considering owned projects +db.delete(user) # ❌ Fails if projects reference this user +db.commit() +``` + +**Why it's wrong:** +- Foreign key constraint violations +- Data loss if CASCADE propagates unexpectedly +- No clear ownership transfer + +**Do this instead:** +```python +# Before deleting user, reassign or delete owned resources +def delete_user(user_id: int, db: Session): + # Transfer project ownership to another admin + admin = db.query(User).filter(User.is_admin == True, User.id != user_id).first() + if not admin: + raise HTTPException(status_code=400, detail="Cannot delete last admin") + + db.execute( + update(Project) + .where(Project.owner_id == user_id) + .values(owner_id=admin.id) + ) + + # Now safe to delete user (SET NULL handles created_by/updated_by) + user = db.query(User).filter(User.id == user_id).first() + db.delete(user) + db.commit() +``` + +## Scaling Considerations + +### Single-Writer Limitation (SQLite) + +**Problem:** SQLite uses a single write lock. With many concurrent users editing requirements, write contention increases. + +**Mitigation:** +- Enable WAL mode: `PRAGMA journal_mode=WAL;` (allows concurrent reads during writes) +- Use connection pooling with conservative pool size (avoid lock queue buildup) +- Consider read replicas for queries (projects, stats dashboards) +- At ~50 concurrent writers, plan migration to PostgreSQL + +**When to migrate to PostgreSQL:** +- **User count:** >100 active users (>50 concurrent writers) +- **Data size:** >10GB database +- **Features needed:** Full-text search, advanced RBAC (Row Level Security), JSON columns +- **Cost:** PostgreSQL adds operational complexity—delay until necessary + +### Token Cleanup (Garbage Collection) + +**Problem:** Expired refresh tokens and invite tokens accumulate in database. + +**Solution:** +```python +# Scheduled task (run daily via cron or Celery) +def cleanup_expired_tokens(db: Session): + now = datetime.now(timezone.utc) + + # Delete expired refresh tokens + db.query(RefreshToken).filter( + RefreshToken.expires_at < now + ).delete() + + # Delete used or expired invites older than 30 days + cutoff = now - timedelta(days=30) + db.query(InviteToken).filter( + (InviteToken.used_at < cutoff) | (InviteToken.expires_at < now) + ).delete() + + db.commit() +``` + +### Query Performance (Indexing) + +**Critical indexes added:** +- `users.email` (unique, login queries) +- `projects.owner_id` (filter by owner) +- `project_members(project_id, user_id)` (composite PK, bidirectional lookups) +- `requirement_nodes.project_id` (filter requirements by project) +- `refresh_tokens.token_hash` (lookup during refresh) +- `invite_tokens.email` (check existing invites) + +**Monitor:** Use `EXPLAIN QUERY PLAN` to verify index usage. + +## Security Considerations + +### Password Storage + +- **Hash algorithm:** bcrypt with cost factor 12 (via passlib) +- **Never log passwords** (sanitize logs, error messages) +- **Enforce minimum strength:** 8 characters, mixed case, numbers (frontend + backend validation) + +### Token Security + +- **Access tokens:** Short-lived (15min), stored in memory (SessionStorage acceptable) +- **Refresh tokens:** Long-lived (30 days), httpOnly cookies or encrypted storage +- **Token hashing:** Store only hashed refresh tokens in DB (SHA-256) +- **Secret rotation:** Support multiple SECRET_KEYs for gradual rotation + +### CSRF Protection + +- **Stateless JWTs:** No CSRF risk (no cookies for auth) +- **If using refresh token cookies:** Require SameSite=Strict or CSRF token + +### Rate Limiting + +Add rate limiting to auth endpoints: +```python +# Using slowapi (FastAPI rate limiting) +from slowapi import Limiter +from slowapi.util import get_remote_address + +limiter = Limiter(key_func=get_remote_address) + +@router.post("/auth/login") +@limiter.limit("5/minute") # 5 login attempts per minute per IP +def login(request: Request, ...): + pass +``` + +## Frontend Integration Points + +### Angular Changes Required + +1. **Auth Service:** + - Update login() to send email + password + - Store both access_token and refresh_token + - Implement token refresh logic in HTTP interceptor + - Add getCurrentUser() to fetch user profile + +2. **HTTP Interceptor:** + - Inject access_token in Authorization header + - Catch 401 errors, attempt refresh, retry request + - If refresh fails, redirect to login + +3. **Route Guards:** + - Update AuthGuard to check token validity + - Add RoleGuard for permission-based route protection + - Fetch user profile on app initialization + +4. **UI Components:** + - Add user profile menu (email, logout button) + - Show project role badges (owner/editor/viewer) + - Add project sharing dialog (invite members) + - Show editor attribution in history panel ("Edited by Alice") + +## Sources + +**Multi-Tenancy Patterns:** +- [Introduction to Multi-Tenant Design with FastAPI](https://blog.greeden.me/en/2026/03/10/introduction-to-multi-tenant-design-with-fastapi-practical-patterns-for-tenant-isolation-authorization-database-strategy-and-audit-logs/) (2026 guide, HIGH confidence) +- [GitHub Discussion: How to implement Multi tenancy in FastAPI](https://github.com/fastapi/fastapi/discussions/6056) +- [MergeBoard: Multitenancy with FastAPI, SQLAlchemy and PostgreSQL](https://mergeboard.com/blog/6-multitenancy-fastapi-sqlalchemy-postgresql/) + +**Authentication & RBAC:** +- [Building a Modern User Permission Management System with FastAPI, SQLAlchemy 2.0](https://dev.to/mochafreddo/building-a-modern-user-permission-management-system-with-fastapi-sqlalchemy-and-mariadb-5fp1) (HIGH confidence) +- [FastAPI RBAC - Full Implementation Tutorial](https://www.permit.io/blog/fastapi-rbac-full-implementation-tutorial) +- [JWT in FastAPI, the Secure Way (Refresh Tokens Explained)](https://medium.com/@jagan_reddy/jwt-in-fastapi-the-secure-way-refresh-tokens-explained-f7d2d17b1d17) (Jan 2026) + +**Email Verification & Invites:** +- [User Account Verification Via Email - FastAPI Beyond CRUD](https://dev.to/jod35/user-account-verification-via-email-fastapi-beyond-crud-part-18-15ib) +- [Top 5 authentication solutions for secure FastAPI apps in 2026](https://workos.com/blog/top-authentication-solutions-fastapi-2026) + +**Dependency Injection:** +- [FastAPI Auth with Dependency Injection](https://www.propelauth.com/post/fastapi-auth-with-dependency-injection) +- [How to Use Dependency Injection in FastAPI](https://oneuptime.com/blog/post/2026-02-02-fastapi-dependency-injection/view) (2026) +- [Dependency Injection in FastAPI: 2026 Playbook](https://thelinuxcode.com/dependency-injection-in-fastapi-2026-playbook-for-modular-testable-apis/) + +**SQLite Multi-Tenancy:** +- [Give each of your users their own SQLite database](https://turso.tech/blog/give-each-of-your-users-their-own-sqlite-database) (Database-per-tenant approach) +- [Multi-tenancy - High Performance SQLite](https://highperformancesqlite.com/watch/multi-tenancy) + +**Database Migrations:** +- [How to do the migration in FastAPI](https://mdhvkothari.medium.com/how-to-do-the-migration-in-fastapi-5c53d3880f12) +- [FastAPI with Async SQLAlchemy, SQLModel, and Alembic](https://testdriven.io/blog/fastapi-sqlmodel/) + +--- + +*Architecture research for: req-planner v2.0 Multi-Tenancy & User Accounts* +*Researched: 2026-03-18* +*Confidence: HIGH (Context7 unavailable, but official FastAPI docs + SQLAlchemy 2.0 docs + recent 2026 tutorials provide authoritative patterns)* diff --git a/.planning/research/FEATURES.md b/.planning/research/FEATURES.md new file mode 100644 index 0000000..a137179 --- /dev/null +++ b/.planning/research/FEATURES.md @@ -0,0 +1,492 @@ +# Feature Research + +**Domain:** Multi-tenancy, user accounts, and role-based access control +**Researched:** 2026-03-18 +**Confidence:** HIGH + +## Feature Landscape + +### Table Stakes (Users Expect These) + +Features users assume exist. Missing these = product feels incomplete. + +| Feature | Why Expected | Complexity | Notes | +|---------|--------------|------------|-------| +| Individual user accounts | Standard expectation in any multi-user app; shared passwords are deprecated UX | MEDIUM | Email/password auth, JWT tokens (already have JWT infrastructure) | +| Per-user project ownership | Users expect "my projects" vs "shared with me" distinction | MEDIUM | Requires user_id column on projects table, workspace isolation | +| Invite-only registration | Standard pattern for self-hosted tools; prevents open registration abuse | LOW | Invite codes/tokens in database, validation before signup | +| Basic role hierarchy (viewer/editor/admin) | Industry standard in collaborative tools (GitHub, Notion, Figma all use 3-tier) | MEDIUM | Role enum on project_user junction table, permission checks | +| Edit attribution in history | Users expect to see "who changed what" in version history | LOW | Already have history table; add user_id foreign key | +| Project sharing UI | Ability to add collaborators and set their role | MEDIUM | UI for managing project members, permission checks | +| Session management | Users expect to see active sessions, logout from devices | LOW | JWT refresh tokens, token revocation list | +| Password reset flow | Expected security hygiene for email/password auth | MEDIUM | Email service integration, reset tokens, secure workflows | + +### Differentiators (Competitive Advantage) + +Features that set the product apart. Not required, but valuable. + +| Feature | Value Proposition | Complexity | Notes | +|---------|-------------------|------------|-------| +| Granular audit trail with rollback | Beyond basic attribution—show full change context and allow per-edit rollback | MEDIUM | Leverage existing history system; add user context to each snapshot | +| Workspace-level dashboards | Per-user analytics showing activity across all their projects | LOW | Aggregate existing project stats by user | +| Inherited permissions on requirement hierarchy | Child requirements inherit parent permissions; reduces management overhead | HIGH | Complex to implement correctly; easy to introduce leaks | +| Anonymous viewer links | Share read-only project views without requiring account | MEDIUM | Signed tokens with expiry, no-auth view endpoints | +| Activity feeds | "What changed since I last logged in" per project | MEDIUM | Requires tracking last_seen timestamps and filtering history | +| Workspace templates | Users can save project configurations as templates for reuse | LOW | Export/import existing project structure | +| Fine-grained permission customization | Beyond viewer/editor/admin—custom permission sets | HIGH | Significantly increases complexity; likely over-engineering for v2 | + +### Anti-Features (Commonly Requested, Often Problematic) + +Features that seem good but create problems. + +| Feature | Why Requested | Why Problematic | Alternative | +|---------|---------------|-----------------|-------------| +| Real-time collaborative editing | "Like Google Docs" — users assume all modern tools have it | Massive architectural lift; requires WebSockets, operational transforms, conflict resolution; SQLite poor fit | Optimistic locking with "edited by X" warnings; auto-refresh on save | +| Organization/team hierarchies | Enterprises expect nested orgs (company → department → team) | Dramatically increases permission model complexity; complicates billing/limits | Single workspace per user; flat project sharing model | +| Social login (OAuth) | Convenient for users, reduces friction | Adds external dependencies; complicates self-hosted deployments; privacy concerns | Email/password sufficient for self-hosted tool; invite-only provides security | +| Fine-grained field-level permissions | "Editor can change description but not acceptance criteria" | Explosion of permission checks; confusing UX; high maintenance burden | Role-based with clear responsibilities; don't split access within entities | +| Public project discovery | "Browse all public projects like GitHub" | Privacy concerns for self-hosted tool; invites spam/abuse | Explicit sharing only; anonymous links for specific cases | +| User-to-user direct messaging | "Chat with collaborators in-app" | Scope creep; users already have Slack/email/Discord | Keep tool focused; collaboration happens in comments/edits | + +## Feature Dependencies + +``` +Individual User Accounts (foundational) + ├──requires──> User Authentication Flow + │ ├──requires──> Login/Logout + │ ├──requires──> Password Reset + │ └──requires──> Session Management + │ + ├──requires──> Per-User Project Ownership + │ ├──requires──> Workspace Isolation (tenant_id pattern) + │ └──requires──> "My Projects" vs "Shared With Me" views + │ + ├──requires──> Edit Attribution + │ ├──enhances──> Existing History System + │ └──requires──> User Foreign Keys on History Table + │ + └──requires──> Invite-Only Registration + ├──requires──> Invite Token Generation + ├──requires──> Invite Validation + └──requires──> Admin Controls + +Role-Based Access Control + ├──requires──> Individual User Accounts (above) + ├──requires──> Project-User Junction Table + │ └──stores──> Role (viewer/editor/admin/owner) + │ + ├──requires──> Permission Enforcement Layer + │ ├──frontend──> Route guards, UI element visibility + │ └──backend──> Endpoint authorization checks + │ + └──enhances──> Project Sharing UI + └──requires──> Member Management Interface + +Project Sharing UI + ├──requires──> Role-Based Access Control (above) + └──requires──> Invite Notification System + +Audit Trail with User Context + ├──requires──> Edit Attribution (above) + └──enhances──> Existing History Visualization +``` + +### Dependency Notes + +- **Individual User Accounts is foundational**: All other features depend on this being implemented first +- **Workspace Isolation must be implemented correctly from the start**: Retrofitting tenant isolation is high-risk for data leaks +- **Edit Attribution enhances existing history**: Already have requirement_history table; adding user_id is low-risk +- **RBAC requires careful sequencing**: Must implement permission checks in backend before exposing sharing UI +- **Invite-only registration can be parallel to RBAC**: These features are independent after user accounts exist + +## MVP Definition + +### Launch With (v2.0) + +Minimum viable multi-tenancy — what's needed to validate the concept. + +- [x] Individual user accounts with email/password authentication — Essential foundation +- [x] Invite-only registration with admin-generated invite codes — Security requirement for self-hosted +- [x] Per-user project ownership with workspace isolation — Core multi-tenancy feature +- [x] Three-tier RBAC (owner/editor/viewer) on projects — Table stakes for collaboration +- [x] Edit attribution in history (user_id on edits) — Minimal audit trail +- [x] Project sharing UI with role assignment — Required to make RBAC usable +- [x] Basic permission enforcement (backend + frontend) — Security requirement +- [x] Password reset flow — Expected security hygiene + +### Add After Validation (v2.x) + +Features to add once core multi-tenancy is working. + +- [ ] Activity feeds per project — Add once users report "losing track of changes" +- [ ] Workspace-level dashboard — Add when users manage 5+ projects +- [ ] Anonymous viewer links — Add when users request "share with client" workflows +- [ ] Granular audit trail with per-edit rollback — Add when attribution proves insufficient +- [ ] Session management UI — Add if users report security concerns with active sessions +- [ ] Workspace templates — Add when users request "clone project structure" + +### Future Consideration (v3+) + +Features to defer until product-market fit is established. + +- [ ] Fine-grained permission customization — Defer until 3-tier RBAC proves insufficient +- [ ] Inherited permissions on hierarchy — Complex; defer until users report permission management overhead +- [ ] Organization/team hierarchies — Defer until serving enterprise customers +- [ ] OAuth/SSO providers — Defer until serving organizations requiring SSO +- [ ] Real-time collaborative editing — Major architectural change; defer until core collaboration proves valuable + +## Feature Prioritization Matrix + +| Feature | User Value | Implementation Cost | Priority | +|---------|------------|---------------------|----------| +| Individual user accounts | HIGH | MEDIUM | P1 | +| Invite-only registration | HIGH | LOW | P1 | +| Per-user project ownership | HIGH | MEDIUM | P1 | +| Three-tier RBAC (owner/editor/viewer) | HIGH | MEDIUM | P1 | +| Edit attribution | HIGH | LOW | P1 | +| Project sharing UI | HIGH | MEDIUM | P1 | +| Permission enforcement layer | HIGH | HIGH | P1 | +| Password reset flow | MEDIUM | MEDIUM | P1 | +| Activity feeds | MEDIUM | MEDIUM | P2 | +| Workspace dashboard | MEDIUM | LOW | P2 | +| Anonymous viewer links | MEDIUM | MEDIUM | P2 | +| Granular audit trail | LOW | MEDIUM | P2 | +| Session management UI | LOW | LOW | P2 | +| Workspace templates | LOW | LOW | P2 | +| Fine-grained permissions | LOW | HIGH | P3 | +| Inherited permissions | MEDIUM | HIGH | P3 | +| Organization hierarchies | LOW | HIGH | P3 | +| OAuth/SSO | LOW | HIGH | P3 | +| Real-time collaboration | HIGH | VERY HIGH | P3 | + +**Priority key:** +- P1: Must have for v2.0 launch — core multi-tenancy +- P2: Should have for v2.x — enhances collaboration +- P3: Nice to have for v3+ — future consideration + +## Expected Behavior and UX Patterns + +### Invite-Only Registration + +**Standard Flow:** +1. Admin generates invite code/link from admin panel +2. Invite stored in database with: code (UUID), email (optional), created_by, expires_at, used (boolean) +3. Registration page requires valid invite code before showing signup form +4. After successful signup, invite marked as used (single-use) or incremented (multi-use) +5. Expired/invalid codes show clear error messages + +**UX Patterns (2026):** +- **Invite links preferred over codes**: Users click link → auto-fills invite code → shows signup form +- **Email pre-fill**: If invite includes email, pre-populate signup form (can't be changed) +- **Context in email**: Invitation email shows who invited them and to which workspace +- **Clear expiry**: Show "This invite expires in X days" prominently +- **Admin controls**: Dashboard showing pending/used/expired invites + +**Sources:** +- [User Onboarding Strategies in a B2B SaaS Application](https://auth0.com/blog/user-onboarding-strategies-b2b-saas/) +- [How to onboard invited users and fast-track user engagement](https://www.appcues.com/blog/user-onboarding-strategies-invited-users) + +### Per-User Project Ownership + +**Standard Patterns:** +1. **Shared database with tenant_id**: All users share same tables; user_id column differentiates ownership +2. **Workspace views**: "My Projects" (owned), "Shared With Me" (collaborator), optionally "All Projects" (admin) +3. **Default visibility**: Projects private by default; owner explicitly shares +4. **Transfer ownership**: Admin capability to reassign project owner +5. **Orphaned projects**: Handle deleted user accounts (reassign or archive) + +**Data Model:** +```sql +projects: + - owner_id (FK to users) + - created_by (FK to users) + - updated_by (FK to users) + +project_users (junction): + - project_id (FK to projects) + - user_id (FK to users) + - role (enum: owner, editor, viewer) + - added_by (FK to users) + - added_at (timestamp) +``` + +**Critical Security Pattern:** +- Every query MUST include `WHERE user_id = ? OR EXISTS (SELECT 1 FROM project_users WHERE project_id = ? AND user_id = ?)` +- Enforce at ORM level or middleware layer, not per-endpoint (reduces chance of missing checks) + +**Sources:** +- [The developer's guide to SaaS multi-tenant architecture — WorkOS](https://workos.com/blog/developers-guide-saas-multi-tenant-architecture) +- [Tenant Data Isolation: 5 Patterns That Actually Work](https://propelius.tech/blogs/tenant-data-isolation-patterns-and-anti-patterns/) + +### Role-Based Access Control (Viewer/Editor/Admin) + +**Industry Standard Three-Tier Model:** + +| Role | Read | Create/Edit | Delete | Manage Members | Transfer Ownership | +|------|------|-------------|--------|----------------|--------------------| +| **Owner** | ✓ | ✓ | ✓ | ✓ | ✓ | +| **Editor** | ✓ | ✓ | ✓ (own edits) | ✗ | ✗ | +| **Viewer** | ✓ | ✗ | ✗ | ✗ | ✗ | + +**Implementation Pattern:** +```python +# Backend: Permission decorator +@require_project_permission(Permission.EDIT) +def update_requirement(project_id, requirement_id, data): + # Only called if user has editor+ role + pass + +# Frontend: Structural directives + +``` + +**UX Expectations:** +- **Graceful degradation**: Viewers see read-only interface, not error messages +- **Clear role badges**: Show user's role in project header ("You are an Editor") +- **Contextual actions**: Only show actions user can perform (hide "Delete" for viewers) +- **Permission feedback**: If user somehow triggers unauthorized action, show clear explanation + +**Common Mistakes to Avoid:** +- **Authorization only on frontend**: Always enforce on backend; frontend is convenience only +- **Inconsistent enforcement**: Missing permission checks on some endpoints creates security holes +- **Role explosion**: Don't create custom roles too early; three tiers sufficient for most cases +- **Cache-based authorization**: Store role in user-controllable location (localStorage) for UI only; always verify on backend + +**Sources:** +- [How to Build a Role-Based Access Control Layer](https://www.osohq.com/learn/rbac-role-based-access-control) +- [Role-Based Access Control Best Practices for 2026](https://www.techprescient.com/blogs/role-based-access-control-best-practices/) +- [Guide to sharing and permissions – Figma Learn](https://help.figma.com/hc/en-us/articles/1500007609322-Guide-to-sharing-and-permissions) + +### Edit Attribution + +**Expected Behavior:** +1. Every edit records: user_id, timestamp, before/after state +2. History view shows: "John Doe edited requirement title at 2:34 PM" +3. Hover/click shows full context: what changed, by whom, when +4. Filter history: "Show only my edits" or "Show edits by John" + +**Google Docs Pattern (Gold Standard):** +- **Show Editors feature**: Right-click text → "Show Editors" → see who edited that specific range +- **Named versions**: Users can create named checkpoints ("Before client review") +- **Continuous recording**: Every change captured automatically (no manual "save version") +- **Real-time attribution**: See colored cursors/highlights showing who's editing what + +**Adapted for Non-Real-Time:** +- Display "Last edited by X at Y" on each requirement card +- History panel shows chronological list with user avatars +- Diff view highlights changes with attribution ("Added by John", "Removed by Jane") +- Filter/search history by user + +**Implementation Notes:** +- Already have requirement_history table +- Add user_id foreign key to requirement_history +- Add created_by, updated_by to requirement_nodes +- Frontend displays user info from history records + +**Sources:** +- [Google Workspace Updates: "Show Editors" provides more context on changes made in Google Docs](https://workspaceupdates.googleblog.com/2021/05/view-more-context-on-google-docs-edits-with-show-editors.html) +- [How to See Version History in Google Docs](https://www.geeksforgeeks.org/websites-apps/how-to-see-version-history-in-google-docs/) + +### Project Sharing UI + +**Standard Patterns:** +1. **Share button**: Prominent in project header +2. **Member list**: Shows current collaborators with roles +3. **Add member flow**: + - Input: Email or username + - Select role: Owner/Editor/Viewer (with tooltips explaining each) + - Optional: Add message to invitation + - Send: Generates notification and adds to project +4. **Role modification**: Click role dropdown → change role → confirm +5. **Remove member**: Click X or "Remove" → confirm dialog → remove access + +**GitHub Pattern:** +- Settings → Collaborators → "Add people" → username → role → "Add" +- Shows pending invitations separately from active members +- Email notification when invited + +**Notion Pattern:** +- "Share" button → modal with member list +- Inline role editing (click role → dropdown) +- "Copy link" for anonymous sharing +- Shows "X has access via Team Y" (inherited permissions) + +**Figma Pattern:** +- Share button → modal showing team/project/file hierarchy +- Link sharing with permission level attached to link +- "Anyone with link can view/edit" + +**Recommended Approach:** +- Hybrid: GitHub-style explicit invitations + Notion-style inline role editing +- No link-based sharing in v2.0 (add in v2.x as anonymous viewer links) +- Clear visual distinction between owner (1 per project) and collaborators + +**Sources:** +- [Guide to sharing and permissions – Figma Learn](https://help.figma.com/hc/en-us/articles/1500007609322-Guide-to-sharing-and-permissions) +- [How to Add Collaborators in GitHub](https://www.rapidnative.com/blogs/how-to-add-collaborators-in-github) + +## Complexity on Existing Features + +### Impact on Existing Edit History + +**Current State:** +- requirement_history table stores snapshots (before/after JSON) +- No user attribution — assumes single user + +**Required Changes:** +- Add user_id column (FK to users) +- Migrate existing records: set user_id to admin or mark as "system" +- Update create_history_record() to include current_user_id +- Update history display to show user info + +**Complexity: LOW** — Additive change, doesn't break existing functionality + +### Impact on Existing Graph Visualization + +**Current State:** +- Graph shows all requirements in project +- No permission-based filtering + +**Required Changes:** +- Filter requirements based on project access (user is owner/editor/viewer) +- Show read-only indicators for viewers +- Disable edit actions (drag, delete, create) for viewers +- Show permission indicators in detail panel + +**Complexity: MEDIUM** — Requires permission checks in graph component, but no architectural changes + +### Impact on Existing CRUD Operations + +**Current State:** +- No user context on create/update/delete +- No ownership checks + +**Required Changes:** +- Add user_id to CREATE operations (set owner) +- Add permission checks to UPDATE operations (is editor+?) +- Add permission checks to DELETE operations (is owner or editor with own edits?) +- Add created_by, updated_by columns to requirement_nodes +- Migrate existing records: set created_by to admin or null + +**Complexity: MEDIUM** — Every endpoint needs permission middleware, but follows consistent pattern + +### Impact on Existing Dashboard + +**Current State:** +- Shows all projects globally +- Statistics aggregate all data + +**Required Changes:** +- Filter projects by ownership + shared access +- Separate "My Projects" and "Shared With Me" views +- Per-project statistics respect permissions +- Add "Collaborators" count to project cards + +**Complexity: LOW** — Mostly UI changes, backend already has project filtering + +### Impact on Database Schema + +**New Tables Required:** +```sql +users ( + id INTEGER PRIMARY KEY, + email TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + name TEXT, + created_at TIMESTAMP, + last_login TIMESTAMP +) + +invite_codes ( + id INTEGER PRIMARY KEY, + code TEXT UNIQUE NOT NULL, + email TEXT, -- optional: pre-assign to email + created_by INTEGER REFERENCES users(id), + expires_at TIMESTAMP, + used BOOLEAN DEFAULT FALSE, + used_by INTEGER REFERENCES users(id), + used_at TIMESTAMP +) + +project_users ( + id INTEGER PRIMARY KEY, + project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + role TEXT CHECK(role IN ('owner', 'editor', 'viewer')), + added_by INTEGER REFERENCES users(id), + added_at TIMESTAMP, + UNIQUE(project_id, user_id) +) +``` + +**Modified Tables:** +```sql +projects: + + owner_id INTEGER REFERENCES users(id) + + created_by INTEGER REFERENCES users(id) + + updated_by INTEGER REFERENCES users(id) + +requirement_nodes: + + created_by INTEGER REFERENCES users(id) + + updated_by INTEGER REFERENCES users(id) + +requirement_history: + + user_id INTEGER REFERENCES users(id) + +graph_layouts: + + user_id INTEGER REFERENCES users(id) -- per-user layouts +``` + +**Migration Strategy:** +1. Create users table first +2. Create admin user (id=1) +3. Add foreign key columns to existing tables (nullable initially) +4. Set all existing records to admin user (id=1) +5. Make columns NOT NULL after backfill +6. Create invite_codes and project_users tables + +**Complexity: MEDIUM** — Careful sequencing required, but no data loss risk if done correctly + +## Sources + +### UX and Design Patterns +- [Best Sign Up Flows (2026): 15 UX Examples That Convert](https://www.eleken.co/blog-posts/sign-up-flow) +- [Login & Signup UX: The 2025 Guide to Best Practices](https://www.authgear.com/post/login-signup-ux-guide) +- [Designing an intuitive user flow for inviting teammates](https://pageflows.com/resources/invite-teammates-user-flow/) + +### RBAC and Authorization +- [How to Build a Role-Based Access Control Layer](https://www.osohq.com/learn/rbac-role-based-access-control) +- [Role-Based Access Control Best Practices for 2026](https://www.techprescient.com/blogs/role-based-access-control-best-practices/) +- [Role-based Access Control: Five Common Authorization Patterns](https://thenewstack.io/role-based-access-control-five-common-authorization-patterns/) +- [Best Practices for Multi-Tenant Authorization](https://www.permit.io/blog/best-practices-for-multi-tenant-authorization) + +### Multi-Tenancy Architecture +- [The developer's guide to SaaS multi-tenant architecture — WorkOS](https://workos.com/blog/developers-guide-saas-multi-tenant-architecture) +- [Multi-Tenant SaaS Architecture: Complete Guide](https://codeboxr.com/multi-tenant-saas-architecture-complete-guide-models-design-patterns-and-scaling-strategy/) +- [Data Isolation in Multi-Tenant SaaS](https://redis.io/blog/data-isolation-multi-tenant-saas/) +- [Tenant Data Isolation: 5 Patterns That Actually Work](https://propelius.tech/blogs/tenant-data-isolation-patterns-and-anti-patterns/) + +### Security and Data Isolation +- [Preventing Cross-Tenant Data Leakage in Multi-Tenant SaaS Systems](https://agnitestudio.com/blog/preventing-cross-tenant-leakage/) +- [Multi-Tenant Leakage: When "Row-Level Security" Fails in SaaS](https://medium.com/@instatunnel/multi-tenant-leakage-when-row-level-security-fails-in-saas-da25f40c788c) +- [Tenant Isolation in Multi-Tenant Systems](https://securityboulevard.com/2025/12/tenant-isolation-in-multi-tenant-systems-architecture-identity-and-security/) +- [Access control vulnerabilities and privilege escalation](https://portswigger.net/web-security/access-control) + +### Audit Trails and Attribution +- [What Is an Audit Trail? Definition and Best Practices](https://trullion.com/blog/audit-trail-guide/) +- [10 Essential Audit Trail Best Practices for 2026](https://signal.opshub.me/audit-trail-best-practices/) +- [Google Workspace Updates: "Show Editors" in Google Docs](https://workspaceupdates.googleblog.com/2021/05/view-more-context-on-google-docs-edits-with-show-editors.html) +- [How to See Version History in Google Docs](https://www.geeksforgeeks.org/websites-apps/how-to-see-version-history-in-google-docs/) + +### User Onboarding +- [User Onboarding Strategies in a B2B SaaS Application](https://auth0.com/blog/user-onboarding-strategies-b2b-saas/) +- [How to onboard invited users and fast-track user engagement](https://www.appcues.com/blog/user-onboarding-strategies-invited-users) +- [SaaS Onboarding Flows That Actually Convert in 2026](https://www.saasui.design/blog/saas-onboarding-flows-that-actually-convert-2026) + +### Collaboration Patterns +- [Guide to sharing and permissions – Figma Learn](https://help.figma.com/hc/en-us/articles/1500007609322-Guide-to-sharing-and-permissions) +- [How to Add Collaborators in GitHub](https://www.rapidnative.com/blogs/how-to-add-collaborators-in-github) + +--- +*Feature research for: req-planner v2.0 Multi-Tenancy & User Accounts* +*Researched: 2026-03-18* diff --git a/.planning/research/PITFALLS.md b/.planning/research/PITFALLS.md new file mode 100644 index 0000000..7771f4d --- /dev/null +++ b/.planning/research/PITFALLS.md @@ -0,0 +1,443 @@ +# Pitfalls Research + +**Domain:** Adding multi-tenancy, user accounts, invite systems, and RBAC to existing single-user FastAPI+SQLite app +**Researched:** 2026-03-18 +**Confidence:** MEDIUM-HIGH + +## Critical Pitfalls + +### Pitfall 1: Forgetting tenant_id Filters in Queries + +**What goes wrong:** +Endpoints return all data across all users/tenants instead of just the authenticated user's data. A single missing `.filter(owner_id == current_user.id)` exposes another user's projects, requirements, or edit history. + +**Why it happens:** +When migrating from single-user to multi-user, the existing codebase has no tenant scoping. Every SQLAlchemy query was written assuming global access. Developers update auth but forget to audit and update every single query in the application. + +**How to avoid:** +- Create base query functions that automatically inject tenant filters +- Use SQLAlchemy events or custom query classes to enforce tenant scoping +- Never trust frontend filtering — always enforce at backend/database level +- Code review checklist: "Does this query filter by user/tenant?" +- Write integration tests that attempt cross-tenant access + +**Warning signs:** +- "I can see another user's data in the API response" +- Tests pass in isolation but fail when multiple users exist +- Postman/curl requests with different tokens return overlapping data + +**Phase to address:** +Phase 1 (Database Schema & User Model) — Establish the tenant scoping pattern from the start, create helper functions/base classes that enforce it. + +--- + +### Pitfall 2: SQLite Foreign Key Constraint Failures During Migration + +**What goes wrong:** +Adding `user_id` foreign keys to existing tables (projects, requirements, history, layouts) causes migration failures or silently drops data. SQLite's ALTER TABLE limitations force table recreation, and foreign key constraints can cascade-delete dependent data unexpectedly. + +**Why it happens:** +SQLite doesn't support true ALTER TABLE — it creates a new table, copies data, drops the old one. If foreign keys are enabled (`PRAGMA foreign_keys = ON`) during this process, cascade relationships can destroy data. The pragma can't be toggled mid-transaction, and migration tools run in transactions. + +**How to avoid:** +- Use Alembic's batch mode for SQLite schema changes +- For simple column additions, use `add_column` + `add_index` without foreign key constraints +- Enforce user_id constraints at application level for SQLite +- Consider backfilling data BEFORE adding constraints +- Test migrations on production data copy, not empty database +- Have rollback plan and backup before running migration + +**Warning signs:** +- Migration succeeds but table is empty +- "FOREIGN KEY constraint failed" errors +- Dependent tables lose records after migration +- `__old_tablename` tables remain in schema + +**Phase to address:** +Phase 1 (Database Schema & User Model) — Design migration strategy carefully, test thoroughly with realistic data volumes. + +--- + +### Pitfall 3: Existing JWT Tokens Become Invalid + +**What goes wrong:** +After deploying user account changes, the single shared password JWT tokens become invalid. All active users are logged out. Worse, the JWT payload structure changes (adding user_id, roles) but the backend doesn't handle old token format, causing 401s or crashes. + +**Why it happens:** +Current JWTs encode the shared password authentication, not a user identity. After migration, the auth system expects `user_id` in the token payload. Old tokens lack this field. Token verification fails or returns None for current_user. + +**How to avoid:** +- Plan for token format versioning (include `version: 2` in new tokens) +- Handle both old and new token formats during transition period +- Accept that a forced re-login is sometimes necessary — communicate this +- Use a migration window: deploy new auth, give users 24 hours to re-login before breaking old tokens +- Consider token migration endpoint: exchange old token for new token + +**Warning signs:** +- All users logged out immediately after deployment +- "Invalid token" errors spike in logs +- `current_user.id` is None/undefined in dependency injection +- Frontend can't decode JWT payload + +**Phase to address:** +Phase 2 (Authentication & Authorization) — Handle gracefully with versioned tokens or planned re-login window. Document in migration notes. + +--- + +### Pitfall 4: Orphaned Data Without Clear Ownership + +**What goes wrong:** +Existing projects, requirements, and layouts in the database have no `owner_id`. After adding user accounts, who owns this data? Assigning it all to the first admin is wrong. Leaving it NULL breaks foreign key constraints. Making it globally accessible defeats the purpose of multi-tenancy. + +**Why it happens:** +The migration script adds `user_id` columns but doesn't define a strategy for pre-existing data. It's a business logic decision (who should own legacy data?) disguised as a technical problem. + +**How to avoid:** +- Decide ownership strategy BEFORE migration: + - Option A: Assign all to designated "admin" user (create during migration) + - Option B: Create "shared" workspace owned by system user + - Option C: Mark legacy data as owned by each user (copy to all users) + - Option D: Delete legacy data (if acceptable) +- Document the decision and rationale +- Make it reversible if possible (backup tables) +- Allow users to claim/import legacy data post-migration + +**Warning signs:** +- NULL user_id values in production +- Foreign key constraint errors after migration +- Users complain about missing historical data +- Unclear who can edit legacy projects + +**Phase to address:** +Phase 1 (Database Schema & User Model) — Define ownership strategy as part of migration plan, not an afterthought. + +--- + +### Pitfall 5: Role Explosion and Privilege Creep + +**What goes wrong:** +Starting with simple roles (viewer, editor, admin) quickly becomes dozens of overly-specific roles: "ProjectOwnerCanEditGraphsButNotDelete", "EditorForPillarRequirementsOnly". Permission checking logic becomes unmaintainable. Users accumulate permissions as they're granted roles for different projects. + +**Why it happens:** +Creating new roles is easy. Removing or merging roles is hard. Each edge case ("what if user needs read on Project A but write on Project B?") tempts adding a new role instead of using proper RBAC patterns. Users change roles but old permissions aren't revoked. + +**How to avoid:** +- Start with MINIMAL roles: Owner, Editor, Viewer +- Roles are per-project, not global +- Don't create roles for every permission combination +- Implement permission inheritance: Editor includes Viewer permissions +- Use project-level assignments, not feature-level roles +- Audit user roles quarterly, revoke unused permissions +- Track role changes in audit log + +**Warning signs:** +- More roles than users +- Role names longer than 3 words +- Permission checks require nested if/else trees +- Users have 5+ roles assigned +- "I don't know what role to give this user" + +**Phase to address:** +Phase 3 (Project Ownership & Sharing) — Define minimal, composable role set from the start. Resist adding roles. + +--- + +### Pitfall 6: Invite System Security Weaknesses + +**What goes wrong:** +Invite codes are predictable (sequential IDs, simple UUIDs), reusable after first use, never expire, or lack the invite token in the registration endpoint. Attackers enumerate invite codes or reuse leaked ones to create unauthorized accounts. + +**Why it happens:** +Invite system feels like a simple feature: generate token, send email, validate token on signup. The security edge cases (expiration, single-use, brute force protection) get deprioritized as "nice to have." + +**How to avoid:** +- Use cryptographically secure random tokens (secrets.token_urlsafe(32)) +- Make invites single-use: mark as consumed on successful registration +- Set expiration (7-30 days) and enforce it +- Rate-limit invite generation and redemption endpoints +- Don't expose invite codes in URLs if possible (use POST body) +- Log invite creation/redemption for audit trail +- Allow invite revocation before use + +**Warning signs:** +- Invite codes are short (< 20 chars) or sequential +- Same invite works multiple times +- Invites never expire +- No rate limiting on /register endpoint +- Invite validation happens client-side only + +**Phase to address:** +Phase 2 (Authentication & Authorization) — Build invite system with security as primary concern, not afterthought. + +--- + +### Pitfall 7: Frontend State Management Assumes Single User + +**What goes wrong:** +Angular services cache user context globally, localStorage keys don't include user_id, or the app doesn't clear state on logout. User A logs out, User B logs in on the same browser, and sees User A's cached project data. + +**Why it happens:** +Single-user app stored state globally (localStorage, service singletons) with no concept of user identity. After adding multi-user, the frontend team updates login/logout but forgets cached state. + +**How to avoid:** +- Clear localStorage/sessionStorage on logout +- Namespace storage keys by user_id: `user_${userId}_graph_state` +- Use request-scoped state, not global singletons where possible +- Angular Guards should verify token validity on route change +- Test user switching: logout → login as different user → verify no leaked state +- Consider using Angular signals for reactive user-scoped state + +**Warning signs:** +- User sees previous user's data briefly after login +- Editing as User B modifies User A's project +- "My dashboard shows someone else's stats" +- Graph layouts persist across user accounts + +**Phase to address:** +Phase 2 (Authentication & Authorization) — Update frontend state management to be user-aware. + +--- + +### Pitfall 8: Cascading Deletes Destroy Shared Data + +**What goes wrong:** +User A shares a project with User B. User A deletes their account. CASCADE DELETE removes the project, wiping out User B's work and edit history. Or worse, the delete fails with foreign key violations because of shared data. + +**Why it happens:** +Foreign key CASCADE DELETE seems convenient for cleanup. But in multi-tenant systems with sharing, data ownership is not 1:1. A project might have an owner but also collaborators. Deleting the owner shouldn't delete the project. + +**How to avoid:** +- Transfer ownership instead of deleting when user has owned projects +- Use soft deletes (deleted_at timestamp) instead of CASCADE DELETE +- Require explicit "delete all my data" confirmation with warnings +- Archive user data instead of deleting (GDPR compliance) +- For shared projects: transfer to co-owner or mark as archived +- Test deletion scenarios: user with projects, user as collaborator, user as viewer + +**Warning signs:** +- User deletion is instant with no warnings +- "I lost my project when the owner left the team" +- Foreign key constraint errors on user deletion +- No way to recover deleted data + +**Phase to address:** +Phase 3 (Project Ownership & Sharing) — Design deletion/archival strategy that respects data relationships. + +--- + +### Pitfall 9: Missing Edit Attribution and Audit Trail + +**What goes wrong:** +Edit history shows changes but not WHO made them. In a collaborative environment, users can't see "User B edited my requirement" or "Who broke the graph?" The blame/credit information is lost. + +**Why it happens:** +Single-user app had no concept of attribution — every edit was "me." The existing `requirement_history` table might have timestamps but no `edited_by_user_id` field. + +**How to avoid:** +- Add `created_by_user_id` and `modified_by_user_id` to all tables with user-generated content +- Update history/audit tables to capture user identity +- Display "Last edited by [username] at [time]" in UI +- Allow filtering history by user +- Consider including user's display name in history JSON (denormalized) to survive user deletions + +**Warning signs:** +- Edit history shows timestamps but no usernames +- "Who made this change?" has no answer +- Audit log exists but lacks user attribution +- Collaboration features planned but no user tracking + +**Phase to address:** +Phase 1 (Database Schema & User Model) — Add attribution columns from the start. + +--- + +### Pitfall 10: Dependency Injection Fails to Scope Tenant Context + +**What goes wrong:** +FastAPI dependency that extracts `current_user` is request-scoped, but a service or background task uses a module-level database session. The tenant filter isn't applied, causing data leaks. Or, background tasks receive user objects with secrets instead of just user_id. + +**Why it happens:** +Mixing dependency lifespans: per-request (auth), application-lifespan (DB connection pool), and background tasks (out-of-request context). The tenant_id from the JWT is only available in request scope, but business logic runs elsewhere. + +**How to avoid:** +- Use per-request DB sessions, not global singletons +- FastAPI dependencies: extract user, pass user_id to service layer +- Background tasks: pass IDs, not objects; rehydrate via dependencies +- Never store user context in global variables +- Service layer methods take user_id as explicit parameter +- Test with multiple concurrent users to catch scope leaks + +**Warning signs:** +- Background task sends email to wrong user +- "Current user is None" errors in async tasks +- Data visible to wrong user under concurrent load +- DB session reused across requests + +**Phase to address:** +Phase 2 (Authentication & Authorization) — Establish dependency injection patterns that properly scope user context. + +--- + +## Technical Debt Patterns + +Shortcuts that seem reasonable but create long-term problems. + +| Shortcut | Immediate Benefit | Long-term Cost | When Acceptable | +|----------|-------------------|----------------|-----------------| +| Skip invite system, use direct registration | Faster MVP | No growth control, spam signups, unclear user source | Never for v2 — stated requirement | +| Check permissions in frontend only | Less backend code | Trivial to bypass, security theater | Never — always verify backend | +| Use global admin toggle instead of RBAC | Simple boolean check | Can't do granular sharing, rewrite needed for collaboration | Never — RBAC is v2 requirement | +| Store user_id in localStorage instead of JWT | Simpler token structure | Easily spoofed, XSS vulnerability, defeats authentication | Never — security risk | +| Soft-delete users but not their projects | Cleaner user table | Orphaned projects, unclear ownership | Acceptable if projects transferred first | +| SQLite without foreign keys | Easier migration | No referential integrity, manual cleanup, data inconsistency | Acceptable for MVP, fix in v2 | +| Add `user_id` as nullable column | Non-breaking migration | Half the codebase forgets to filter by it | Only during transition, make NOT NULL ASAP | +| Embed role in JWT instead of database | Fewer DB queries | Role changes require re-login, harder to audit | Acceptable if roles rarely change | + +## Integration Gotchas + +Common mistakes when connecting to external services. + +| Integration | Common Mistake | Correct Approach | +|-------------|----------------|------------------| +| Gitea issue integration | Include user's Gitea token in JWT | Store Gitea token per-user in database, encrypt at rest | +| Email (invite system) | Send from user's email address | Use system email (noreply@), track sent_by_user_id separately | +| Frontend SPA | Auth state in component, not service | Centralized auth service with RxJS for reactive state | +| Background tasks | Pass entire user object to task | Pass user_id only, re-fetch in task context | + +## Performance Traps + +Patterns that work at small scale but fail as usage grows. + +| Trap | Symptoms | Prevention | When It Breaks | +|------|----------|------------|----------------| +| N+1 queries loading user for each project | 100s of queries for dashboard | Eager load user with project: `.options(joinedload(Project.owner))` | >50 projects per view | +| Check permissions on every graph node render | Slow graph rendering | Bulk permission check on page load, cache results | >100 nodes | +| No pagination on shared projects list | Slow API calls | Paginate, default limit 50 | User in >20 projects | +| Recalculate full audit history on every edit | Edit lag increases | Incremental history, paginate old history | >1000 history entries | +| JWT validation queries DB for user on every request | DB bottleneck | Cache user in request context after first validation | >10 requests/second | + +## Security Mistakes + +Domain-specific security issues beyond general web security. + +| Mistake | Risk | Prevention | +|---------|------|------------| +| Include user_id in URL path (/projects/user/123/items) | Enumeration attack, user ID leak | Use /projects/me or /projects (filter by JWT user) | +| Expose user emails in project collaborator API | Privacy leak, phishing target list | Return user IDs + display names only | +| Allow project transfer without new owner consent | Unwanted ownership, storage quota bypass | Require new owner to accept transfer | +| Reuse password reset tokens as invite tokens | Token confusion, security model mismatch | Separate token types with different validation | +| No rate limit on invite endpoint | Spam, resource exhaustion | Rate limit: 10 invites/hour per user | +| Allow changing your own role on a project | Privilege escalation | Only project owner can change roles, can't change own | +| DELETE /users/:id accepts any user_id | Account takeover, data destruction | Only allow /users/me, verify JWT user matches | + +## UX Pitfalls + +Common user experience mistakes in this domain. + +| Pitfall | User Impact | Better Approach | +|---------|-------------|-----------------| +| No indication of project owner vs. collaborator | Confusion about edit permissions | Badge/icon showing role, owner prominently displayed | +| Can't tell who made an edit | Blame culture, lost context | "Edited by [name] [time ago]" on every change | +| Force logout on every deployment | Frustration, work loss | 24-hour token expiry, graceful re-auth | +| Invite links expire too quickly (1 day) | Missed invites, support burden | 7-day default, 30-day option | +| No way to leave a shared project | Cluttered project list | "Leave project" button for non-owners | +| Shared projects look same as owned projects | Can't find my projects | Visual distinction, filter by role | +| Delete account is one-click | Accidental deletion, data loss | Require typing account name to confirm | + +## "Looks Done But Isn't" Checklist + +Things that appear complete but are missing critical pieces. + +- [ ] **User registration:** Often missing email verification — verify emails are unique, consider confirmation +- [ ] **Invite system:** Often missing invite revocation — verify admin can cancel pending invites +- [ ] **RBAC implementation:** Often missing inheritance (editor includes viewer) — verify role permissions compose correctly +- [ ] **Multi-user queries:** Often missing tenant filter on JOIN tables — verify cross_pillar_links filtered by project owner +- [ ] **Logout:** Often missing token revocation/blacklist — decide if tokens are valid until expiry or revocable +- [ ] **Project sharing:** Often missing "can they still edit after shared?" test — verify permissions persist correctly +- [ ] **User deletion:** Often missing "what happens to their projects?" flow — verify ownership transfer or archival +- [ ] **Edit history:** Often missing user attribution on old edits — verify migration backfills created_by/modified_by +- [ ] **Role changes:** Often missing "does it take effect immediately?" — verify role updates don't require re-login +- [ ] **Password reset:** Often missing rate limiting — verify can't spam reset emails +- [ ] **Frontend state:** Often missing user-scoped caching — verify localStorage cleared on logout +- [ ] **Audit log:** Often missing sensitive action logging (role changes, deletions) — verify admin actions tracked + +## Recovery Strategies + +When pitfalls occur despite prevention, how to recover. + +| Pitfall | Recovery Cost | Recovery Steps | +|---------|---------------|----------------| +| Deployed without tenant filters | HIGH | Emergency rollback, add filters, test with multiple users, redeploy | +| Migration destroyed data | HIGH | Restore from backup, fix migration script, re-run with copy of production data | +| Role explosion (30+ roles) | MEDIUM | Audit actual role usage, merge similar roles, migrate users to simplified roles | +| Invite codes leaked/enumerated | MEDIUM | Revoke all pending invites, generate new codes, notify legitimate users | +| JWT tokens invalidated prematurely | LOW | Communicate forced re-login, monitor for issues, extend token expiry next time | +| User deleted with active projects | MEDIUM | Soft-delete user instead, transfer projects to team lead, restore from backup if recent | +| Frontend leaks previous user data | LOW | Force cache clear (version localStorage key), deploy fix, notify affected users | +| Cascading delete destroyed shared data | HIGH | Restore from backup, change CASCADE to RESTRICT, add ownership transfer logic | + +## Pitfall-to-Phase Mapping + +How roadmap phases should address these pitfalls. + +| Pitfall | Prevention Phase | Verification | +|---------|------------------|--------------| +| Forgetting tenant_id filters | Phase 1 (Database Schema) | Integration test: User A can't see User B's data | +| SQLite migration failures | Phase 1 (Database Schema) | Test migration on production data copy, verify no data loss | +| Invalid JWT tokens | Phase 2 (Auth & Authorization) | Test old token format handled gracefully | +| Orphaned data without owner | Phase 1 (Database Schema) | Verify all rows have non-null user_id after migration | +| Role explosion | Phase 3 (Project Ownership) | Verify <= 5 roles exist, permission matrix simple | +| Invite system security | Phase 2 (Auth & Authorization) | Security audit: tokens secure, single-use, expire | +| Frontend state leaks | Phase 2 (Auth & Authorization) | Test user switching, verify state cleared | +| Cascading deletes | Phase 3 (Project Ownership) | Test user deletion with owned/shared projects | +| Missing edit attribution | Phase 1 (Database Schema) | Verify edited_by_user_id on all history entries | +| Tenant context leaks | Phase 2 (Auth & Authorization) | Load test with concurrent users, verify isolation | + +## Sources + +**Multi-Tenancy Patterns:** +- [MergeBoard - Multitenancy with FastAPI, SQLAlchemy and PostgreSQL](https://mergeboard.com/blog/6-multitenancy-fastapi-sqlalchemy-postgresql/) +- [Multi-Tenant Architecture with FastAPI: Design Patterns and Pitfalls](https://medium.com/@koushiksathish3/multi-tenant-architecture-with-fastapi-design-patterns-and-pitfalls-aa3f9e75bf8c) +- [Python FastAPI Postgres SqlAlchemy Row Level Security Multitenancy](https://adityamattos.com/multi-tenancy-in-python-fastapi-and-sqlalchemy-using-postgres-row-level-security) + +**SQLite Limitations:** +- [The SQLite Renaissance: Why the World's Most Deployed Database Is Taking Over Production in 2026](https://dev.to/pockit_tools/the-sqlite-renaissance-why-the-worlds-most-deployed-database-is-taking-over-production-in-2026-3jcc) +- [A mere add_foreign_key can wipe out your whole Rails+SQLite production table](https://kyrylo.org/software/2025/09/27/a-mere-add-foreign-key-can-wipe-out-your-whole-rails-sqlite-production-table.html) +- [Fixing ALTER TABLE errors with Flask-Migrate and SQLite](https://blog.miguelgrinberg.com/post/fixing-alter-table-errors-with-flask-migrate-and-sqlite) + +**JWT Security:** +- [JWT vs PASETO vs Branca — The Future of Secure Tokens in 2026](https://mojoauth.com/blog/jwt-vs-paseto-vs-branca-the-future-of-secure-tokens-in-2026) +- [Beyond the Secret: The Silent Risks of JWT and Machine Identity](https://medium.com/@instatunnel/beyond-the-secret-the-silent-risks-of-jwt-and-machine-identity-49bea4aa4547) +- [JWT Security Best Practices:Checklist for APIs](https://curity.io/resources/learn/jwt-best-practices/) + +**RBAC Implementation:** +- [6 Common Role Based Access Control (RBAC) Implementation Pitfalls](https://idenhaus.com/rbac-implementation-pitfalls/) +- [Role-Based Access Control Best Practices for 2026](https://www.techprescient.com/blogs/role-based-access-control-best-practices/) +- [Common Challenges in Role-Based Access Control Implementation](https://censinet.com/perspectives/common-challenges-role-based-access-control-implementation) + +**Invite System Security:** +- [How can I enhance the privacy and security of my invite-only event?](https://help.rsvpify.com/en/articles/5517672-how-can-i-enhance-the-privacy-and-security-of-my-invite-only-event) + +**Data Migration:** +- [Top data migration mistakes to avoid in 2026](https://www.kellton.com/kellton-tech-blog/top-data-migration-mistakes-to-avoid) +- [Top 10 Data Migration Risks and How to Avoid Them in 2026](https://medium.com/@kanerika/top-10-data-migration-risks-and-how-to-avoid-them-in-2026-fb5dc93c12f5) + +**FastAPI Dependency Injection:** +- [Dependency Injection in FastAPI: 2026 Playbook for Modular, Testable APIs](https://thelinuxcode.com/dependency-injection-in-fastapi-2026-playbook-for-modular-testable-apis/) +- [How to Use Dependency Injection in FastAPI](https://oneuptime.com/blog/post/2026-02-02-fastapi-dependency-injection/view) + +**Cascading Deletes:** +- [Why on_delete=models.CASCADE Doesn't Work as Expected in Django's Multi-Tenant Architecture](https://medium.com/@kevinrawal/why-on-delete-models-cascade-doesnt-work-as-expected-in-django-s-multi-tenant-architecture-45aac3faad3d) +- [Foreign Keys vs Performance (Part 3): The CASCADE DELETE Story](https://medium.com/@thyagodoliveiraperez/foreign-keys-vs-performance-part-3-the-cascade-delete-story-aac5cabd843b) + +**Permission Inheritance:** +- [SharePoint: Breaking and Managing Permission Inheritance](https://td.usnh.edu/TDClient/60/Portal/KB/ArticleDet?ID=3173) +- [Top 5 Common SharePoint Permissions Mistakes and How to Fix Them](https://lightningtools.com/blog/top-5-common-sharepoint-permissions-mistakes-and-how-to-fix-them/) + +**Angular State Management:** +- [Angular State Management for 2025](https://nx.dev/blog/angular-state-management-2025) +- [7 Angular 2026 Predictions That Could Redefine Frontend Architecture](https://dev.to/karol_modelski/7-angular-2026-predictions-that-could-redefine-frontend-architecture-e5n) + +--- +*Pitfalls research for: req-planner v2.0 multi-tenancy migration* +*Researched: 2026-03-18* diff --git a/.planning/research/STACK.md b/.planning/research/STACK.md new file mode 100644 index 0000000..f914a82 --- /dev/null +++ b/.planning/research/STACK.md @@ -0,0 +1,302 @@ +# Stack Research: Multi-Tenancy & User Accounts + +**Domain:** Requirements management tool with multi-user support +**Researched:** 2026-03-18 +**Confidence:** HIGH + +## Executive Summary + +Adding user accounts, invite-only registration, RBAC, and tenant isolation to your existing FastAPI + SQLAlchemy + SQLite stack requires minimal new dependencies. The core stack remains unchanged - only authentication primitives need upgrading from shared password to individual accounts. + +**Key additions:** +- **pwdlib** with Argon2 for password hashing (replaces bcrypt/passlib) +- **email-validator** for email validation in Pydantic models +- **secrets** module (stdlib) for invite token generation +- **Tenant isolation** via SQLAlchemy query filters (no library needed) + +**No changes needed:** Your existing FastAPI, SQLAlchemy 2.0, SQLite, PyJWT, and Angular 17 stack remain unchanged. + +## Recommended Stack Additions + +### Authentication & Security + +| Technology | Version | Purpose | Why Recommended | +|------------|---------|---------|-----------------| +| pwdlib | 0.4.0+ | Password hashing with Argon2 | Modern replacement for passlib (deprecated in Python 3.13). FastAPI official docs now recommend pwdlib. Argon2 is GPU-resistant and OWASP-recommended for 2026. | +| email-validator | 2.2.0+ | Email validation for user registration | Required by Pydantic's EmailStr type. Standard for email validation with deliverability checks. | +| secrets | stdlib | Secure invite token generation | Python standard library (3.6+). Cryptographically secure token generation with 256-bit randomness. Use `secrets.token_urlsafe(32)` for invite links. | + +### NO Library Needed (Use Patterns) + +| Capability | Implementation | Why No Library | +|------------|----------------|----------------| +| Multi-tenancy | SQLAlchemy query filters with `user_id` FK | Simple pattern - add `user_id` column to all tables, filter queries by current user. PostgreSQL Row-Level Security (RLS) not available in SQLite. | +| RBAC | Enum + dependency injection | Three roles (viewer/editor/owner) fit in simple Enum. Use FastAPI Depends() to check role on routes. Libraries like fastapi-user-auth add unnecessary complexity for 3 roles. | +| Edit attribution | Add `modified_by_user_id` column | You already track edit history - just add user reference to existing `requirement_history` table. | +| Email sending | Python `smtplib` (stdlib) | Built-in library sufficient for invite emails. Use FastAPI BackgroundTasks for async sending. No need for Celery yet. | + +## Unchanged Core Stack + +Your existing stack is already optimal for this use case: + +| Technology | Version | Status | Notes | +|------------|---------|--------|-------| +| FastAPI | Current | ✓ Keep | Dependency injection perfect for RBAC | +| SQLAlchemy | 2.0+ | ✓ Keep | Event listeners support tenant filtering | +| SQLite | 3.x | ✓ Keep | WAL mode supports concurrent readers + 1 writer (sufficient for small team) | +| PyJWT | 2.x | ✓ Keep | Already used for JWT - extend payload to include user_id and role | +| Angular | 17 | ✓ Keep | Frontend unchanged - JWT payload has new fields | +| Pydantic | 2.x | ✓ Keep | Add email-validator extra for EmailStr | + +## Installation + +```bash +# Backend additions only +pip install "pwdlib[argon2]>=0.4.0" +pip install "pydantic[email]>=2.0" + +# No changes to frontend +``` + +## Integration Patterns + +### 1. Password Hashing (pwdlib + Argon2) + +```python +from pwdlib import PasswordHash + +# Initialize once at startup +password_hash = PasswordHash.recommended() + +# Hash password on registration +hashed = password_hash.hash("user_password") + +# Verify on login +is_valid = password_hash.verify("user_password", hashed) +``` + +**Why this pattern:** FastAPI docs migrated from passlib to pwdlib in 2026. Argon2 is more secure than bcrypt and future-proof. + +### 2. Email Validation (Pydantic EmailStr) + +```python +from pydantic import BaseModel, EmailStr + +class UserCreate(BaseModel): + email: EmailStr # Automatically validated + password: str +``` + +**Why this pattern:** Pydantic's EmailStr uses email-validator under the hood. No manual validation needed. + +### 3. Invite Token Generation (secrets) + +```python +import secrets +from datetime import datetime, timedelta + +# Generate invite token +invite_token = secrets.token_urlsafe(32) # 256 bits +expires_at = datetime.utcnow() + timedelta(hours=24) + +# Store in database +invite = Invite(token=invite_token, email=email, expires_at=expires_at) +``` + +**Why this pattern:** Standard library, cryptographically secure, URL-safe for email links. + +### 4. Multi-Tenancy (SQLAlchemy Filters) + +```python +# Add to all models +class RequirementNode(Base): + __tablename__ = "requirement_nodes" + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + # ... other columns + +# Filter queries by current user +def get_requirements(db: Session, current_user: User): + return db.query(RequirementNode).filter( + RequirementNode.user_id == current_user.id + ).all() +``` + +**Why this pattern:** SQLite doesn't support PostgreSQL's Row-Level Security. Manual filtering is explicit and debuggable. Add SQLAlchemy event listener for automatic filtering if needed later. + +### 5. RBAC (Enum + Dependency Injection) + +```python +from enum import Enum +from fastapi import Depends, HTTPException + +class Role(str, Enum): + VIEWER = "viewer" + EDITOR = "editor" + OWNER = "owner" + +def require_role(required: Role): + def check_role(current_user: User = Depends(get_current_user)): + if current_user.role not in [required, Role.OWNER]: # Owner has all permissions + raise HTTPException(status_code=403, detail="Insufficient permissions") + return current_user + return check_role + +# Use in routes +@app.delete("/requirements/{id}") +async def delete_requirement( + id: int, + current_user: User = Depends(require_role(Role.EDITOR)) +): + ... +``` + +**Why this pattern:** FastAPI's Depends() makes role checks declarative. No library needed for 3 roles. + +### 6. JWT Payload Extension + +```python +# Extend existing JWT to include user info +def create_access_token(user: User) -> str: + payload = { + "sub": str(user.id), + "email": user.email, + "role": user.role.value, + "exp": datetime.utcnow() + timedelta(hours=24) + } + return jwt.encode(payload, SECRET_KEY, algorithm="HS256") +``` + +**Why this pattern:** Minimal change to existing JWT implementation. Frontend receives role for UI decisions. + +## What NOT to Add + +| Avoid | Why | Use Instead | +|-------|-----|-------------| +| FastAPI-Users | Over-engineered for your use case. Brings OAuth, 2FA, cookie auth you don't need. | Custom auth with pwdlib + PyJWT (you already have JWT logic) | +| Alembic | SQLite migrations are low-risk. You're self-hosted single-user currently. | Direct SQL migration scripts for v2 upgrade | +| Celery / Redis | Overkill for invite emails. Adds deployment complexity. | FastAPI BackgroundTasks + smtplib | +| SQLAlchemy-History | You already have `requirement_history` table with snapshots. | Add `modified_by_user_id` FK to existing table | +| PostgreSQL RLS libraries | SQLite doesn't support Row-Level Security. | Manual SQLAlchemy query filters | +| Casbin (RBAC library) | Policy engine for 100+ permission combinations. You have 3 roles. | Python Enum + FastAPI Depends() | + +## Database Schema Changes + +### New Tables + +```sql +-- Users table +CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT UNIQUE NOT NULL, + hashed_password TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'viewer', + is_active BOOLEAN NOT NULL DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Invite tokens table +CREATE TABLE invites ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + token TEXT UNIQUE NOT NULL, + email TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'viewer', + expires_at TIMESTAMP NOT NULL, + used_at TIMESTAMP, + created_by INTEGER REFERENCES users(id) +); +``` + +### Modified Tables + +Add `user_id` FK to existing tables: +- `requirement_nodes` - owner of requirement +- `cross_pillar_links` - who created link +- `projects` - project owner +- `requirement_history` - who made the change (`modified_by_user_id`) +- `graph_layouts` - who saved layout + +**Migration strategy:** Existing data gets assigned to "system" user (id=1) created during migration. + +## SQLite Concurrency Considerations + +### WAL Mode (Already Recommended) + +```python +# Ensure WAL mode is enabled +engine = create_engine( + "sqlite:///data/planner.db", + connect_args={"check_same_thread": False}, + echo=False +) +with engine.connect() as conn: + conn.execute(text("PRAGMA journal_mode=WAL")) +``` + +**Capabilities:** +- Multiple concurrent readers (unlimited) +- One writer at a time (sufficient for small team collaboration) +- Writers don't block readers + +**Limitations:** +- No true concurrent writes (BEGIN CONCURRENT is experimental, not production-ready) +- Single writer serialization means write-heavy workloads may see SQLITE_BUSY errors + +**When to migrate to PostgreSQL:** If you exceed ~10 concurrent users with frequent writes (requirements updates). For now, SQLite + WAL handles your use case. + +## Version Compatibility Matrix + +| Package | Version | Compatible With | Notes | +|---------|---------|-----------------|-------| +| pwdlib | 0.4.0+ | Python 3.9+ | Requires `pwdlib[argon2]` extra for Argon2 support | +| email-validator | 2.2.0+ | Pydantic 2.x | Required by `pydantic[email]` extra | +| PyJWT | 2.8.0+ | FastAPI 0.100+ | Already in your stack - no changes needed | +| SQLAlchemy | 2.0.0+ | Python 3.12+ | Already in your stack - no changes needed | +| Pydantic | 2.0+ | FastAPI 0.100+ | Requires email-validator for EmailStr | + +## Migration Checklist + +- [ ] Install pwdlib and email-validator +- [ ] Create `users` and `invites` tables +- [ ] Add `user_id` FK to all existing tables +- [ ] Migrate existing data to "system" user +- [ ] Replace shared password logic with user registration/login +- [ ] Add role-based route decorators +- [ ] Update JWT payload to include user_id and role +- [ ] Add query filters for tenant isolation +- [ ] Update frontend to handle user-specific data +- [ ] Test multi-user scenarios with WAL mode + +## Security Checklist + +- [ ] Use `pwdlib.PasswordHash.recommended()` for Argon2 defaults +- [ ] Validate all user inputs with Pydantic (EmailStr, password length) +- [ ] Use `secrets.token_urlsafe(32)` for invite tokens (256-bit) +- [ ] Set invite token expiration (24 hours recommended) +- [ ] Filter ALL queries by `user_id` (tenant isolation) +- [ ] Use FastAPI Depends() for role checks on protected routes +- [ ] Set JWT expiration (24 hours recommended) +- [ ] Enable SQLite WAL mode for concurrent access +- [ ] Rate limit registration and login endpoints (future: middleware) +- [ ] Use HTTPS in production (Caddy/nginx reverse proxy) + +## Sources + +**HIGH Confidence (Official Documentation):** +- [FastAPI OAuth2 with JWT](https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/) - Official security tutorial +- [pwdlib introduction](https://github.com/frankie567/pwdlib/discussions/1) - Modern password hashing +- [Python secrets module](https://docs.python.org/3/library/secrets.html) - Secure token generation +- [Pydantic email validation](https://docs.pydantic.dev/latest/install/) - EmailStr documentation +- [SQLite WAL mode](https://www.sqlite.org/wal.html) - Official SQLite concurrency documentation + +**MEDIUM Confidence (Industry Best Practices 2026):** +- [TestDriven.io FastAPI JWT Auth](https://testdriven.io/blog/fastapi-jwt-auth/) - Production patterns +- [FastAPI RBAC Tutorial](https://www.permit.io/blog/fastapi-rbac-full-implementation-tutorial) - Role-based access control +- [Tenant Isolation with SQLAlchemy](https://personal-web-9c834.web.app/blog/pg-tenant-isolation/) - Multi-tenancy patterns +- [The New Way To Generate Secure Tokens](https://blog.miguelgrinberg.com/post/the-new-way-to-generate-secure-tokens-in-python) - Token generation security +- [FastAPI Dependency Injection](https://fastapi.tiangolo.com/tutorial/security/get-current-user/) - Current user pattern + +--- +*Stack research for: req-planner v2.0 Multi-Tenancy & User Accounts* +*Researched: 2026-03-18* +*Confidence: HIGH - All recommendations verified against official docs and 2026 best practices* diff --git a/.planning/research/SUMMARY.md b/.planning/research/SUMMARY.md new file mode 100644 index 0000000..499ce3f --- /dev/null +++ b/.planning/research/SUMMARY.md @@ -0,0 +1,321 @@ +# Project Research Summary + +**Project:** req-planner v2.0 - Multi-Tenancy & User Accounts +**Domain:** Requirements management tool with collaborative features +**Researched:** 2026-03-18 +**Confidence:** HIGH + +## Executive Summary + +The req-planner v2.0 upgrade adds user accounts, invite-only registration, role-based access control, and project sharing to an existing FastAPI + SQLAlchemy + SQLite stack. Research shows this is a **well-documented migration pattern** with established best practices from 2026. The core architecture requires minimal new dependencies (pwdlib, email-validator) and leverages existing JWT infrastructure. The main risk is **tenant isolation failures** — forgetting to filter queries by user context is the #1 cause of data leaks in multi-tenant systems. The recommended approach uses shared-schema multi-tenancy with explicit query filtering, three-tier RBAC (owner/editor/viewer), and refresh token rotation for security. + +**Critical Success Factor:** Build tenant-scoped query patterns from day one. Every query must filter by user ownership or project membership. SQLite's limitations (no Row-Level Security) require manual enforcement, making systematic testing essential. The good news: this migration is additive — existing single-user functionality remains intact during the transition, allowing incremental deployment. + +**Key Recommendation:** Structure the roadmap in dependency order: (1) Authentication foundation with user accounts, (2) Tenant isolation with project ownership, (3) RBAC for sharing, (4) Polish with attribution and invites. This sequence minimizes risk by validating tenant isolation before adding sharing complexity. + +## Key Findings + +### Recommended Stack + +The existing stack (FastAPI, SQLAlchemy 2.0, SQLite, PyJWT, Angular 17) is optimal for this use case. **Only two new dependencies required:** + +**Core additions:** +- **pwdlib (Argon2)** — Password hashing, replaces deprecated passlib. FastAPI official docs recommend it as of 2026. Argon2 is GPU-resistant and OWASP-approved. +- **email-validator** — Required for Pydantic EmailStr validation. Standard for registration forms. +- **secrets (stdlib)** — Cryptographically secure invite token generation using `token_urlsafe(32)` for 256-bit randomness. + +**No library needed (use patterns instead):** +- Multi-tenancy via SQLAlchemy query filters with `user_id` FK (SQLite doesn't support PostgreSQL Row-Level Security) +- RBAC via Enum + FastAPI dependency injection (three roles don't justify a library like Casbin) +- Edit attribution via `modified_by_user_id` column on existing `requirement_history` table +- Email sending via stdlib `smtplib` + FastAPI BackgroundTasks (Celery is overkill) + +**Critical decision:** Stick with SQLite until 100+ concurrent users or 10GB+ database. WAL mode supports unlimited concurrent readers + 1 writer, sufficient for small team collaboration. Migration to PostgreSQL adds operational complexity — delay until necessary. + +### Expected Features + +**Must have for v2.0 launch (table stakes):** +- Individual user accounts with email/password authentication +- Invite-only registration with admin-generated tokens (security requirement for self-hosted) +- Per-user project ownership with workspace isolation (core multi-tenancy) +- Three-tier RBAC (owner/editor/viewer) on projects +- Edit attribution in history showing who made each change +- Project sharing UI with role assignment +- Password reset flow (expected security hygiene) +- Permission enforcement on both backend and frontend + +**Should have for v2.x (competitive differentiators):** +- Activity feeds per project ("what changed since I last logged in") +- Workspace-level dashboard aggregating user's projects +- Anonymous viewer links for sharing with clients without accounts +- Granular audit trail with per-edit rollback capabilities + +**Defer to v3+ (future consideration):** +- Fine-grained permission customization beyond three tiers +- Inherited permissions on requirement hierarchy (complex to implement correctly) +- Organization/team hierarchies for enterprise customers +- OAuth/SSO providers +- Real-time collaborative editing (major architectural change) + +**Anti-features (commonly requested but problematic):** +- Real-time collaborative editing: Massive architectural lift, requires WebSockets and operational transforms +- Organization hierarchies: Dramatically increases permission complexity +- Social login (OAuth): Complicates self-hosted deployments +- Field-level permissions: Explosion of permission checks, confusing UX + +### Architecture Approach + +The recommended architecture uses **four layers:** Authentication (user identity via JWT with refresh tokens), Authorization (RBAC via junction tables), Tenant Isolation (explicit query filtering by user context), and Migration Strategy (backwards-compatible schema changes). The key pattern is **shared-schema multi-tenancy** — single database with `user_id` columns and explicit filters, rather than database-per-tenant (which performs worse on SQLite's single-writer model). + +**Major components:** + +1. **Authentication Layer** — Extends existing JWT system with user accounts, adds refresh tokens (15min access, 30-day refresh), invite token validation, password reset flow. Uses pwdlib for Argon2 hashing. + +2. **User Context Injection** — FastAPI dependency `get_current_user()` extracts user_id from JWT, verifies user active, returns immutable `UserContext(user_id, email, is_admin)`. All routes receive current_user as dependency. + +3. **Tenant-Scoped Queries** — Manual filtering pattern: `query(Project).filter((Project.owner_id == user_id) | (Project.members.any(user_id)))`. Explicit over implicit (no hidden session middleware). SQLite lacks Row-Level Security. + +4. **RBAC Layer** — `ProjectMember` junction table with role enum (owner/editor/viewer). Permission checks via `require_project_access(project_id, required_role)` dependency. Role hierarchy encoded: owner > editor > viewer. + +5. **Migration Layer** — Add nullable `user_id` columns, seed default admin user, backfill existing data to admin, make columns NOT NULL. Dual auth during transition (old password endpoint + new email/password endpoint). + +**Critical pattern:** Never trust client-provided user_id. Always extract from server-verified JWT. Never use global query filters in middleware (breaks admin operations, hard to debug). Never store refresh tokens in localStorage (XSS risk). + +### Critical Pitfalls + +1. **Forgetting tenant filters in queries** — #1 cause of data leaks. Every SQLAlchemy query needs `.filter(user_id == current_user.id)` or project membership check. Missing a single filter exposes another user's data. **Mitigation:** Create base query functions enforcing tenant scoping, use integration tests with multiple users, code review checklist: "Does this query filter by tenant?" + +2. **SQLite foreign key constraint failures during migration** — Adding `user_id` FKs to existing tables causes migration failures or silent data loss. SQLite's ALTER TABLE limitations force table recreation with CASCADE risks. **Mitigation:** Use Alembic batch mode, test migration on production data copy, have rollback plan, consider enforcing constraints at application level initially. + +3. **Existing JWT tokens become invalid** — Deploying new auth invalidates all current sessions. Token payload structure changes (adding user_id) breaks old token format. **Mitigation:** Version tokens (`version: 2`), handle both formats during transition, communicate forced re-login window, consider token migration endpoint. + +4. **Orphaned data without clear ownership** — Existing projects/requirements have no `owner_id`. Assigning all to admin is wrong, leaving NULL breaks constraints. **Mitigation:** Decide ownership strategy before migration (assign to admin, create shared workspace, copy to all users, or delete legacy data), document rationale, make reversible. + +5. **Role explosion and privilege creep** — Starting with 3 roles (viewer/editor/owner) becomes dozens of overly-specific roles. Users accumulate permissions. **Mitigation:** Resist adding roles, use project-level assignments not feature-level, implement permission inheritance, audit roles quarterly, track role changes in audit log. + +## Implications for Roadmap + +Based on research, the roadmap should follow a **dependency-ordered sequence** that validates tenant isolation before adding sharing complexity. The architecture naturally divides into four milestones that can be developed incrementally with clear validation gates. + +### Phase 1: Authentication Foundation +**Rationale:** User accounts are the prerequisite for all multi-tenancy features. Build the identity layer before touching data isolation. + +**Delivers:** Users can register with invite tokens, login with email/password, receive JWT access + refresh tokens, and maintain sessions across browser sessions. + +**Addresses (from FEATURES.md):** +- Individual user accounts with email/password authentication +- Invite-only registration (security requirement) +- Session management with refresh tokens +- Password reset flow + +**Avoids (from PITFALLS.md):** +- Pitfall #3: JWT token invalidation (handle old token format gracefully) +- Pitfall #6: Invite system security weaknesses (cryptographically secure tokens, single-use, expiration) + +**Implementation notes:** +- Create `users`, `invite_tokens`, `refresh_tokens` tables +- Implement pwdlib password hashing +- Add `/auth/register`, `/auth/login`, `/auth/refresh`, `/auth/logout` endpoints +- Update `get_current_user()` dependency to extract from JWT +- Seed default admin user in migration + +**Research flag:** No additional research needed — well-documented pattern in FastAPI ecosystem. + +### Phase 2: Tenant Isolation & Project Ownership +**Rationale:** Establish data separation before adding collaboration. Validate users can only see their own data before allowing sharing. + +**Delivers:** Projects belong to specific users. Users see only projects they own. All queries filtered by user context. Existing data migrated to admin user. + +**Addresses (from FEATURES.md):** +- Per-user project ownership with workspace isolation +- Basic permission enforcement (backend + frontend) + +**Avoids (from PITFALLS.md):** +- Pitfall #1: Forgetting tenant filters (THE critical pitfall) +- Pitfall #4: Orphaned data without ownership +- Pitfall #10: Dependency injection scope leaks + +**Implementation notes:** +- Add `owner_id` column to `projects` (nullable initially) +- Add `created_by_id`, `updated_by_id` to `requirement_nodes` +- Update all queries to filter by ownership +- Migration: Assign existing projects to admin (id=1) +- Make `owner_id` NOT NULL after backfill +- Integration tests: User A cannot access User B's projects + +**Research flag:** No additional research needed — standard SQLAlchemy patterns. + +### Phase 3: RBAC & Project Sharing +**Rationale:** Now that tenant isolation is validated, enable controlled sharing with role-based permissions. + +**Delivers:** Project owners can invite collaborators with viewer/editor/owner roles. Permission checks enforce RBAC on all operations. UI shows user's role per project. + +**Addresses (from FEATURES.md):** +- Three-tier RBAC (owner/editor/viewer) +- Project sharing UI with role assignment +- Edit attribution in history (who made each change) + +**Avoids (from PITFALLS.md):** +- Pitfall #5: Role explosion (stick to 3 roles) +- Pitfall #8: Cascading deletes destroying shared data +- Pitfall #9: Missing edit attribution + +**Implementation notes:** +- Create `project_members` junction table with role enum +- Add `POST /projects/{id}/members`, `GET /projects/{id}/members`, `DELETE /projects/{id}/members/{user_id}` +- Implement `require_project_access(project_id, required_role)` dependency +- Update all requirement routes to check RBAC +- Add `user_id` to `requirement_history` for attribution +- Update UI to display editor names and role badges + +**Research flag:** RBAC patterns well-documented. May need phase-specific research for permission UI patterns if complex interactions emerge. + +### Phase 4: Polish & User Management +**Rationale:** With core multi-tenancy working, add user-facing features that improve collaboration experience. + +**Delivers:** Users can invite others to the platform. Admins can manage invites. History shows detailed edit attribution. Enhanced security with rate limiting. + +**Addresses (from FEATURES.md):** +- Enhanced invite system (user-driven invites, not just admin) +- Improved edit attribution in history UI +- User profile management +- Security hardening (rate limiting, token cleanup) + +**Avoids (from PITFALLS.md):** +- Pitfall #7: Frontend state management assumes single user +- Performance traps (N+1 queries, pagination) + +**Implementation notes:** +- Add `POST /auth/invites` (users can invite others) +- Add `GET /auth/invites` (list sent invites) +- Integrate email sending (SMTP + BackgroundTasks) +- Add rate limiting to auth endpoints (slowapi) +- Scheduled task for token cleanup +- Update history UI with user avatars and "edited by" attribution + +**Research flag:** Email integration may need focused research on SMTP providers, deliverability, and template design if building production-grade invite emails. + +### Phase Ordering Rationale + +**Why this order:** +1. **Authentication before isolation** — Can't isolate data without user identity +2. **Isolation before sharing** — Must prove single-user works before adding multi-user +3. **RBAC after ownership** — Roles are meaningless without established ownership model +4. **Polish last** — User-facing improvements after core functionality validated + +**How this avoids pitfalls:** +- Validates tenant isolation (Pitfall #1) in Phase 2 before sharing in Phase 3 +- Addresses data migration (Pitfall #4) early in Phase 2 when schema is simple +- Prevents role explosion (Pitfall #5) by defining minimal roles in Phase 3 +- Frontend state management (Pitfall #7) addressed progressively across phases + +**Dependency chain:** +- Phase 2 depends on Phase 1 (needs user_id from auth) +- Phase 3 depends on Phase 2 (needs project ownership to share) +- Phase 4 depends on Phase 3 (invites need roles to assign) + +**Incremental deployment:** +Each phase is independently deployable. Phase 1 can launch without breaking existing single-user mode. Phase 2 migrates data but doesn't force collaboration. Phase 3 adds sharing as opt-in. Phase 4 enhances without breaking. + +### Research Flags + +**Phases with standard patterns (skip `/gsd:research-phase`):** +- **Phase 1 (Authentication):** FastAPI JWT auth is well-documented, official examples exist, pwdlib docs comprehensive +- **Phase 2 (Tenant Isolation):** SQLAlchemy query filtering is standard ORM practice, multi-tenancy patterns mature +- **Phase 3 (RBAC):** Three-role model is trivial, junction table pattern universal, permission checks straightforward + +**Phases likely needing deeper research:** +- **Phase 4 (Email Integration):** If building production-grade invite emails, may need research on: + - SMTP provider selection (SendGrid vs Mailgun vs AWS SES) + - Email deliverability (SPF, DKIM, DMARC configuration) + - Template design and testing frameworks + - Invite link generation security (signed URLs, token rotation) + + However, for MVP, stdlib `smtplib` is sufficient — defer detailed research until email features become critical path. + +**Overall assessment:** This is a low-research-intensity roadmap. All core patterns are well-established. Only Phase 4 email features might warrant focused research if expanding beyond basic SMTP. + +## Confidence Assessment + +| Area | Confidence | Notes | +|------|------------|-------| +| Stack | **HIGH** | Recommendations verified against FastAPI official docs (2026), pwdlib is official replacement for passlib, SQLAlchemy 2.0 patterns stable. No experimental dependencies. | +| Features | **HIGH** | Feature categorization based on industry-standard collaboration tools (GitHub, Notion, Figma). Table stakes vs differentiators validated across multiple UX sources. Anti-features identified from documented failures. | +| Architecture | **HIGH** | Multi-tenancy patterns sourced from official guides, 2026 FastAPI tutorials, SQLAlchemy events documented. Dependency injection is core FastAPI feature. Refresh token rotation is OAuth 2.0 standard. | +| Pitfalls | **MEDIUM-HIGH** | Top pitfalls (#1 tenant filters, #2 SQLite migrations, #3 JWT invalidation) extensively documented across sources. Some pitfalls (#7 frontend state, #10 context scope) inferred from general patterns rather than domain-specific sources. | + +**Overall confidence:** **HIGH** + +The research is built on official documentation (FastAPI, SQLAlchemy, SQLite, Pydantic), authoritative 2026 guides, and multiple corroborating sources. Stack recommendations align with maintainer guidance. Architecture patterns are production-proven. Pitfalls are sourced from documented post-mortems and security analyses. + +### Gaps to Address + +**Minor gaps that need attention during implementation:** + +1. **Email deliverability details:** Research covered email *sending* (smtplib) but not email *deliverability* (SPF/DKIM/DMARC setup). If invite emails must reach inbox (not spam), Phase 4 may need focused research on email infrastructure. + - **How to handle:** Start with simple SMTP in development, use established email service (SendGrid/Mailgun) in production, defer deliverability research until user reports spam issues. + +2. **SQLite concurrency limits:** Research confirms WAL mode supports multiple readers + 1 writer, but exact concurrency threshold (when to migrate to PostgreSQL) is deployment-specific. + - **How to handle:** Monitor write contention in production (SQLITE_BUSY errors), plan PostgreSQL migration at 50+ concurrent writers or 10GB database, not earlier. + +3. **Cascading delete policy:** Research identifies the risk (Pitfall #8) but ownership transfer strategy is product decision, not technical. + - **How to handle:** Decide in Phase 3 planning: transfer to co-owner, mark as archived, or require explicit reassignment before user deletion. + +4. **Frontend state management details:** Research identifies risk (Pitfall #7) but Angular 17 specifics (Signals vs Services for user context) are implementation details. + - **How to handle:** Validate during Phase 2 frontend work, test user switching scenarios, follow Angular 17 best practices for auth guards. + +5. **Password strength requirements:** Research recommends enforcement but exact policy (length, complexity) is security policy decision. + - **How to handle:** Implement OWASP-recommended baseline (8 chars, mixed case, numbers) in Phase 1, allow customization in Phase 4 if self-hosted users request it. + +**No critical blockers identified.** All gaps are solvable during implementation without requiring additional research phases. + +## Sources + +### Primary (HIGH confidence) + +**Official Documentation:** +- [FastAPI OAuth2 with JWT](https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/) — Official security tutorial for authentication patterns +- [pwdlib introduction](https://github.com/frankie567/pwdlib/discussions/1) — Modern password hashing, FastAPI's recommended replacement for passlib +- [Python secrets module](https://docs.python.org/3/library/secrets.html) — Secure token generation (stdlib) +- [Pydantic email validation](https://docs.pydantic.dev/latest/install/) — EmailStr documentation +- [SQLite WAL mode](https://www.sqlite.org/wal.html) — Official SQLite concurrency documentation + +**2026 Technical Guides:** +- [Introduction to Multi-Tenant Design with FastAPI](https://blog.greeden.me/en/2026/03/10/introduction-to-multi-tenant-design-with-fastapi-practical-patterns-for-tenant-isolation-authorization-database-strategy-and-audit-logs/) — Comprehensive 2026 multi-tenancy patterns +- [Building a Modern User Permission Management System with FastAPI, SQLAlchemy 2.0](https://dev.to/mochafreddo/building-a-modern-user-permission-management-system-with-fastapi-sqlalchemy-and-mariadb-5fp1) — RBAC implementation guide +- [JWT in FastAPI, the Secure Way (Refresh Tokens Explained)](https://medium.com/@jagan_reddy/jwt-in-fastapi-the-secure-way-refresh-tokens-explained-f7d2d17b1d17) — Jan 2026 refresh token pattern + +### Secondary (MEDIUM confidence) + +**Multi-Tenancy & Architecture:** +- [MergeBoard: Multitenancy with FastAPI, SQLAlchemy and PostgreSQL](https://mergeboard.com/blog/6-multitenancy-fastapi-sqlalchemy-postgresql/) — Production patterns +- [GitHub Discussion: How to implement Multi tenancy in FastAPI](https://github.com/fastapi/fastapi/discussions/6056) — Community approaches +- [The developer's guide to SaaS multi-tenant architecture — WorkOS](https://workos.com/blog/developers-guide-saas-multi-tenant-architecture) — Tenant isolation strategies + +**RBAC & Authorization:** +- [FastAPI RBAC - Full Implementation Tutorial](https://www.permit.io/blog/fastapi-rbac-full-implementation-tutorial) — Permission layer patterns +- [How to Build a Role-Based Access Control Layer](https://www.osohq.com/learn/rbac-role-based-access-control) — RBAC design principles +- [Role-Based Access Control Best Practices for 2026](https://www.techprescient.com/blogs/role-based-access-control-best-practices/) — Industry best practices + +**Security & Data Isolation:** +- [Preventing Cross-Tenant Data Leakage in Multi-Tenant SaaS Systems](https://agnitestudio.com/blog/preventing-cross-tenant-leakage/) — Security patterns +- [JWT Security Best Practices: Checklist for APIs](https://curity.io/resources/learn/jwt-best-practices/) — Token security +- [6 Common Role Based Access Control (RBAC) Implementation Pitfalls](https://idenhaus.com/rbac-implementation-pitfalls/) — Documented failures + +**UX & Collaboration Patterns:** +- [Guide to sharing and permissions – Figma Learn](https://help.figma.com/hc/en-us/articles/1500007609322-Guide-to-sharing-and-permissions) — Collaboration UX patterns +- [User Onboarding Strategies in a B2B SaaS Application](https://auth0.com/blog/user-onboarding-strategies-b2b-saas/) — Invite-only registration patterns +- [Google Workspace Updates: "Show Editors" in Google Docs](https://workspaceupdates.googleblog.com/2021/05/view-more-context-on-google-docs-edits-with-show-editors.html) — Edit attribution patterns + +### Tertiary (LOW confidence, needs validation) + +**SQLite Limitations:** +- [A mere add_foreign_key can wipe out your whole Rails+SQLite production table](https://kyrylo.org/software/2025/09/27/a-mere-add-foreign-key-can-wipe-out-your-whole-rails-sqlite-production-table.html) — Migration risks (Rails-specific but conceptually applicable) +- [High Performance SQLite: Multi-tenancy](https://highperformancesqlite.com/watch/multi-tenancy) — Performance patterns (video, not peer-reviewed) + +--- + +**Research completed:** 2026-03-18 +**Ready for roadmap creation:** YES + +All four research files synthesized. Recommended phase structure validated against dependencies, pitfalls, and architecture patterns. No critical blockers identified. Roadmap can proceed with high confidence. diff --git a/backend/app/database.py b/backend/app/database.py index 5959c37..8d2c56f 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -20,3 +20,14 @@ def get_db(): yield db finally: db.close() + + +def switch_database(db_path: Path): + """Hot-swap the active database engine and session factory.""" + global engine, SessionLocal, DB_PATH + DB_PATH = db_path + engine.dispose() + engine = create_engine(f"sqlite:///{db_path}", echo=False) + SessionLocal = sessionmaker(bind=engine, class_=Session, expire_on_commit=False) + Base.metadata.create_all(bind=engine) + return engine diff --git a/backend/app/main.py b/backend/app/main.py index e071b3a..955b8f2 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -3,7 +3,7 @@ from fastapi.middleware.cors import CORSMiddleware from .auth import router as auth_router from .database import Base, engine -from .routers import layouts, links, projects, requirements, stats +from .routers import databases, layouts, links, projects, requirements, stats Base.metadata.create_all(bind=engine) @@ -27,6 +27,7 @@ app.include_router(links.router) app.include_router(projects.router) app.include_router(stats.router) app.include_router(layouts.router) +app.include_router(databases.router) @app.get("/api/health") diff --git a/backend/app/models.py b/backend/app/models.py index 9ee58b6..dfd48a4 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -58,7 +58,7 @@ class RequirementNode(Base): status: Mapped[Status] = mapped_column(Enum(Status), default=Status.draft) node_type: Mapped[NodeType] = mapped_column(Enum(NodeType), default=NodeType.requirement) parent_id: Mapped[str | None] = mapped_column( - String(64), ForeignKey("requirement_nodes.id", ondelete="CASCADE"), nullable=True + String(64), ForeignKey("requirement_nodes.id", ondelete="SET NULL"), nullable=True ) sort_order: Mapped[int] = mapped_column(Integer, default=0) project_id: Mapped[int | None] = mapped_column( @@ -99,7 +99,7 @@ class RequirementNode(Base): "RequirementNode", remote_side="RequirementNode.id", back_populates="children" ) children: Mapped[list["RequirementNode"]] = relationship( - "RequirementNode", back_populates="parent", cascade="all, delete-orphan", + "RequirementNode", back_populates="parent", order_by="RequirementNode.sort_order" ) project: Mapped["Project | None"] = relationship("Project", back_populates="requirements") diff --git a/backend/app/quality.py b/backend/app/quality.py index 7c659a6..06a8606 100644 --- a/backend/app/quality.py +++ b/backend/app/quality.py @@ -18,7 +18,7 @@ def validate_requirement(node: RequirementNode, children: list[RequirementNode]) traceable = True if node.node_type != NodeType.pillar: if not node.parent_id: - errors.append("Non-pillar node must have a parent (traceability)") + errors.append("Orphaned node — must be assigned a parent for traceability") traceable = False if not node.description: warnings.append("Missing description — hard to trace how this serves parent") diff --git a/backend/app/routers/databases.py b/backend/app/routers/databases.py new file mode 100644 index 0000000..f37fa35 --- /dev/null +++ b/backend/app/routers/databases.py @@ -0,0 +1,135 @@ +"""Database switching endpoints — list, switch, create, upload, and download.""" +import shutil +from pathlib import Path + +from fastapi import APIRouter, Depends, HTTPException, UploadFile, File +from fastapi.responses import FileResponse +from pydantic import BaseModel + +from ..auth import verify_token +from .. import database as db_module + +router = APIRouter( + prefix="/api/databases", + tags=["databases"], + dependencies=[Depends(verify_token)], +) + + +class DatabaseInfo(BaseModel): + name: str + path: str + size_bytes: int + active: bool + + +class SwitchRequest(BaseModel): + path: str + + +class CreateRequest(BaseModel): + name: str + + +class CopyRequest(BaseModel): + name: str + + +def _db_info(path: Path, active: bool) -> DatabaseInfo: + return DatabaseInfo( + name=path.stem, + path=str(path), + size_bytes=path.stat().st_size, + active=active, + ) + + +@router.get("", response_model=list[DatabaseInfo]) +def list_databases(): + """List all .db files in the data directory.""" + current = Path(db_module.DB_PATH).resolve() + dbs = [] + for f in sorted(db_module.DATA_DIR.glob("*.db")): + dbs.append(_db_info(f, f.resolve() == current)) + return dbs + + +@router.post("/switch", response_model=DatabaseInfo) +def switch_db(req: SwitchRequest): + """Switch to a different database file. Path must be an existing .db file.""" + db_path = Path(req.path) + if not db_path.exists(): + raise HTTPException(404, f"Database not found: {req.path}") + if db_path.suffix != ".db": + raise HTTPException(400, "Path must point to a .db file") + db_module.switch_database(db_path) + return _db_info(db_path, True) + + +@router.post("/create", response_model=DatabaseInfo) +def create_db(req: CreateRequest): + """Create a new empty database and switch to it.""" + name = req.name.strip() + if not name: + raise HTTPException(400, "Name is required") + if not name.endswith(".db"): + name += ".db" + db_path = db_module.DATA_DIR / name + if db_path.exists(): + raise HTTPException(409, f"Database already exists: {name}") + db_module.switch_database(db_path) + return _db_info(db_path, True) + + +@router.post("/copy", response_model=DatabaseInfo) +def copy_current_db(req: CopyRequest): + """Copy the current active database to a new file (snapshot/backup).""" + name = req.name.strip() + if not name: + raise HTTPException(400, "Name is required") + if not name.endswith(".db"): + name += ".db" + dest = db_module.DATA_DIR / name + if dest.exists(): + raise HTTPException(409, f"Database already exists: {name}") + shutil.copy2(db_module.DB_PATH, dest) + return _db_info(dest, False) + + +@router.get("/active", response_model=DatabaseInfo) +def get_active(): + """Return info about the currently active database.""" + return _db_info(Path(db_module.DB_PATH), True) + + +@router.post("/upload", response_model=DatabaseInfo) +def upload_db(file: UploadFile = File(...), switch: bool = True): + """Upload a .db file from the client and optionally switch to it.""" + if not file.filename or not file.filename.endswith(".db"): + raise HTTPException(400, "File must be a .db file") + # Sanitize filename + safe_name = Path(file.filename).name + dest = db_module.DATA_DIR / safe_name + # If it already exists, add a suffix + counter = 1 + while dest.exists(): + dest = db_module.DATA_DIR / f"{Path(safe_name).stem}_{counter}.db" + counter += 1 + with open(dest, "wb") as f: + shutil.copyfileobj(file.file, f) + if switch: + db_module.switch_database(dest) + return _db_info(dest, switch) + + +@router.get("/download") +def download_db(): + """Download the currently active database file.""" + p = Path(db_module.DB_PATH) + if not p.exists(): + raise HTTPException(404, "Active database not found") + return FileResponse( + path=str(p), + filename=p.name, + media_type="application/octet-stream", + ) diff --git a/backend/app/routers/layouts.py b/backend/app/routers/layouts.py index b0bd6c2..eadf6dc 100644 --- a/backend/app/routers/layouts.py +++ b/backend/app/routers/layouts.py @@ -29,14 +29,52 @@ def _to_detail_response(layout: GraphLayout) -> GraphLayoutDetailResponse: ) +AUTOSAVE_NAME = "__autosave__" + + @router.get("", response_model=list[GraphLayoutResponse]) def list_layouts(project_id: int | None = None, db: Session = Depends(get_db)): - query = db.query(GraphLayout) + query = db.query(GraphLayout).filter(GraphLayout.name != AUTOSAVE_NAME) if project_id is not None: query = query.filter(GraphLayout.project_id == project_id) return [_to_response(l) for l in query.order_by(GraphLayout.updated_at.desc()).all()] +@router.get("/autosave", response_model=GraphLayoutDetailResponse) +def get_autosave(db: Session = Depends(get_db)): + layout = db.query(GraphLayout).filter(GraphLayout.name == AUTOSAVE_NAME).first() + if not layout: + raise HTTPException(404, "No autosave layout found") + return _to_detail_response(layout) + + +@router.put("/autosave", response_model=GraphLayoutDetailResponse) +def upsert_autosave(data: GraphLayoutUpdate, db: Session = Depends(get_db)): + layout = db.query(GraphLayout).filter(GraphLayout.name == AUTOSAVE_NAME).first() + update_data = data.model_dump(exclude_unset=True) + if "node_positions" in update_data: + update_data["node_positions"] = json.dumps(update_data["node_positions"]) + if "viewport" in update_data: + update_data["viewport"] = json.dumps(update_data["viewport"]) + + if layout: + for key, value in update_data.items(): + setattr(layout, key, value) + else: + layout = GraphLayout( + name=AUTOSAVE_NAME, + description="Auto-saved working state", + node_positions=update_data.get("node_positions", "{}"), + viewport=update_data.get("viewport", "{}"), + is_default=False, + ) + db.add(layout) + + db.commit() + db.refresh(layout) + return _to_detail_response(layout) + + @router.get("/{layout_id}", response_model=GraphLayoutDetailResponse) def get_layout(layout_id: int, db: Session = Depends(get_db)): layout = db.get(GraphLayout, layout_id) diff --git a/backend/app/routers/requirements.py b/backend/app/routers/requirements.py index a5c9c7d..680a55e 100644 --- a/backend/app/routers/requirements.py +++ b/backend/app/routers/requirements.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Body, Depends, HTTPException from sqlalchemy import func from sqlalchemy.orm import Session @@ -54,11 +54,21 @@ def _next_version(db: Session, req_id: str) -> int: return (max_ver or 0) + 1 -def _auto_summary(update_data: dict) -> str: - fields = [k for k in update_data if k in _SNAPSHOT_FIELDS] - if not fields: - return "Update" - return "Updated " + ", ".join(fields) +def _auto_summary(update_data: dict, node: RequirementNode) -> str: + changed = [] + for k in update_data: + if k not in _SNAPSHOT_FIELDS: + continue + old_val = getattr(node, k, None) + new_val = update_data[k] + # Normalize enums for comparison + if hasattr(old_val, "value"): + old_val = old_val.value + if old_val != new_val: + changed.append(k) + if not changed: + return "No changes" + return "Updated " + ", ".join(changed) def _to_response(node: RequirementNode, include_children: bool = False) -> RequirementResponse: @@ -150,6 +160,8 @@ def update_requirement(req_id: str, data: RequirementUpdate, db: Session = Depen raise HTTPException(404, f"Requirement {req_id} not found") update_data = data.model_dump(exclude_unset=True) + summary = _auto_summary(update_data, node) + for key, value in update_data.items(): setattr(node, key, value) @@ -158,7 +170,7 @@ def update_requirement(req_id: str, data: RequirementUpdate, db: Session = Depen node.traceable = quality.traceable version = _next_version(db, req_id) - _create_history_entry(db, node, version, _auto_summary(update_data)) + _create_history_entry(db, node, version, summary) db.commit() db.refresh(node) @@ -166,10 +178,53 @@ def update_requirement(req_id: str, data: RequirementUpdate, db: Session = Depen @router.delete("/{req_id}", status_code=204) -def delete_requirement(req_id: str, db: Session = Depends(get_db)): +def delete_requirement( + req_id: str, + db: Session = Depends(get_db), + delete_ids: list[str] = Body(default=[]), +): node = db.get(RequirementNode, req_id) if not node: raise HTTPException(404, f"Requirement {req_id} not found") + + # Recursively collect all descendants of the IDs marked for deletion + ids_to_delete: set[str] = set(delete_ids) + if ids_to_delete: + queue = list(ids_to_delete) + while queue: + nid = queue.pop() + child_node = db.get(RequirementNode, nid) + if child_node: + for c in child_node.children: + if c.id not in ids_to_delete: + ids_to_delete.add(c.id) + queue.append(c.id) + + # Orphan direct children not in the delete set + for child in list(node.children): + if child.id in ids_to_delete: + continue + child.parent_id = None + quality = validate_requirement(child, child.children) + child.traceable = quality.traceable + + # Flush so the parent_id = NULL changes are persisted before we delete the parent + db.flush() + + # Delete the marked descendants + for did in ids_to_delete: + desc = db.get(RequirementNode, did) + if desc: + # Orphan any grandchildren of a deleted node that aren't themselves being deleted + for grandchild in list(desc.children): + if grandchild.id not in ids_to_delete: + grandchild.parent_id = None + quality = validate_requirement(grandchild, grandchild.children) + grandchild.traceable = quality.traceable + db.flush() + db.delete(desc) + + db.flush() db.delete(node) db.commit() @@ -197,7 +252,7 @@ def get_children(req_id: str, db: Session = Depends(get_db)): node = db.get(RequirementNode, req_id) if not node: raise HTTPException(404, f"Requirement {req_id} not found") - return [_to_response(c) for c in node.children] + return [_to_response(c, include_children=True) for c in node.children] # ─── History Endpoints ──────────────────────────────────────────────────── diff --git a/backend/app/routers/stats.py b/backend/app/routers/stats.py index 3ae5a9a..35c9ef4 100644 --- a/backend/app/routers/stats.py +++ b/backend/app/routers/stats.py @@ -25,6 +25,7 @@ def get_stats(project_id: int | None = None, db: Session = Depends(get_db)): quality_passing = 0 leaves_with_tests = 0 total_leaves = 0 + orphaned = 0 for n in nodes: by_type[n.node_type.value] = by_type.get(n.node_type.value, 0) + 1 @@ -39,6 +40,9 @@ def get_stats(project_id: int | None = None, db: Session = Depends(get_db)): if n.test_spec: leaves_with_tests += 1 + if n.node_type != NodeType.pillar and not n.parent_id: + orphaned += 1 + coverage = (leaves_with_tests / total_leaves * 100) if total_leaves > 0 else 0.0 return StatsResponse( @@ -47,6 +51,7 @@ def get_stats(project_id: int | None = None, db: Session = Depends(get_db)): requirements=by_type.get("requirement", 0), sub_requirements=by_type.get("sub_requirement", 0), leaves=by_type.get("leaf", 0), + orphaned=orphaned, by_status=by_status, by_priority=by_priority, quality_passing=quality_passing, diff --git a/backend/app/schemas.py b/backend/app/schemas.py index ea103de..e8639de 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -222,6 +222,7 @@ class StatsResponse(BaseModel): requirements: int sub_requirements: int leaves: int + orphaned: int by_status: dict[str, int] by_priority: dict[str, int] quality_passing: int diff --git a/backend/pyproject.toml b/backend/pyproject.toml index c19fbb6..62b5963 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -11,6 +11,7 @@ dependencies = [ "pydantic>=2.10.0", "pyyaml>=6.0.2", "aiosqlite>=0.20.0", + "python-multipart>=0.0.18", ] [project.optional-dependencies] diff --git a/backend/uv.lock b/backend/uv.lock new file mode 100644 index 0000000..e890efa --- /dev/null +++ b/backend/uv.lock @@ -0,0 +1,812 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "aiosqlite" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821, upload-time = "2025-12-23T19:25:43.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" }, +] + +[[package]] +name = "alembic" +version = "1.18.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "fastapi" +version = "0.135.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/e7/7b/f8e0211e9380f7195ba3f3d40c292594fd81ba8ec4629e3854c353aaca45/fastapi-0.135.1.tar.gz", hash = "sha256:d04115b508d936d254cea545b7312ecaa58a7b3a0f84952535b4c9afae7668cd", size = 394962, upload-time = "2026-03-01T18:18:29.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/72/42e900510195b23a56bde950d26a51f8b723846bfcaa0286e90287f0422b/fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", size = 116999, upload-time = "2026-03-01T18:18:30.831Z" }, +] + +[[package]] +name = "greenlet" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, + { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, + { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, + { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, + { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, + { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, + { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "mako" +version = "1.3.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { 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 = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[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/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { 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 = "req-planner" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "aiosqlite" }, + { name = "alembic" }, + { name = "fastapi" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "sqlalchemy" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[package.optional-dependencies] +dev = [ + { name = "httpx" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiosqlite", specifier = ">=0.20.0" }, + { name = "alembic", specifier = ">=1.14.0" }, + { name = "fastapi", specifier = ">=0.115.0" }, + { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.28.0" }, + { name = "pydantic", specifier = ">=2.10.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24.0" }, + { name = "pyyaml", specifier = ">=6.0.2" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8.0" }, + { name = "sqlalchemy", specifier = ">=2.0.36" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.34.0" }, +] +provides-extras = ["dev"] + +[[package]] +name = "ruff" +version = "0.15.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/df/f8629c19c5318601d3121e230f74cbee7a3732339c52b21daa2b82ef9c7d/ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4", size = 4597916, upload-time = "2026-03-12T23:05:47.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/2f/4e03a7e5ce99b517e98d3b4951f411de2b0fa8348d39cf446671adcce9a2/ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff", size = 10508953, upload-time = "2026-03-12T23:05:17.246Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/55bcdc3e9f80bcf39edf0cd272da6fa511a3d94d5a0dd9e0adf76ceebdb4/ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3", size = 10942257, upload-time = "2026-03-12T23:05:23.076Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/005c29bd1726c0f492bfa215e95154cf480574140cb5f867c797c18c790b/ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb", size = 10322683, upload-time = "2026-03-12T23:05:33.738Z" }, + { url = "https://files.pythonhosted.org/packages/5f/74/2f861f5fd7cbb2146bddb5501450300ce41562da36d21868c69b7a828169/ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8", size = 10660986, upload-time = "2026-03-12T23:05:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a1/309f2364a424eccb763cdafc49df843c282609f47fe53aa83f38272389e0/ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e", size = 10332177, upload-time = "2026-03-12T23:05:56.145Z" }, + { url = "https://files.pythonhosted.org/packages/30/41/7ebf1d32658b4bab20f8ac80972fb19cd4e2c6b78552be263a680edc55ac/ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15", size = 11170783, upload-time = "2026-03-12T23:06:01.742Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/6d488f6adca047df82cd62c304638bcb00821c36bd4881cfca221561fdfc/ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9", size = 12044201, upload-time = "2026-03-12T23:05:28.697Z" }, + { url = "https://files.pythonhosted.org/packages/71/68/e6f125df4af7e6d0b498f8d373274794bc5156b324e8ab4bf5c1b4fc0ec7/ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab", size = 11421561, upload-time = "2026-03-12T23:05:31.236Z" }, + { url = "https://files.pythonhosted.org/packages/f1/9f/f85ef5fd01a52e0b472b26dc1b4bd228b8f6f0435975442ffa4741278703/ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e", size = 11310928, upload-time = "2026-03-12T23:05:45.288Z" }, + { url = "https://files.pythonhosted.org/packages/8c/26/b75f8c421f5654304b89471ed384ae8c7f42b4dff58fa6ce1626d7f2b59a/ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c", size = 11235186, upload-time = "2026-03-12T23:05:50.677Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/d5a6d065962ff7a68a86c9b4f5500f7d101a0792078de636526c0edd40da/ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512", size = 10635231, upload-time = "2026-03-12T23:05:37.044Z" }, + { url = "https://files.pythonhosted.org/packages/d6/56/7c3acf3d50910375349016cf33de24be021532042afbed87942858992491/ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0", size = 10340357, upload-time = "2026-03-12T23:06:04.748Z" }, + { url = "https://files.pythonhosted.org/packages/06/54/6faa39e9c1033ff6a3b6e76b5df536931cd30caf64988e112bbf91ef5ce5/ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb", size = 10860583, upload-time = "2026-03-12T23:05:58.978Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/509a201b843b4dfb0b32acdedf68d951d3377988cae43949ba4c4133a96a/ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0", size = 11410976, upload-time = "2026-03-12T23:05:39.955Z" }, + { url = "https://files.pythonhosted.org/packages/6c/25/3fc9114abf979a41673ce877c08016f8e660ad6cf508c3957f537d2e9fa9/ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c", size = 10616872, upload-time = "2026-03-12T23:05:42.451Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/09ece68445ceac348df06e08bf75db72d0e8427765b96c9c0ffabc1be1d9/ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406", size = 11787271, upload-time = "2026-03-12T23:05:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497, upload-time = "2026-03-12T23:05:25.968Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.48" +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/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/91/a42ae716f8925e9659df2da21ba941f158686856107a61cc97a95e7647a3/sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b", size = 2155737, upload-time = "2026-03-02T15:49:13.207Z" }, + { url = "https://files.pythonhosted.org/packages/b9/52/f75f516a1f3888f027c1cfb5d22d4376f4b46236f2e8669dcb0cddc60275/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb", size = 3337020, upload-time = "2026-03-02T15:50:34.547Z" }, + { url = "https://files.pythonhosted.org/packages/37/9a/0c28b6371e0cdcb14f8f1930778cb3123acfcbd2c95bb9cf6b4a2ba0cce3/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894", size = 3349983, upload-time = "2026-03-02T15:53:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/0aee8f3ff20b1dcbceb46ca2d87fcc3d48b407925a383ff668218509d132/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9", size = 3279690, upload-time = "2026-03-02T15:50:36.277Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/a957bc91293b49181350bfd55e6dfc6e30b7f7d83dc6792d72043274a390/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e", size = 3314738, upload-time = "2026-03-02T15:53:27.519Z" }, + { url = "https://files.pythonhosted.org/packages/4b/44/1d257d9f9556661e7bdc83667cc414ba210acfc110c82938cb3611eea58f/sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99", size = 2115546, upload-time = "2026-03-02T15:54:31.591Z" }, + { url = "https://files.pythonhosted.org/packages/f2/af/c3c7e1f3a2b383155a16454df62ae8c62a30dd238e42e68c24cebebbfae6/sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a", size = 2142484, upload-time = "2026-03-02T15:54:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4", size = 2152599, upload-time = "2026-03-02T15:49:14.41Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f", size = 3278825, upload-time = "2026-03-02T15:50:38.269Z" }, + { url = "https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed", size = 3295200, upload-time = "2026-03-02T15:53:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/1609a4442aefd750ea2f32629559394ec92e89ac1d621a7f462b70f736ff/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658", size = 3226876, upload-time = "2026-03-02T15:50:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/37/c3/6ae2ab5ea2fa989fbac4e674de01224b7a9d744becaf59bb967d62e99bed/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8", size = 3265045, upload-time = "2026-03-02T15:53:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/6f/82/ea4665d1bb98c50c19666e672f21b81356bd6077c4574e3d2bbb84541f53/sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131", size = 2113700, upload-time = "2026-03-02T15:54:35.825Z" }, + { url = "https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2", size = 2139487, upload-time = "2026-03-02T15:54:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/7b17bd50244b78a49d22cc63c969d71dc4de54567dc152a9b46f6fae40ce/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae", size = 3558851, upload-time = "2026-03-02T15:57:48.607Z" }, + { url = "https://files.pythonhosted.org/packages/20/0d/213668e9aca61d370f7d2a6449ea4ec699747fac67d4bda1bb3d129025be/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb", size = 3525525, upload-time = "2026-03-02T16:04:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/85/d7/a84edf412979e7d59c69b89a5871f90a49228360594680e667cb2c46a828/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b", size = 3466611, upload-time = "2026-03-02T15:57:50.759Z" }, + { url = "https://files.pythonhosted.org/packages/86/55/42404ce5770f6be26a2b0607e7866c31b9a4176c819e9a7a5e0a055770be/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121", size = 3475812, upload-time = "2026-03-02T16:04:40.092Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ae/29b87775fadc43e627cf582fe3bda4d02e300f6b8f2747c764950d13784c/sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485", size = 2141335, upload-time = "2026-03-02T15:52:51.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/44/f39d063c90f2443e5b46ec4819abd3d8de653893aae92df42a5c4f5843de/sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79", size = 2173095, upload-time = "2026-03-02T15:52:52.79Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b3/f437eaa1cf028bb3c927172c7272366393e73ccd104dcf5b6963f4ab5318/sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd", size = 2154401, upload-time = "2026-03-02T15:49:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1c/b3abdf0f402aa3f60f0df6ea53d92a162b458fca2321d8f1f00278506402/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f", size = 3274528, upload-time = "2026-03-02T15:50:41.489Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5e/327428a034407651a048f5e624361adf3f9fbac9d0fa98e981e9c6ff2f5e/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b", size = 3279523, upload-time = "2026-03-02T15:53:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ca/ece73c81a918add0965b76b868b7b5359e068380b90ef1656ee995940c02/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0", size = 3224312, upload-time = "2026-03-02T15:50:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/fbaf1ae91fa4ee43f4fe79661cead6358644824419c26adb004941bdce7c/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2", size = 3246304, upload-time = "2026-03-02T15:53:34.937Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5fb0deb13930b4f2f698c5541ae076c18981173e27dd00376dbaea7a9c82/sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6", size = 2116565, upload-time = "2026-03-02T15:54:38.321Z" }, + { url = "https://files.pythonhosted.org/packages/95/7e/e83615cb63f80047f18e61e31e8e32257d39458426c23006deeaf48f463b/sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0", size = 2142205, upload-time = "2026-03-02T15:54:39.831Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/69d8711b3f2c5135e9cde5f063bc1605860f0b2c53086d40c04017eb1f77/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241", size = 3563519, upload-time = "2026-03-02T15:57:52.387Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4f/a7cce98facca73c149ea4578981594aaa5fd841e956834931de503359336/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0", size = 3528611, upload-time = "2026-03-02T16:04:42.097Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7d/5936c7a03a0b0cb0fa0cc425998821c6029756b0855a8f7ee70fba1de955/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3", size = 3472326, upload-time = "2026-03-02T15:57:54.423Z" }, + { url = "https://files.pythonhosted.org/packages/f4/33/cea7dfc31b52904efe3dcdc169eb4514078887dff1f5ae28a7f4c5d54b3c/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b", size = 3478453, upload-time = "2026-03-02T16:04:44.584Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/32107c4d13be077a9cae61e9ae49966a35dc4bf442a8852dd871db31f62e/sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f", size = 2147209, upload-time = "2026-03-02T15:52:54.274Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d7/1e073da7a4bc645eb83c76067284a0374e643bc4be57f14cc6414656f92c/sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933", size = 2182198, upload-time = "2026-03-02T15:52:55.606Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.42.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/ad/4a96c425be6fb67e0621e62d86c402b4a17ab2be7f7c055d9bd2f638b9e2/uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775", size = 85393, upload-time = "2026-03-16T06:19:50.077Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/89/f8827ccff89c1586027a105e5630ff6139a64da2515e24dafe860bd9ae4d/uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359", size = 68830, upload-time = "2026-03-16T06:19:48.325Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] diff --git a/data/demo.db b/data/demo.db new file mode 100644 index 0000000000000000000000000000000000000000..c19ccd1f04b9f40ee4c5a32ded49b033ebdad932 GIT binary patch literal 53248 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 NSJ06k11^$~{{x*2u&@9C literal 0 HcmV?d00001 diff --git a/data/planner - 20263103.db b/data/planner - 20263103.db new file mode 100644 index 0000000000000000000000000000000000000000..1f6bcc113d360f8e3ddda907d9fc19d72ae19f2b GIT binary patch literal 172032 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 literal 0 HcmV?d00001 diff --git a/data/planner.db b/data/planner.db new file mode 100644 index 0000000000000000000000000000000000000000..50617ec592c773098f9d3ddfe0be41ea71dc1036 GIT binary patch literal 86016 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 VPX=^6pxOXm0GQo0Hzfl3{{ty$9oql^ literal 0 HcmV?d00001 diff --git a/data/planner_backup_2026-03-18.db b/data/planner_backup_2026-03-18.db new file mode 100644 index 0000000000000000000000000000000000000000..e733c568a880d9eb1068e3848b25f6b007c4d0aa GIT binary patch literal 172032 zcmeFa31A%Ac^+5*i2L$rM9~0{%_jvU9? z{r>mrc-40Upk|~rW&n0|y?XWD|NrlQ|NGDN=dZ=GhP<6j?}f9nCVhp})+W6}mL;hp zE=kf;`0d8;TkzY1-xK(?e%h=L)&E5wo_k+A*xxVpw*8JY^yi(ap^x;=_I;}DcZOaZ z{H1}3>QA=r91{dy8|dmjrD<*7_)Ipu6*tnx-CQhf>=}vd=58#LO{NbjeAPR*Hb1*D zFK^5~du?8>&_JH->y1U_#pR9pEAwme^2&z1eEr(B3vyqt*cwe-S2)hYGfkmSSm}8Z_K~AL2pf^W65+Zdtg7MlsuH$4QCAN zZS+JY8_wo3Rof?$QDZZEkaEVMsqSHfBM~E&4JRVTCWezW(y_2ToUD<_Ze~(O#QjYK zo!Z~T_EPap*Ee*r84o9Ra^W54y_gA&Hycjx7}-tM^L9*tkFKa<1J-TD275+0lS>m7 z$j`2>G8ScB&aEu3Z>-INLgn1uO_3-q%DcY2`26*Gc~ZO=i(Zh;`>dCRm9_cBE6db# zNxLa|ZGK^XZGL%fe%-Y~h;TA&81l-pe0l!bJQmX2?E2j7<@vs;*OuG6PhGs&_Mzv> zmLq#m91Q!Sqv}#LU&Iz-wN-W;D=FwE`??EBakr;ox)+k7yIZ8fX<|oiX2F(dGM(K_ zrlSrWxu_zQPQKfSum?>>#MVhV95JdA`9og=Uo@`Z6&L*xxB_QYCCsn{hxOKHE)}h@ zervIX8cd-jab7^cW?ls|}ese#l(IRcc!?a0ec4H~`3M}1>U7)sK zeL=S0QN|4IMlNWTajztdk)7{=Q_Ji-B8-3SR!iN&)kh;AKDD(vPpK0-MJJ4 zn@ah2T|?j3_7-Bz-xzMOW=oh$g^T08-KU;>vh9@v29=$3IJFBI(up&jdaJPB6nx(D>}K- z305lfw{@QyA8#wh7(rVM-uk!QMaI^*jZ$z!XB85GH&fy4t}O>!G|leh((wv6>|?!eKnnsb z2zxKC^M=y;|+O2K}`?(s^7`KFf=-LG5_SrsuQ_-<7zk^-G^mM zUWFE({44A)GDaL`D>jQrGLxm@X0qvUERoIdX{2@!GOtULLegQI8k{MdNaiLGB3iX=7|bmP7i1%`1C)ar5<3K%h>-y` z-_3>Tt{l#0)3L1_Y>f`(awA#wm57P>Jb@6_VWAEIpH{qAv813qUP>ln;FJV#5Y+WWBGiOEGMiFCgJkt!v)$!Sn!Yc|Z8;1C?^>LS zDaxrY0u0zL!*T*vOygp5dz=2f0J`O-KRK5H>jm1=X)tH7B@hO+&m^%zA{a*|mCO=! zv6%KU@|FSOXAA+vu)Pib$6i8Q++@=wG8O`enfinC`2_V%OMfWI*-=}MZ zbG@S{wZb!10O<{FfD`W-S-6y}K_{Qi0+iK&Xt#u=I}9jLb0aEmr;~f70Gym#y?#O7 zGxp$I1skfd$7lu2@q(N%vir&O9hnf2nR0-YgW)obFP%Ni#8eW-8_p*9X4Wi})SxFA z()~&WC}|$W>(#tULGKwoxl~v>S()6t!8v$vOlOnR0(X!%xR(W6`;0+GjeAC%Rt3== z_&}6+Up8jQ_#W^j;*mjb%-?A%9JxccGuX^B+XqCz1X7#z5nLD-1^#y#d4qs*F<9pI zW7%C;9%ai5i2!CbGCPRMv5XuywzG0B!5(59r)ju>hx&r5u9j1m;_>=*#TP1^>mEHh zRUK}Dxy6ffm*v%1%AnboL&@AnwsTm6ylu*COTZ!fHGDD+HuETh5hhFC8uS^v=&lSi zET{uerS%$4rQ+B*Nv^Qr$6+5~-ctmYd*H^*Q^Q)1j(~AEDEY9{sD4c=gA#-&ucjzL zz0lD$dUB%hD6PRVnGd_Je~t@lCjYY8>_v;i+I%cbED3-yr91l95eTu@>~VI;`3e&R zO=&8Z*_Br|X65ZzdXGtD++Bt3&TigUnHAOBi511;*StQ(Uk-(U$LsY5^iV}Yq8=Z~l=5mAVw-{E!t~`dM9YLc&vuv$TXJQR8+!yy1AD*q9Nc>d$Pp&U z&~ix_aWHFY!~Gbs&t)T?DI&ipy+Stl9wzi)@hr%DDJ)EaQS<=r4Y3G72AA7NHhutc zHw=$bTCR@3SZU?eBROxtqX)3{RUkPPLcTAc`U~EU(UbF>JyimoHh&+d9}o_NT@zmN z0>P0ssLGI|ac^5qi0`mTFm}WNPVm_RaGaiPae$kqu`}$Ou;X$RYZ}E9@I0lCk_pHW z7MI^kMq_+?pf^WIfZ!**LxD{9=sw-2RREb6dt@*eQ1Yqv(UTYRk!sYYUV}_LM@Kem zxmJghz5F;x;&_rB_H2M~)(d_Y&&E=~0T;fksc&I%-64R`%(udcJBOJv4%SaitG_=(~748J;@82<3^+VD$5 zzc~D-!@ocL+2PL&{mjre58WC5mElhee{A@p!yg%59Qw>z!RQ zT^G7ecMWv@kIp~t{O!(v-TCvK|ETkOJHNT}1D&bPmpa!vpY2pT&vw4K^Tdh2=o{#3 z@BORZ&-eaz?`L}dMeoOZf3o+7d%vsqn|mMh-tSHHZuP#<`&{qadzIcNdPjN(dfR&b zqUR5Keyiu#dOp$fb3OmC=X-j-rRSBN`#pO-FZHbVT6=`xpD4?w{^I)Bk9HXWxJ8`=h?k_WfGl zC;NV;?<0NR+4qfo@9(?YXY}3ZTkLzf?_%FX-=o7{fZpV{hyM-W^RvT0GW=b`-!S}v z;q-8H_{HIO4!>>KJA8Kd&BHxI|9$9>hyL@>r-y!d=${Pz_|W$bebdkfhB8CZq3c5n zLxG{mp|PQX!M`2+lfmB^{Pf@_2Y-6-BZJ>D__c%Y8{8ZG>cN%4X9u;xw+@~h>>2p$ zfzJ>8=D@EG{KCLT2Yz7STLxYkxHqsf@Z!L818*Cc9(Zivk%9L9|Jwh%9k)AHJLWq4 z9p^h9>v*K&MEhU2|4IArw11}kpSOR!{U_Rgu>ITHzpnk|_Efvk{$l%c?eA#U+Mj42 z>(~WpNK#+NsrKuO)80^9_dv%O`xx}7ZC%cV($;A;3bviFz7L>LhjXE{wOfq>ZEe=~ zeh-|3oC{U@|C|d&`k&Sle(7(m?{$wV{SW6tk^aVNq)Y$Z`rhZ!q`!79ROt)Og(Ceo z>j|IqSJp>O`b(>ciiiKgxlpD5>Rc$&pIc9;(w|x1D|q6+I2Wq)r_O~U{fYI2BK^Pg z@y7hK*B7Nf9_$!zpIf*x?FBTYKN7AK>GS;^Bj!En5A8busPz8`SBmrpcDsP|`}Q5c zRQf&PN|kcU`N}r(D;H^v{H+z0xl`cOfhJq))o8HR%(sYgPIs*R>-3qSKo< zB>jSLH;CST-gT`>|A*^Zl|JseR-}I_^cIx=ekyw682zTLS{q;IntY0|e^-_zRquydhE|C`lFmA=LLp4RN&aV`|;o2^EQ^i9_H zUNriUbD>DzXf^Um-(Y@^1)@q{?_4O-*O`q#`d@2(Pb~L?&V?eqW;F^*uUg*|zkS8I zP^1S|qkvSjzW1Y1!MRYRyw%7reW1T%tPSAzg15fk`K)|RZ^tSA{ma(30QdWx&&qqP z7QXjb-vZVL&S&Mm)k531z6GG~IiHoB)k4i$--2;voX=|7`K;WvS}Uopjz=)M* zcd-4vAYQ50#VcjQc}iP%?f{`{;+68e(@tG=?qKn(h*!$8(@t6H={U)7b#16)qHT3; zWn+c*JMB5wrTR|SrLs8CF>W`$D%=AJU2$Ei3$9CLUU*WuEZoDsH|M%kpLJa-v%-_! zcR2TmuAXsSs&98)Dwmumy`i@W_prY`?Yh)vT$gIdb*Th}j)DQ<9*EEHx>R-7rQ#Ew z3~0hVNMow&QdL}+idT5je^IyxlJvMPm1&`=enGegsyy$yRG)G#i8MCm=dOBI7d^U1 zyz)(pSK0;fNSugYJt5ZZ@>|$_V;LZMz~NOvl}Vn)cfkS+2zyxJ9Md!*<Q z_D5~R{-}PX{ZTpfrjDsLQnq;JR+g65J#*^Sx0HN!Rh>KeXvcZ66={&(Ow)Ise3S-4 zQ+g9Yj~OpK^UGHrajsULUH=M#3w!4J;;{Ys>Wv|4#l9!&>L77s*24gGO5gXJy?guk z2XCm?{1EIhKLonX5B@ImgWhR=@SQL}XdUJUwVe+CWb-8d|L);FY4|sWe|q>sIQK6P zU%+MSrv-r)1X>ViL7)YJ76e)lXhEO_fffW>5NJW51%WS92*}+hM#iVOg2L2IDB*Y} zxyilMV(?4ovRkOEh2kk`@mS}H@ssESY6$#CeYtyh?8J$Y5&jO`MJvsH|E(P-PEOJD zUPrS)I*F22?YxyYRI^o^*-Ed~ijMl-!+X;3AHt@eAKn}Ox3KSj>z65tThnSmpap>z z1X>ViL7)YJ76e)lXhEO_fffW>5NJW*ivfX0p+7KPAlT_&^(fuWceYNU>k()XxPOGH zMd*A4dISEXsW&)r3a5K&Q=%#8FuyO=4Ya=n?R!Zh;noJ2(>{rR&*AqG{8C^5<@^6i z{8DDX5&Vwgm*NK~a^P|Np2qJuexJZEg%q5@?+|`v{8Co`Vf>!OFZD@%(6jU$^-s^d z3BOO`cM`u-_@w{^8pEUbeRDdQ+u6O0*nLE^A&xK_MkFQ0q#`5&0hfq2O@%X=Xbi%Hj+2%Pc5^g%M?PTV~Dy1V+UW zjvGUS-Fq+;isO*4uRn_~Q@QL##4O?;nSw?$eK)R}U6`T96jWv0H`0+<#)$T9(A^`B z5|K+4(i?bvGd}N(25((2;$Xx`dEC*p86W7;rz!rxD__$!K9=vupBZoCi5d_&UVP-i z%RmlG)nI~>LBNHJND?{i5r{>wkl&^v?l%<*A*KgGY!)^-8p|I=m<)l-1XRNvx$!K- zkg_q#x6Gk>l-oyzY2UQdn|fQO$oVaVjIkgcN+p4?EahbFUkWHka9I8sNu5pMA5-mm7p2wQL@4H5ULvd;O{} zASO;Q7l7B}4Jx`TE}2u|Qw?s9CK0tpcgZ$x6-F(Z1or`&Yz@H6Fb-5ncUQFs~$0pgA?W)S$Fpr|#I z6U*sbBEu=>aO13lN57(J{&Gg91wCHEua4yGqi1g47VvnxaK03eD#Aw*4~thBn_yLD zNI5=lZzG65Lun3VDg=^gv|UwPHQK?kVyF5h)97&OAVKr{f*~=gk}oC=c~?T3s;Nb_ zqZA3n{!%0;$oqpF7T5vU%3-@E+b$8@jO;71X!2q@OH8cMc2$wkXa~oN1cd(@v3s;K zj6^hr0BZ0B1i&Ki$~}Xxk4A^ZSSRm}$wo*=Zak^hSogzXJ^jTjbz z-R7Tg|d^+%O33ogALUv@18Z{GH7ly#Ka*IEHp?RJH4ZJ=@-E!5&-vA?U1O z(9kY@IDpioQyn`sh*Zp#d*Ac`$jg(wST4iQtV$9}(OFH6d5Rj7v>l10k_`}Ze!Xio`=Z`~Ex8>%k z+@UQHB$pjrvf|-{W2aZp>l#i^)5!#q5M;wCWavrpbY1La<|4I&&n>xD6z-kQeC80% zLRzoi0C&mPER_?(U6>168ykqC1JSaCH9B} zACQgd2nbn~x7*%!9`@gs=jN;(n(~gNlUdXaW39hN^Im2fzQPs^EfGpaMB(^* zvBakJLURsAv}An>WFzz3PBbve*m59Kp)Hns4u>{&q@!F)k=WXd}0$Bz@8E7ug~ z^J>Tw)U}|`JJ|lCQa=*>wf#i*ewQNcb)N0`^Y)Jpj`V-3@4xi*_Ye2^d-rus&VvHQfLL^l2ZSI5#EmX5B_8xDve%1r zZZlpSKiChpD|;Wa7%8$RPNJ$Wc);d!}-_dgF+5@A(<2^JL+fp3`Qs zswyP(Ws|O|1c-J_(GkH|nC?Dprmd<1!7tIoRSyeZ16`-h1Xfj++#5B8RS7`YD!jgU zRPvcSaoWttRE2!Lta36{f*}Rz9TkdCcbqnpGgX1&mqmJ}hXlQ$#kLpB1exW~Bb&&V zZGy}afL0MJgnY&|5@l+XLQ{jj->ae8^P?y8TLY)7W{Z_1_O3TtzSt^aQVjuWz5jI8 ze6f;5Jo5F=7~Ax$3afpX70Vb~z92{}{9k@aG!N40-qU98*vcUNQq3OQ7z=3{UXs-I zb5*&=8N((ws$_Fppfl}!+Re8r7gL9P3$;v2QJ}2uA<^OLR{u|Ply$S!k zi#{CB&#V0>#-x$nP(Ff$>*tG)zXJO|TnS)Ilcd=Y^MBA5l4&Gljmp9Ep%E z)fCiFp%A|5fFAN(PNYu0_(sQMN#rq~jq&Se0r_Gme+*tse!nmnCFiV7PP6_BUNsW!O z1VgCsrrM+#wwj@n|&U+_|HGM`L*Qi zLW`ld_sZj-fJbBTnJ-_}67=tvxiXo-4>$SB!63JPN`T+h46dVLbT zXATmPCoQclCSWG|rIyT%CoFql{<9=2!&k{h-HZW|`yqD1>AM)lVd30@Vg4|4kXK%9!)xoxT z8=y-Y`{5`o3yj8T_edDY=~V2#5f}Gha14D$LB={?$R7%>f$Wjn*eoVs5w-F`Upr$N z;R-B6vsp&FNkFSju?(zalTC$pfQuIEXt9nK>%g`ye~GXTI5>H|!9dUwHegk?_~r-C zw}EZ+R+Kaxws9z~!8U}JpwSXET7m|b)4v3`g%X_cp$3qSf_5d`0_Tsx6MgvGHT+}J z@TU;>|6{{{j>>ViL7)YJ76e)lXhEO_fffW>5NJW51%Va>ULOby zoEYmhD=sW{4V)Nh<2eGf_RjtjBfaLuiQyAx+N@TvD0aB7+uH|Eoaybg%?GXX|LcRx z)>~T;XhEO_fffW>5NJW51%Va>S`cVKpap>z1X>WN27!lh{-^qX?QN%}p4afF_2YuT zhj!Zsr0F^7yK?z6J+rec<-%T)MII!Ooq(<>3IShH_9&z)$gq+QaIJjrSqcX^J0rjM z?0x(VXnv3Pf_(M>pLKkE8J}4+E=7{styERDTdA7Hn;|EHX154A-Dn}zqfw>s$#ba`tg~= zoE7+R0H*nYE^`9zR!*CM>R`|PKBQf=MIEcR?SW?^J@ z8&%bvw_D4~j)--dd4XIDIW~1st`WP55)3#?!hRby5iq0pLJrUcqM{<-m3wkGaxkMR zYUQ3nc0aBr!iW^F(?knYSO5G@P~}~_ ziRShCY(QK1AVss?!lwx9L~MZy#daf&dN0G5t+005DabHqwX;WLwR2E{^f%7@%i1|u z!5XdrPpdy?WoW@r1z=h29HR0e^G=oV30ze3>kiOt?C{>Gtg;UfWP`=sj%;)o!a@?2Sd-graEG|0r0{ivKUm(7YRH+sJZ{-_lr4DJO4l(nDuv9fI)=`IbSi(jt{=b!IsFi4lZ6 zkDo+Exkq?Ot$d`g$@)7tJG<~AQk2dikCh=C+uJxeh@UAtQ70?4A(Vf?6Y?$g7?plm z;%51H44Ki49Xh`A>{Z776cTwM=^m1aZHHl^&!$nK3pv7yZ_N*lj$OZw@ti!x$8)_% zl@D2gJKnFlo!vyKj zc4A0NhU`~mr3SCgUc;A254ashzCNeKU=mpeS@t%TGYhwq={;nc$|1d5T8<~fX1&2i zhQpFyQI;@dA0)pA8M;g-@ALyq=Jv z1_PxPyn-`I(Bsp60bMQj<_AZ|Vle?vu>w_+6!4@phyCVSSHN9!9UKQ1tJm;qZY*_> z;n#zT&sA&(bs>Qa_zO_69p#O-h|Tqnk~xjg?@&z-Q~Hk%891lF#eY zmD18(z>?3S=^?M;FI>#`kB(isBrKUr`JAw1yhuF9#(`{`$RLW$iO;Ss$%+m$ueaLw zWy`=Ds@C+Va3&VR^MSH7e@XB&yDnvNX5CUL^RDaM{frh;vRS)_U4Q#)6?9{n)K!`0aEETS}Fmi(sz;{OB zqXWkZ!UCZY%%VBCLRuh3L#xJQG!@p(QGw5>iBE>}mSzV@sY4I5IxsDNz~@&3Wvjy* z^x(TdKr0O9dq&63pBGlg`TUNE3$$Dy=?V$-(gtLlj8SswV8xVxyup$dM+G2pjq21} z^0GSA=~Dwizg`XyzemvnAzd%7=etM8Zru_9a;q3|14LOvHqhiAYIKFucj8E)j5NZK zn6_g(xim}YjQqMtDtpjivkCxg@X6x;6vx@0+NOWNeH8DM`vy);wpmhSi{k{UH$Ey%iB20lX;mM@@t?22!TkX>7%V?uQK>o28_@K@j=NHi^s+SY z_xfJy`H}8K+iyxQcTRQu(-XgWBGhpgHy`Ft;a&N+j!vwsP<`fceh6PFwAskNHb1|C z>UbU>t1N1jkj!9%L8^HW1O7)6d}Inw)ooxE?CfT01V#~N?{yz_Xk$BWAkjDyWFuiY zQ!!)?z=igR4_fK;F(mj|YN~Spuf8YMeRHj)MQ1X_=MR-`+o2h+=JBa0`luG)ls`8* zar34C-J6B=GU#fTV;Qy~-H0uqM0|1|ih&G?R+Bdt7Zx{H*Eg3}mglD$YgHZ2jXi%v zV3Y81FARkQD`+YwBavL%tQOGN`|UMoX5?lm@d>|1HXTFPThRR^vzt^~0R13Tg(U+@?1K_`h>>B0nynVl5+jR|&@jPL)VrzB;?Owz)nxyS%)(d}Ri8 zKS_^+4DGw{gEitAS(z@pJO9M!#52zb_LW)+T)q0(Ro~1|cBF#X3~wQCzz?r`_Yo0z3wU&_C7=3WHvjn8_?0VE5!+N4 z7H$=nO2K^vNZQBtgyE6{Gnm2j^2!Z)D;+~+lbSc(Ko=zlMiV%LaM}PyZ&UY`amo%W zRznC6mq|g#nnO>Qlv$PDtsGRWX?fBoXX43h=7O9s_7Te@-!W3zDOU$58*G(mq4EpP zY#{bU!gz#bgkGAR<9byieFQStF(H?WrdZXLOlmrTD!m+XTuiAxYZ6SFpDGh)%rYj8 z&9BblhcmxVSIdb=_xOVTkWVZ4^JhmVE?p9q?4`V2tlC5{ijuEqp|g$1i>orZ5-Waa zLp{nx)mQC%H|>bqTeT;SfT-Fht!gi<2{|2l-~iZT3FLw^aG2J7K1eV{Uw&eAVsTM` z++xwu2XKl}vTpZuC@Vke~wSX=-9j zU~*V(XkBH!x!QplHYh~U>CK`@6icrw=pBK8EUpQ_?Zf_p5ExT^RD1qRTU2e*jF~nh z6f(N2JH0wf->0fUz5Ix%Bd!ih-><)VGk<36)bwo#Od~j}pnzD<2YU-{uDQTO^Cep} zH>n3zg~Bn~^Df9M8?zVWT_YTa~V>ytyekbSaPo2*Tdw{K7gcmYFPTD@Rz99ifTtyc||a z6dSYWE;RBxjWG3emK4?4QLWvZ*P++FURHO^qS=5wR_7TbvYTXV6?LN0u?PvoBu0|* z+w6pDK3P2k@!B-(klVR9PV0!&f*5Tc*erUD#xh{cR8vfNE_9pFByY#kdnhdi?K-Pb z<1|pbSgP>QwhX!q>8K18Pae>DOlSxW!1#!LXpHdiF`)^t=W0StA)+n5Fu%;IK9!Ob zV$9+wg+-@5%qxB%qn1>hodd0kOW}AN@_8&C1$1mTawz^V6U#cv7bgdq(1^=&q}F1u z4x=PhDjP;ECse;8$se*8?Pt&;#!#0jxsPTfuw_tism|hp?0~S$@_iL2j9@UJX@e(z zN$QraNrM}`KhyoD&gVNKZJ&^?;mbq(6w>*(k4`-I9O-h*xgZU*lX^XS7E=P$DHj$mjQ!S;#{(2%q&M z++Xz<$Mcs)Czh53s4o?Hc{y^d;QLU}qb(EF$va82*5K0O^5(N^i&w60EYGj6UsC+_ z?pKC;y{C={ct^uxs@K49xbH#&jtp?>0L?^W#9Yi{e!cOS_`o8LCGLf>u)vacQDHA* zc1vrR))sWk8B}XTi)5zO5(9z|_#W_m~#fvd?q#I;&1wtkDsNUHzb5z$m#$yNW=b)XCC_bR0f4A4dT3tjhzRIrzC z9vRmp6iH*}@=EHIP=#HEAjPZq!ue6U8hWZ&?NV4ZrD#RM4mfyy5ZK%=6eh zj<8%+Me~+fBtTGJ4^HloIA1xFpBWox2(%PE#fb-wYnZuS8y9dIZ|UH#lqdz+icU9; zlY}GikU5u5F!h*>>j}uMbvddyHVCeDHpC;)7zD{iWlH7=+60vNk*6+bs99ZjP@_6P zsL5j#Pe@0`pmH5DlKa3}B@k3z8_S2rCkjgianmm>z0zO8W&$2%iOcJ2OkAI#>ZZmf zI85pSYM>^}PdU`vq)MFIDd+;D>0~NpL^mUQQCvmRkiCtjl&`neD#(g+R8k_Wj%^aS^S!Z)Q7KcpB4nLVU`uLnB zDQMLKZ}d?gVUcmqtG#fR&ZIl+IAY?H|GgWps zoy_g*LIUJxz-XQ28Bl_7hAcT#g=TzYl7s0d^vYyDI5wWkK?JaNkiwk@fzrikDg>PU zzJWvbVV-eFxcaUFP5~$CH#o020#!%2@}((Q^RxOJ9Vm5Z)#vvkxm7u>!dWz+`+`31 z|NojaBoF*#|3>f4?!WE&1!w`zwf&*=TIaPBA3O1}q2C|6G5GPpa|0jl|Kt8!eZShL z^?pn5P|r)Jb*b>x`J1ETH*b>EJi>K+H}mhcR?OVO6{Nv~Sp*y@ zPW8B9zCv|!LxxTywv%AC_{l4a*QPV!Z8%;w)QVQc30sZ8-_Y2T4jI~}1*Y9{&Z>fU zej|9hYQ836MZ6HJ~)$4okRQ|@;2n!WC z_b8{$La3;_iSueQI}I%nG^^|!7lvCK40sNuX7<$waMt)!hHq=v=q$Y74O|nqh|$Z$pmQdXhWt)oRfUYTXJDvLA|Q_b;QG!q8{l(p^y%FeNgzXjgDWxE-9fp&KBU@5MQ~47%iqr}`AyV4oiy zzkOSP?(MvLUBLrn^&kq<4n(kL5sb2gS`cVKpap>z1iq*cc<@r! zs5I95Wd8l+&h-k__gb9%Of6*WJP)FA6_Ce2p3q z@mPfW8w*jHYvD|43yupolEh@)BVWZ?o1*fWVtDc53S1qm;CwRXz?T7@S5Tf`U|!D| zBTdR$I{WS$Tk`cq59#Sk-N&YJ+@ayf@C8%P!#G7@q2&31L&ySLx#?X0_DiU*p(r}t zrLLHJ1maZgpiS;x3_2(TIcMbA^(%`|zT>TF+(0>uq>KPU&wZ$cc>dw(be4)_;9DKv zBC8Xd3EJ8RjC(5k+I=d))YyyItrP$VCBEP~&L(>=hLBpgRwAEVES+T5HFEA=L(~|1 z3@K2U<2Pw5D4kR|OP4gvgVX_QL2m6hdS?$(6tHMS?(q8+ESjiYLv>}ESqSB5uZnM}^uOyu@}^$RlpjyivJ zpmR(bX$$2e6V#m8SkGAX#{_4f8k(rk#zNO`n&E9mVS-18HW1Nqq;qW7{JKtG`q z9<aWT zt=^9y-0_J`_R9*fn_0K)6$IK5Vfv=gqZP5zPZzn=?vmHsEkMyaX(eqGV(iw7!p^7(*B% zkBJE*ivzsd53uvn2>BjC6LDrh8z>7731c&)e2Zj=VwUNP*>L6#c28uFAy#XSmw^d8 z(JVO^#^Ye7m?DCl_buSq0EmQc^&W@H#g}_ zDs~5{FKvS4YsR_@hM7x*S#D8~McUZG!h!&Z9=0vwAUcd2_mPyz(E&4=k{li(tk61S z(h5j@g6K7avL(FJwMm@K*#fV>cBz_MhAZ! z#_;Uqh%|QysAQWrpC$6dN7>$22n_7WcruwfBD!9k7AioiX zXTD?2C!#FH1f+6{Jxp?-GEfQNB;uMPVQY_Pw2iXGYtk7|RI-}v?#jh8G`0X@cgNE1 zkinnr!U>$Y=y-vxYY&_a>t7fXogge6Gk=U!5)WnSFe;n0cuztnt-?NKyzy9Ank2RT z=*Y#w8G)r0A9=9olKgy2+wY)j*ZG{j`wvhCmjn_CPEyF|0m!qn(%wSz-AOvI!w!97`VnKP!lR z3UXk%G&mV~l462ES){tk%+OJZlA0nz68o^deLBy9l0ikqG8w z2N>FiE&h!?T39x0wOlA)(G*C9h7ir{)e4q{ZGgT(kXGhc_|;&~PfPvMr={-P@Z&@8 z8hmZw7f!sdC^q+)BAhJa1{QccTw=2JUum4n1crS`~=S4u7F@ka;o5ZP@SB~ zY(OKAtC>sg?d1}d_SimLGC6ZHlEgxYL7GgaFJ51qVs4~nR0)m}EMG+eenuJjb07Gy zTbV4Ua+zHN!EYsrEhvSw;7So&)8-?Bj^2{ua4IrO5}0r_w;sIE+-dC{jw$aS8Zgg2 zdU|52paYoaNK&p!gD#58%tB!TI|ZCj*|C&BLWJOuB$PIA6}9!x2C~9{9U$>C!A69- zZM|TUB@(Hj4TlAGT}t?tC7T0fZA)Y*kqf7~3Se}@OHe}-;T@6WUb*+C@spQ|OjbHi zQ0nb{^<2sEk4Dh6r!7cns~Z}tpvY_us+$rFV=nGrPe z=xxhZ9}(l{Cr8>^Om+>LqBJ%9-rfPd+~&-CNk~9A-X}jm|YNa zP7QA3h1n7IW9&sP7*vwI`25R+H}3K3C^;C`L?O-+7Awh`)}Wmcnq!*8h!|*@!jpta z?srHW*;GxMqBYazFvVmBN|rd%?I77g2Fd_I{b;feWc-0J>*!cZGJ-U%_b9w=@bnqI zKpCy7vWNAnD>trz_si?MP+!j9hsp*n>k)$yK1@cj2y&MY$?sDN6XXXs{^$&^nx))G$vaf5&vpwS)XggpkL5EwEV@WZD^UOSZShsif^#!^PKf+n1?3R5=UV2WDBmJJsZWMf-{@?^u> zVK8#!dtqR35Nv6ZOLRt@z)Y19SuW|A#kdNzJ7wAyPzg`2NyN2f96(7Y?=$V6xju1n ziQEAsS$e3NWnYJ$7RpH5Ja6aH14U;ku>(n~=n97{m|b{&?99WLTJ!UdjGfUA zDklGtq-C~)&M#k?H$?yNsz(PLc3d43)zU_$08_5yzQg!YK(!+hjhv z3x^1_a3&;3LhLO^#B_f3D^8zzGJkTUQWhg?A}i0XuWXU7#2oLn9LFXCyC6eP37v1s}^KeAxG2Z zYJXv;tS2ucC^9f$_c0~4dw>lcTdf*{mN}iscLOtNOf+RiukU7*?>nC49_uC6g-H;4O;>r zy%5{WOHD+Kzl$3H$rH3UbTjj|+KPy}x?*~~R$$mNci2~m-@4D49H)8P!<2@*m(K`}}sD!S33 zKL!Hgo=hht*sNSHHi2U0!z7{6)eo|+q7Y;3J+RWU1WnYSMBXY#0Nzfp!uczcLte~G zhGpOE>nj;Bnz@vNIFp$MS(_-tu{wDfOxX_4TdNRf02lo1v6}qGf;}6)oyU|&fvaw; zgN8UB98Zu;2~a;4tzFR=A0L@5433!oCD1Rh9CO^ggw}&s_b8UV>4jJl9Erv^8#kB> z+KpXU4H>A-!;p1$HGR+ zO~MSUCy-ts#>I1_qNCZ=7_KPgF;y^nGB;btaB`Q{_y7M(G(v*lYaTCj4Tp1!_w=Pa2@UVyCB*Qa>o zr=FTatvrgRUZ+IMM&_xfn2w5v?ZPxp>A%NSAV!&JA7yZ;zmn}@2-98II-f<7GLXu2H8c%+sQqL7zMq6WnA^h zi}1|AIPewA(g6h|=Yb|_F(HhEPnGON2z6ze5|)a#FG%m6%^@+Z{5(H6utee3>(_D# zJ|kiAVE~{`Bq7^anp?MGXo32fK3SHh(cK0=sKkw@o|=&_lOG>?-=#V|lpC7S8$m#z znrCPdIG0=?*x#Y%KnHV|A>3H~E-}Xj0vUC@$ay2ChZ7G#za67H&&YGwP7jhf^KqVy z`XVI;WUk!oP4~h(Ip{>-v(5Yy2%xib)?4O4#4N)B(CiE!@&e9Z(HPhS=j>aoyruw_ z4C^|Rq<+ylLVkppMF!^UtkpM912dh)5~m^XnOteAFGSS2XeBQP2ch+0u0FnCXiWheuo)0Z*?G>n48gzOAxH_!hO8DD zwilR$%1+b>8i0>oAGS~K%S23oEU&B&cJv0sOa(-kuRj68&SSJNQGFMr&W1)Wzf#8y#&@OzNJaL-#bcWQKf`^e`4Uo}LZIGuC*pJDz1r6l;&ArlErc)73&N z9fL=rxxP7g*PPVyZssCS66=^EV1rvc^_0ag*oaxx0UR(RITFsIGYp!Q*#>)l>8Yn| zzQ8(QTQ)!d`4M(;4k?jEFiN56Gv-S}z_w&B`U+hDhP2@L@~1tHBQz6ZJb>IdpL%Nj zsiy$)I7pYT?F!wnC7!9Y#u+hUGmB-|4vG#pkYEuT8POZW8UDRI3HfO!htLxrcqM(Uiq;L;`lw1!;u_jQS^!KBD4jup^25|KBDJ{Aqtr?|RSauHWnY zdFk6uoa;#9#_RU;$_sBD8xMsj>4aHp>%pTBZn5FO(QtKbWn%>i0k0WR63ZBm%E+3+ zeWNOWYX#J_m#VsWXYwtpQ%Z!n3KGE*fo9i8PwG<$x}xN_h$Wg->4F>WH)GLBg)V^s zgMT8$VFYSIer2Y>dSq?&QxtF*0$h&X`qk z%r!uNxIy|nLDUNmxboESL0Wgf8~$nn8AD-GEw8F*0Rm}(ULWE-?5vTz4T*-u>^#=2 z`JKXBQQ`CHrzsVZS#ag)yj=m(oHYt1Q27IMy<+qB-uJ;`ec!iGrzjZ&KA);EKMTw2KI^C<-%s_xCr31FJb-z?cpRFBUp z<|KkjBT#7zHEvZ0SB-~`5o9&qQWI>NCy3%CWt9*BwtxpEn|*$7!H1H2b-|{s@)BDv z3Zod^`|3iCTh+l<0o2kgLmIM52LdSATa2DLJ+Ytf7LnFnI4DD!vS8K=6`6!r0oJ_0 z8gdH&O#;R^8mHvj2Cr{k$~vOkcYD zqn)pHd~4g!w|$-T>(VpQGwpZV?=l8h{ZIZa6C)FIHKlH3imfqqYl)N|TGX2JLIUFo zn=l<(tULLO^2kK4&cF_}10Qz8m3AOLVtyy@Ju@<~UuVQ6+fQ+0e01XAK-jbn3U;Dd zlOG_T$tq7vLyD5Q^XE$xx)gzxjONG>)E#7YxTh@el${=sl9eULX>t_JF@$gy!IUt} zP{~YkG=NVUH;UL_m`aL6jfL%1Fw;=@C`@w};9!ceA;*;|HHlXR@v455lrI%VR8(O> zsYk!Jm^?i`k?$-KA&VU)(g-%S>j_HYW2&mS1{EW)@(NEvZ%EotC=`)|2X-L1$|Iy` zi)N6SI?A)vv3!(X9}cdMGNR+cIL=c(twhIyGlQyoyrBTnNk4dE3>Y8Dj|fXSKT@>o z2H0zvX)sKD&rn%e!tZ1*I0Z{jW@nS)*7((SJiL?PAI*g3n?V0$PVvK$eTwuTNAION z6&z3hP0<4>7T_qv{pE-Wm@e+%0o(vj0SAtX^60a-C0F>wlMw*s4AZNR-flTau!QcW zuL=2-)?p2bSJgx1hf9=~^lI3!^;e!59UEU+0hw3=Z1L8EP$^^3rzvQH+%mGst4xhe z&R%5DCUybmfVvIbsI4x#@?^}%LQR5%$dWZ5Y27+9EcQRhu!rLJP0^nTQlDb)Lm2s-q6GCctzyGy&j+{QFFh@L#*h{#!F!>7yiO&LU?fX4-Y$Ev|Z7uDc!t;>7W) zk;9+x1PHLQRZ3`s)6?AA+_d7Ko|{|T*nl2kXD7~PM+qVpuFYQA#Qn|HwfXh=E-Yiu|bMALXdP6x%*hDK;b!*hrfP#)xlbcKq|CE zLHKnhxi!$3io-j@VFqvRsEOe_T`Dl|nMXO6Qj0kx@yG~fdZiEup*3(lzrH%XymAAj znX!LFkj9aVS$@u9mtYFVY~h!ZcDRKz42OKbqq;r|`C^(TT-Hq3HEue$%1@lg+|Tvzm+_BoIjj zHzlaKy{bZa55S-}~ zp@s34N?diusD*&!l6A^Q&+ili=lO|Jz)%IF1Ps_Ek`nfu682n4Uw|bIUnLY%ah>p+ zdX^IEmgx+9N9oa7?3T<>!=s_LS@I~usDn9$%5aqH0(70K52E5tapx&ut|Q+eVy>gu zR<^iQ=G2dxQILq>q#dW7O64vw@+6LY%%_P^!jh38>rRCEC$i*nM|iX{-j2{=Q)pxH z;^4G(6+TB8(DCR1bTC}a=dRGn_EKH*phgRX=7-8dIft!`tZIChKhqRG>MWz0?7!wh zLBT%=B4B2vN;4EV^@ns{NPp$g$+7WB1k1!e8J~R6UAj!L02-CCn`r95wUlo==TK~a zkh*I^z!5yXKC(a3LGk&bDNrc9jep<0WTbrN~sdXWBooKzAZ+cEY;8#PnHm+=Fvp&sm49N4)>IG z7B1Uu4>^ja$fF4@-Qi~U5VUfZBV0L_QIjsDqwI3=ollHT+`1(wZ*LVZm0`J2A31U2 z4Y<D2I&0XBPFG-@9UI$ zKG}X@;Ddetap)GlcsM`BnWsl5l1X7@B@5w_g%$Mp)(jpTg$!RzL&*c3bY@~K*mPG-kzq|os-|CUlT7-EC5^o^I+djf`MssGj6Q=h z+|VrqHT{+617p0_wk2Ud=zcI?ioIH@-CFnD7^yBt2T6`z2l8j+TY?f6GUayc{%u59 z+%oN*x5*c82hLmj;e(kFa(xQob&$ksSQJ>oH?s}J{30=)m3_pFSF{QNxgLfy`3dZ^cDpq^H7dG zJ39hR>=ckO%Ej2xwy7Phsm4`N9Bitarcg-?oHICbNP>*SszlHUo##U0`+rmK=lJ4G z6X_+$!IGIhv0hjLk$r^a)7Dp$(L);szb3uZ4vBr(=kck&P*5qZ2Fv#0Tp7BQLy0bj zdZ;@34%J;#Xl!^z9%?AZvjP!YM=P+zE~*2iaQOE770q8fJ2N^Fi3lq&VpjKYVz|NG zn~)HjXj&b8O?1)}(rWIlO}z9tH_s5_W?^$GH7^25evdDpzyn}#;1f9i`=t{<)&A>k zU)TQ?J^vn#S^l9LLmwTC4txSPEB+Loe|B_YX^GrE%(&B~d`Ya7;`OL--9mD0ZkvN# z%#!hi@8QU9Ca|n`pWDMrkLZfG-g8aijVlhbJdosK2FZ3=S~J*;DcKyWKxY~%>ddAV z2ZWU>=*O}sUoJkxX!tbEA1FNc_R)#wo)gTx&*e*f`9mIcHkVCKbH^rfP1001eCymM zLscm}y`ee}Jsfb>d1*~(dI3$p;x88pHE6taH4s#bFJ2m*c==@knlBgLTe6=Z24(@t z7*NmEbS>n*vE2KI`^ryqwF2cFnnU_kY^g|HgT3{z&sB#1VS8%|F%54jhaJta>?=@1 zL1h^z^|!&kq9JZV@#+4+gPpgHjkASfsiBI4#hp^z*6|yxbKAjFbww1&=eB%kU^(_# zEb7~Goz);{pRn=XHp^GD+(~3iMba~aG!PW^Led^3oteh*meS(F8w3vUEQuY|Z&OXt zjM-ytb<&|hK>D6~^QJbuR`+2hTZf5g0mv{VUQ)sW zf#?Bl9!&tqW&|WLS&y0vuY%0va00XkC*YoiSR7;zg`;*V9YbEt?7Pi=Ts=i@b34-!i#zF@FiO^Q@y$f$+W z=wRn8{Qs{>1L3}FJ)i5|X#Yanwa$FUZ*}>-{!r+)Q z6AMW}ROulevGiXjN+u3VriF-xU+-{@nDg)+(!;W|2KW8qhh|e=vHZ7K!q`TL{}6+! zi9mP@tHFS;)UfEEQQ-Fy2nF@PgN4NFEsETgtCXVRksYAmri z1;qhX%+62v$~4v3jUGXoWN(&l+0=3yg;APD7|DoM4-hHY$QVsg4qYLAwwcNDBBq3* zmXPswjEh?al1f8_G^yjbaBs$08TV0nK1TL)ByoqMpJjeK;F9EF=1RiiQD#_&khGxc zQ%V(0ASA@b;f7A3*Y7x!?__y9s_-t=qgEeaadmAR&+}XlCIdxe4k||D2a0kVkJY>2 z`9N!7@hH!_NtL7!b4W+&^ti~X`hI$p(bXXd?40CL;@TW^kObp8UY}O%Ul~0UHiUh^ zFzeIV_(Xua!#S9X5u%u>ezv{VX^TXO2~zIVZiyt;LfdNCQkv=H;bW(n(bT~Pw1t7P zWGem{ZwM9+NrYidszSekXO9c$JTw)|HN`C$mJ@XYL#PR9NQ^il zm!;VRqywB14Gazl-uQ zpzT9E;zmTZ{goxJXaFwGe##P4G()_{h1F*F&#ENK6mreXpbk-xq#P_YFoGzM4HMSk zpzpQQ*G`|D&Oa`sd-&j$f}L`Y_7VpJ@)AjvD{g8)9v5!uGRv?#0vKv?*rF!ofiTM> zQ?3GBCdg1@5dsr~?Zgj!2ZN&S5TxZ0gC$xClv`6mbe^PuEaYDRXjw@L1XIj7S;c8m zkjKbrEp8wv8HvVGjDl*AA%hP-Vq)~c>5aLRA|Vy(BvHK;lmMBLktSkec5UN=%uY=g zia87$*nO(ry^LTUDW%FU!2}LE z;<;gRR!R=CkX5{hIa5{ao815ZIcf0Y{lC`xvF=~#x^?1q+aF2KNgo;B82X8!pJ3d( zu|LJ}tf!IW}3ab3t%aA;HpdvEK>qRYUJ5E} z2t;riYWe{>?w1~W15NjGT#UQAgxd6wYI0(@dO+Oqm8OpJ&yWYDA6dka=QKPvw!RMh zSle^qt`i_@BB+6d+dvZs1=Y4*Py&pe&C-IslqsvoZFvcd0!0*NkedZKXT?_pf`Uo{ zHLdKUF#JD}b(HfS8HF5XKwl|SfkZ5cl`W!*=85U8I3=`VT?us&1U}TH@&in*E;nR_ zsG0`|oqihu1GQ^@@^vT^@3s>uWSVhj0|Q49#$ zwYjCsn``se=4X-c%9J*0HmZo}hMqhw{8s1n4Gp8tYEv`~=I!#;2KADn!H2AP=c~uY zxL*tvDTYcjpDaCo`RfRAb#6N#uEy8>#Mc{|t;3nY1K>zQGGyai;5bmm3C=$ZX}Yjwe7vR}HH z>mJo5S6k(Y5KEm#S-BZjlQE2g1`2uMe1tG2oHOpGQ9IrY?h=ZIK|+a79!uNTh^!mNQDpf1%Te0c*i$j9b`I3EfY-~Cn4CZEiowCIAk zz)8pLnzLObG*}9!9MnS4CJjt-8zECERIg*@Uc(3U56U)X@c{xkiWs7H?gdlULNU+G zar9AuPja^n+V;vTtA+tnj%_E%M;Q8PUYMSPIFng<1pbr%zZhf2Hz$w3h zCJvgaF@De#3FJqoDra8+|0>Z8G|G&Q1Nrv5SVD`+>JcWiAIqd^1345gszXCm+^fQ& z5Ps4K$Wi$DzgPNkY3Mr!KGpY1&o_1b+=(CWxZd6={do7s8awtMQ>()D*ytJV{%&dy zo+}KyX*pE5ey465kG2srrnlgMi5wY7GfAaP1$~eat#jLhfcq z0aPafWyTAi1Eh{FbD;{%c##Cs3l8HgEWLa5%#|y`o^hqnUjlphUnKVln3jW~%=ByI zSr|)avNdm3glWwu9zH~CzP=_r5y}zrBNthTBo27`JON$t`F)MhGE)6dQ;JF(oRi*Z z1+^wgwE$PugCtvCzxJSecWjK&sja~pD?VOA zs5N!LHE%i~t+vTAm;X9*boMXi*k6;*#J12*)mi1ofj*1eDr?HwepYXOv{luf6}6}{ zmpYUH-2#eom9H>FhhT-_tb2WD^o)@cI99GW;U)wG_bwTzJ`a^bF1ll6XP```+6R|2 z@_kt|6Jy>-O;onpGS%9CvPgA%t@ixNdd49bXRinzrlaG)Y{T(AhZ!I{@f}LGuxwQc6J23?iB4HIDU9R{+FfM7dM$*Yu!2A(OH#T3E9~d zTH)!S%p97gnrd7bqs_eTI8fQ}>9wi3y)LI#U>(7Fi03Mc=I) z_b#w*^Yz|RXS|?GI&R2B*!&X?`m5~#Ec;M%V7Dz0Ea}i;R^M^Z6kiy*G%G345cs%D zUUPFL&VQI0*4z%j^i@>PDUAUD0r`-{3_)D};!1S%3?m@R^mw;a^9l1$^Y)MCpKvJ1 z*%AWHcXJ#VuNjcu{E!&?AkKsY6R}i`vK^VZbjX>L{1^~t6oEj?Q|;8js21>7avRZ6 zh8I<7RlQ(D$g5n~2-ck-9KI321lWM(tAQpJan`_d$AP5U$G!#zQ3uX+{x3VKVC8s8 zVTh1)X8&(bN<;4$i1htwZ?gLvI)C>>wmsR_FKtWzpyyY*zq9LayZ)9*R!8`gKl{Fs zu^r|kd#cpGf8oVx%67b(HtvC@kW2(A)Q~iQs&1li3DSZh_5lvbIWva|^C@?E_0U3N zozw&c5+1+??$(_m4rNs3-ci`c$!(Jc3C-!cqk-U~ROVqQh7XLfw}oTcy)fc! zN$yA+Lh*_Tle@vuoddX$M27h z<#GbNa|OjZEOU4(bICpAHH>8u_+bTDA&mgm;I8SI$lUP4*HGrU7hgE(Mg!>@y4?0;Uh)F%c@QGxW~gATs% z^1ab9R;0p`F(QRC;!=b&$1>s?9Y%jQha%VTN<$4-iqB$2${=c!yd5=~Xi7q^Ti$5q zMGtqaH8G8v@b`I8Iuk6fM86c6LAh%q4tN~{gWFC<)U z5Th15!y*JbYkV0V6;1D{IepI88lD5=WsQ%$4sv};P%R68#DaotUe`mKQoNKI9gD>T z*&^n~Epji+k_>O4G+5Lwi6<{^ASn)(6qZ#i@ootGOu{Gk&*oaYH#bYRQ=04KsQ4op zuYTIi=ElS|?nlYHiK8uQ4q9E;)t@5W3%by^LUSO*w5DTnZfdOY;NCaZBckZ&$=ME5(v4FLRs` z9O(IInpIicL)Hk2P2x{;X4v2Wi4|5KSRFhl0UxTLmThY&%ojqoM!&CkI^RAz_Wbh# z(Le7bCMRhfey!`KI~`;=7K;Q=I=Gi|$5RfT`YjxUS;X$I|Il#|dV|@(>JP?N9UoH? zFmN`k2Dn29%1=`$rx(OI%c~UE^KGMJw{8jex#f7rar`Lr_ftR!r6EbvA6^Yubbm!I zEhyL;Y*qnN4L*4s=xQjspuzFHUbaHyR|%@vm&%tN6&BYONRyA|rLj{_J}I)l=L;($ z`}42GQYolqImd8aI2fwL{Fw84qYyHcfXsQl{t`GUg45!ZOx0dvw98Oge}FZ)Aw_um zHKm*zqRt(%uF6B2pkQUW2AOoE992O-2aQq?M?03)P@moDU>_ zXqv)E&w9A0D$9lX;W|4i@@ne<4>zVd_)>7%t>8f83wQ#4#j7iY>8}|b3xx!Hh4Oa3 zbz+WGQ8@;|eJck=Q<|suVR6ZozpVoA%8ef`JS)GmCcONlI6&#QGPdJG8D6gr0lGN) z^4O`z9~X6VHwx=60$)JMUF-ZRes+VV65nCI)0Ky930ce+`f!wTRA{4Yr#&yud8!PR z#I3^(u?}+ldLZB{jfDwN5i?|ghD==Cc;6^b=45Ggc8Zjkhb7PiFosS%no95XaB!zb+jyYR2h znC}RKS5O2lEH;@4s!$^$KT`*-l?<{v(24}Fs^$%aS_at$tao)_T+bj&hx=aXFQuW` zf%o+7_xw?Jp{xDG&5j=FFNYorqeh%W@1QH%XEJx|3h0A3zCeJm~N+R(BUvm2dWGt zg*&c0Fo(gHn&gKH2haz@`BNmV2uFj12X;kuHXjO|JIu=t_ml%ClYJ=K;W|1FKGM)R z;&8)zosJbmX^2!WD>N3QSgIhj)QVPE%D-ilN6%V&$a4j|dN?Nur4Fsuxm^Ltb)Gm5 zY*yD+)p>a>#-ga65aJEX3X1@sh=B;fCok}2$26LAv_Vje#ShKRwho^o;puRTst%%% z#~#I(N+eq#3PBaRio9Kg_vPO_IyO5iFvHnmhaf!x*<6psbyt=u%jB-?WLfb}Ilb5D z<~T^b8GTj75t}j&Qt`$1Xg7htL^dcmQIUhyJ*1G{jzPYm?9qEhn2Ij!)vSD_%m8*s z5~-JEA_e@BtR80$U!Y**9~~Xzc@j*UH11W16O zv&bxy#Ux&3GmGppt3ftF7LBa3m`xBMSs2Loo&P@TaqCtU)#D7*ZL3Az*Z-e${_}qP z-sA_NkW-Dh-CS`|I@hsAj9a>l`)3<^UFb){gTttxqwv>psyNiGeG;NQ?=M#z6&-LF8PMUr84YJoSv{s$iK(oEO z>1vi;Vl}9S(a7DGDC~%!O_zq_2^A5!p*WM}>@A5S)^homoP$zJALQz%!#`=`*i^=% z!@q1)4AQXk$=8NoC^SC?PC`<+Yaih#s=EC1=rx5r*y{?C__mz85*j_T|90~*nHs1b1*houo2y#VxY={Lrw zZc8ZvM-RG21#H|~Uzc$Mu@uS&@)>PwM?=s8Ne~FDhof_`gz2lZ6Yq^jJi&;NZ**ZX z;p)30wF7ARtgh(e`qcQ;R-a`>Q>m>r(L5{ZTJ5Av+2#l~4!gMd`2CM>3XLzRIMAy8w&Wu-PEqX8#u<`}%a~Gz z$t&@Aq_v<2zcZQFFF7!d{{Akr1Hm24C#t}fWG&B$kT)6vqaF&a;ZJJXz0f1H&CO=j zPmeq)?>BbqtnYTt-YA zQ*(e)FF9nM)#xWrr(u4eldyCbE2b-$BmZLd^cIua&};zNjoI7_UpI9{jISrp0|UMK zT=`l+A3!G}NDuBjBM3%EAXew+&o=bQ$q$lA91%06t=VXO?&6BEH6&uAwCRQR$y5AI zW(RfqofC>qp=A_#fxW^;_q8e0Mwig*C}e2A2t7UqVr!!!Hiq&k1+*8aT2w$&im5*c zh!4P%Gns6qJFgM8JkXB&P!hsfD6nv=I^ebl(?>9kB@%CaSD)xe@c)^UkPr%Xl!!>H zX=Wu$oD@SkE11)XlPj$w{N869F{Z;H;rG!|5lm_NcnkUfYIjPiB%3=i0=#}#VcGxL ze651=B2xpxd!?qkaLRtHs{B%wp?IPx&I&-}aCM>yVS`0X9gnmU`gl8zOJvlzAk30S7aMY-RO|NSt!g_{RR# zFUS7;-J?tY^X-56*1rv`UHOYEe<5ts#~){VdU!Iput2IX?Df_)yKRqQl_wtjIE50b zT3rk;ICV2eppCA-Bq>}L6WvJ)VyUC@T3PHu5Gj~^In%6@F-S8~Cs*QVx{}71lW{dN z8MG^BP7nj8lPr87?fvUBgHk}loGmBFoA!PD|wFpS` z4W_>Avtv922R>5JqDY897EHRN|9F-aj82bkm+>x)@$c#1pjw0HX<`>xhwDt+IE5fZZP22<%a6fheG=tIqO_Bx!1I&b6H0yCTn=K{_rs-2oazsY2*xo6R}i}E%}Y>sU6=p z_JOjWZ@pfK`&mIB=tmdk0p&K07tlw(^-^p;vT*kM`VTNzJg5g9@YjQ_#~tJx6}YBW zR~|grda${ypgwYcMdBZ5xdq| zXO;$ZO;9Sl07hOaM{6hvd0y~%4VpKqnPM?i({|Di^3UTay_kP>tlydpZf}EnOdad& zn`gCl>RByFV=kwE+pybE#SVQd*Y^fDwtMSeDtVKf_Z$eop^{0Wr*GEcdJwvpJjg$~ zi2~Qn7@9VW7#x9Ajd6#)aq}9;)IT7p)OkaXcMJnEPtJ*guZ0+T)O1oltkK2!$b7W* z`}*|6lqxYdw1B)LzO1x>SUA2>uAVSkgxOh8nL`h@pE#JLjFjY@ROWLPa@E1^HV?(S z?Qz4{VDA}$dOW-N4Ymr4fuSgM#7=&YZ|o2N?71V%kbKAhL4{^ysWqYBoR}(?9dfVS zw0iX#dI$2L)+A5q@!Ae7H+tZxa~Q=((+RCsC^I})%1ICM=ebJ#*1!++#^S)Q8Uc~i z*n4Y$1Fxjd>59(BBOOD7mN*@kbI=zTRsR3Wz&B!7pI-U(<-|LyZ~yY*g(&9|2H#KhFI zXAZi2*1Fa~m$6;tf?GzwrdU2^D>}j-AJ=MZ3J8~9JVIZc-}`#Q-tQYm1ieqw#qVk? z$zDIN1a>BhB{BEAEu|&7#L}7QQe5`^MBgTR6wmdE&1jn!4IBOTrOz>2izQpf` zq6&xkCn0^`hsybJ*qFYVqU(dOH`_(l2y}I5Wb3uTR#}k4L5Jp@Z;~X^?EIJxaC|bez>p|lbgWhk4svJRp#Gr2( zff7_cU0t;onq$yO;zpW>M;&8 z0+J-d%7L1ZHIL>tDsv($5M@=i$0ZwpJop{;{#B1j-Y0aSdUbwyugtjKZj@=bfZ zh|*1<;vl^%&l(@Jsu78_UAlt|Fk}Z6(MJX`WTY8Nqa@~m<+Fp`ZBv)SB3K&i9V4LF zJG?O1GWuX67LB3*Rwp*lQw38R(KGEW{m#TxF6Y2T&K}}qm;mU^zSh9`W#N?t_nPWA z$Fw}g@8LhFmJ0>u*bQ=TcT5cOhS!FNK^EbO1tiEN7njfkt(}#k=NV)X#%Q)~=qpT} zb6}y-yy@no2nWTC%m5T5mSp}8VsNIH!K|M^-Q686TRtffF~dC59V^4U>9t{J zn8ozL61oV+x}6ntVOfArE!z6N{)Zh_)vXRJMVWzxqq27gE$ts5m@Q+FYOJWsAyq#s z$7s5g%b_pZ;*ahy_apQC)=&@iz|%0_e|?A=W;J~;j z{fCpmcpT@+R87u)_v~pqs>shQgGW`AcKMYJL&1&@4g@ehFc_5jP;+z%QU1>C8V=mW zFrvr;1MZhE{G-78e|1&6vOhNQ?oTfLtG7e=#RmTUv%sId_0NQ``~0DQs6QMJK9lnI z@%F0x)s4*%NB{1eX5kt^RsNlHEq5Q&YYIxrqo-0qGVZC8m8&%~5|FJTSDQMNdV%Lg zk!Fd0?eVq{BU~Uc^9pwG<@op$DT4o2_a&xI(Y~^{swN1VSjb{hv}P^+IObZNf#aDP_TWb`Pw_rvpfJ^vQjS>flJ4CCG)CY ztY|Z=r%M-e*6m;$cdaGKZ9p`iu2O7q&06gwU9s*5t4N^?qnI{5Ma%_~46Zc8@79*A z2a+uFDIMewwcHD~#+u8c9#wbEllfJCz zk(tZ%p-{{Uh56l~N@99dd?^ya^rV9RU}Ea&QFJ7y(UAAPcr0Wk|OXN=w=;0TDQvv$t+jlH1m z`voHc-e@pq<9^$QL_>KB^vwqtj$mm0P9r~b0_)i3_U*OR?4 zF#H=|8-9jgk|*{M1xzNA5!bjCbqb)bV>)^!M}okm6;8aC!oesas#O}((<>){a*;8X1QTFPq>IUGg-8=~{DG0+=d zh(+hmLi$~nVmm}mGwtZAX>cuk~V}JeD!;8-^|NFpi-ucr@#dmi2ukh)=n}0NS^z=mV;X_I$x-DRFKh&+7IHy-F`>^XdBcL-R?VgICcAoCu9)0hAFqu&YJ<5SU7^&!tny3J^fDpXk6*|K~Yk_X$LB!+;lBG)!| z%lMd)Cj0{9%T6QiJ%%(Y7pmB{iiPCgxZ>dy9wjt zZ08ibDfz<^vkJ87&8>SOc9_29?=BYsH@~lECW5zbJI=;!yZlB707>v!D-T=x?s;{bbY&^kjh08lx&95|zca9KLim>trbbMH{QQmy8* zwuf{+3=JlQ0tnY#(sQd?Z&Zdrn_9Przf%rl&)xAj4!bEn+j}pbl!4Ci2xq$!zZGR5 z%LcvQk_$~u|7;?-wB(T4OS;uITrtsbv^$gO{#z$2wZaWbHg|B&ArRqxe!4HI4~$^4 zMcmB-lne&JqE~l(srg6xvx#6T9t5B7rdtkyHq#7I zgw-oVCT!kwQ)JhJ?1lGt+d>~=h@p2UqD$?5LYb${nWW9C{tqUC884<4M{De@wJn6b zsyP^As2oJ`XYRy2O0XkJxfgZJ=Bd@tG<<+tZOX8i*C1aS0(st2VSuH3K^=i4XjxHr zMYuYGn39dLy)lXZUl{w}0sQaHKW`xL1_Ey&@CE{JAn*nPZy@jn0)JN_@Q*KtuYdSM zJ%#Q~Gn4d0kByxL|LL}D7nz0^Hh0%o?}m4_!pT+E@IFE?9wlikhhm@nQYN3PtuTF! z7g1m?7g2d-T|_!MT5U5Oo#}Qe7mu~7VU3FItf8-C3=giJ6Y&MU##%!Qg73psrjksjKmb`(iPlE%OTUH{2I0r}d+9 zDO9cF4taAl-1gpznZ}C8;o5gUj@{JCnddnq-tp}n*VFkjW-`KT;lqjcrwLRGbZ`+= zPEp4HN2taKBVDqfeQN&Xgx53M_^9OPI2)7?F!8W*EfQEChEP9huB`?aFlK0IG1EX*T0 z&^K|^+=o*o_2QV(G12{?sx!m9-vy+|WS+xCq4Ir=d|o*_#&IKy#5h5Y9@R*KN`cXa6-yI@T>H=atWuiiqURa%uJmlR|_G9x|GBGv;2!(f%*UMT=;R|{pt7a zUrt{7{Ng{s4{!b%5dvq4zaP97YCeR34F)a;Fx~F*nd*qe@pgd#Mc8gGb5ug2`QfRS zVwILed8KhNnz@))GDl_9?jwmDnQ!E_l%K@kgIsxs=+k!BFl{5p1`v?0>qxRPlFCsK zhHk&0FAW1->Ug}U@fGEAfsVp#xyYfwLUGIsM_Oh^E+TK(pg{bMY~4YJwm`-!rOQk- ztWjiHUyy1uqDthTy!OFK#zq@fOGZyrhD1;npv%} ztJY*bUCI<1SrELj9@d}C8N)WM<>0A%#xAmD+zWK!ehxeWMS<0#bvVjV)UmKX20|)j zd2E#0G$M{)t=zaC{7A0_1J_ivXl$(c>h z6;cg9w~{&1gpx2*gWOECKqXVnVMzI%AT>#QzW}Y3*tOpvlB|F)hRw<$qLKdb6f-_! zAx$mCYZbt$*XMx`wJ|I268I|%T4X`pR)c47P>qi&`B{6O$H~kRiG?x>OtK9*r2I0r zWNV>Eyoe=16_y@ZLci7#QE(&qx~L8nG%!w-co&oa|G5y3ntKV%4KW~Wya;iK^a~kM zE{{p;9%4gHaT?-O1)b@p_n!l~2d>w4NS5(#PiOb+jkTzic+1FIaJ*060>(b=E@2qm(~ z)f(c+guiUS5^t1>JQ@H4xB-VcIgdXm3sMJwKbLdS`a}zkZ#E~L^80za{En>+BDmYR zxf24vy>i>NYJ185Ka1_hA6^eGHRqfx@24#cBXWsth((v!ene}KQhNnNNPmoy)s(6x zL6#de5H{I0SU8mFd@0>J!*CC|$^4OR1m;KV9*xYnH$Mxl}Q zUII%~MXXqBQD+pqO!g_e!r{U`xYeU-xuiD7<7eqxO0cVC+3V>d)kTyyWG-h7WuvfH zz-+pU)HXf|-sC*v!i_C<<04J6Fkhk_eYv`ST8xo)X-+&^nCk^L#kM=;w}4n+*#~Cl z!m`Bv4?GTx-G8@n>G$6HKY_>Z{f7~a-$3A%b}JZ6>UmBFFzlFLK2&eD%-=9(bjV~o z&1}MCn87nMcIyVJN^(=^$)oM<`=K}{{+g)W2Mt*rYJ|h9l5TVg%GF?)R%sVyQm5zl#+f%IjhJa>^j5m~5?WXo+wl z{6d@F$6?@*NfGysmHZ*n{@|+P6OSRxPgS|My>9{$0{uCQgd4a)u9FpSY;0)L8|mjc zZNr>jy$S>*@(KR(;JBvN!`G$v^9Ag3L^JH{ zgX!SpjQ-d`qGxIlg6*LRuWqew&8@C!-`94P<75+7SXx^VTN`b{@|7IyLK#3wj;)mV zN~3n9{31+KLXUcbIH2BQ^}rvT2di5Oiy0;Y!w(1+1;ihf3)z|nNxHu-WPo4YZS0#X z3PE&W06`w17VYWE{d+`_#53faEF+#E_`r-z^Sd($?i?JT#Ikr=ImPYjM6&M_?#|O8 z0{n$?nt|SFMPM^RI!+u5y{?oF8wmRtAEPnJr>B3Et`^gCALH*N(N*)))|Npupf!Xl zv}v}W#LyQWA866pYPljW-Z23!(uL%A(NccDfXZixQ`o%968?#%2O*qTaa8aQ#qZh zl*R3ViJ3JS@<-a7nE{&axW8>!U@AJKXt>*Qh~G!6q3C>Qb#-fZm$5O`^};Y(IB6AJ z8}7ry2HoGdAI7}Iiw!u+}m5-zQ1#C{gbWTr{N+lDI>Fnp!QS{VjgwNXF9YA`r2M6e6* zmX+8daaPoW2cgJn{*qUi4vBz6a5E{jB?-ZSk#@Pz( zqjwGnT@*R;b(k*uWsbc<*zOotAlqpF?BvC+4ns2o!I0DPx5u$Cs)Ep9sFW`h(kQp# z>xGX1!gwVxVa^dxSJ9k=_AG`m7+ySy#dgX`-fLNL7M&WoDQPnU zNTnp~Dqx`Vm(mNGR3Rk6Y1My=FSD7E++rX{ zTmw1ca1g|*18Lw@Y-Xo*ZYnzf)T4B*vX8P6bR)`ZNyR%DNftX{Yi`>Ns~hcs(DMTT z1f45CzL1Ba0)*LscMr~Ca4Agqi_Jinr^ZDn0(w+qAp02|R1tSvAeBiU96-y!%8V;G_;Ll6&E^%gm&S45{0nyklo40{>qDzD)q^&`NXCH`DxW6dhz@Dh7%+;zH32yE| zL!N`gP(*PHjj{4`#8Yh!pZT#(6IQH&;{riRTwmBAN+=f+g2Lcvsuyw&@bn|YZTwl! zEDAk@5!Im_$;`b1%vSwR1FRKHsFC$lI9Jku`U1 z#E`Wx`pX|Xe(#E&AY5f?8fxv#*lz!C=rF z5gQ!fT;;k6lRWe)V4PrZ)0+Z?8ESt9X;Y$;0DFSLXN55p_k*g6AVx)O5CX$|Dr}J? zQktF-AhhqmDx`UXA?u)XsZ+$92mf^{}%WXKm6AH^Tk5w`iJYyiw%FKfm!aVXJB@AZXeo)b*wNp2@+? zt(s>xjE^ki|DWMm*)lG`*_BJ4MQa6@uQAe%r1Y$|}r zIToXC6bRx3;7JJoqcYsu){{U4vO%^}_;Y(_7ingx5n0>Yp1kc>@!9JiCiNdVILE6? zzv`0a8V7@?PA5v7>7X!vXMznDNe>+l&K@>mh9Xf=DxuMRkqBzj2sOjXg@e+EcBS-8 zyhSYq-koDA1q8ZNZsD-18^Yjm#LTBOo2krTPY`Myd8GE1c+NZhG1PmMD@M8B7bD-J z=f>n3>cyY&s3|Hm_AB6lqZ}y$fF%_jCsdrDQtGi0X9o+VpQ(zV2LMQ*9o35k?3EhZ z7D&tJU#egWqqNq3pyeUunvg^6lObXK&Wu<9%Q$Q9J&-WK9FV_M5ZhnArnW4!&cs(- zManDg8anHhUkMSzw@n7Aehkqi77{$%wwM8KQ*J{8koa%~DGzimI$$ZQGhyjaDsZ<+ zhFQ6XS|$wP?rFSe-f4af`j3!;!TCBhGK1+#1j9*$B+NbJQc>G~rg#f!le1DC+~Ed) zS92*>gEM!LZ;C=eNA=1JZG~cJRbm7FIsbk+dkUBs8f820ACYTa^yFG!+?l!l;i|sp z;96%Nei0SD%$1xU6I+%CMoiXKLp>4xE|=_cE+X5ocxP;D7j8f=V>hhbp|_5-P6IT!2D0z6JC!d*nNruK2msR?+hW=>8hUv1yAzt5)&Jo(~0p*tm)9c~2{ zF#UZMn-&*M+ot_+Cm}?|6dYuSiPs=J#TkY6LZ(9i_MecU#iRUugxfKgo27Wt z+}0y`yLVc*%QZxq;WFz2fwyjTRQOmPX4__ZQ{V%Si+1)^z?HGOQ7G!QkGKM6bj)3+R{R4y0R6TezZWLP(GHnwEy3^Z@X{y z8BH4KU#3;&zPr5p&N=tobI(1?++BwfxwyEP%B)0lq9Qy)u-Sy?ilQKN{e>V1llXuA zUoU=i>jnJR#{I7G6JtzXe5q&P8-mO37Y4r9-#_rEYr5}~J?EYO*8Kz?ZT{PYz|R~6 zN+T}Ewp(toU0KgX&n4qanP_@>J{et4<#Sm}VRG;BnduWV;)&@!hh{`el{nt#N-T)8 zM^4Nfm^m&UIeJ1oa`MojonoJBHM$ZP@18!s_m1h~AU^ZTAAwHi@WfRmVE1sCS2k+wEcQFxPOQ$lq%9=!Wej&aX%_no> zo})((%}gJ$bc|)<(Oi6C9*kojKJ&!v;TcPPKD|)4{@C%^!_&u4iw9>;i{ps}_6hrs z9-o;#aD<+Xo1e2?JU+93=J?E!y)$#7_BhM>h)0iz`(_T!0Lpu(=k`wTo9WwLxW?(& zwpXzgZcnT(#4jZlF3o4+7v`4}*<32K&VFBvXR?3>9_JI8_)2^=r&mvCIGa7PZ{{Ac zPG5R+mHy&5|1hzzQ`FwI{SkMsW1FJbUU@qs8@`z~wJMKxSV*W!gF1x7w+2b*Bo*{q zDN37PRa(p?a!HdIy%!OX5;$Xs7E#z&E>N-+piM+ z=GN2ZJQUeFjc6V#D;JJ7S<}<`RsxK_8QxGetvTAiU zvAUGRlFnzas;iH%1Smx36WMG&KEIk@ITz3H09R`>@pQ@nI-!=nti}gi0p+VAT}S(=TfX}nWoM-Bz|!1;m%T5w_}?m*{1-;W0Xf%MwmOm3Uf7vD_ips)fr+2+vEE&w3vj>~d_|wafO(Xa#>}l*D77 zJ+NEwSW&HU=2~pG6RfpNHfYFMB`e>ogYNvtTk7%hqph z-8|pPWumcoO|JKpGsXY5ZNk@~Z9?6<%67*x95!Ou8y3QJ3h2l}H|fGMTtks=+TOeE zZ*y!L8L^cTY?hW9bNd(R`0)if8c$;_uby#@xIgh zUGJ}Zf7Saf?;E_o=>59))!vtRzv{j0eX;io-b>z$_bdG*a(QmJ>_M3z_I5?!uyr+CZzR-256Yn{Z1b z`{dw+PnnQHqR&6&_u>C017W{}=QV2zL)Q$Bo~l1p=>+tp=hc~EE^KnS1SXb-)^#J3 zQ~Yu$5UDmfwWhFj%i!=VP40%RHQT|%+;S$B18f$=TuOYg;=AKM17MtH zj^Ne+XufdJ7YS6Gms(S}?rH+sNNxNECgr`URopA(XfAsX@0&l4%i-y{8Sz{kc#Vr0 zT+-wEjIIe2v1qaktATnGR3ejM1=9;vOwgz)Tz6H&2}=9oIb71?S~!`C;huE2PfV9UM*7TO@w0XLcJZ(#7OzJSncw#V#&gaSIH}Oo^%0 zxKTGQP2dXGAWm~N`y1fh7Y+nNl?w)+pw<+gJuo=BR)3<>(UsMNI=yr>o5i|`CX@Qq za80Rwz`QzlakqE`Q*V8;VItD7RJn>2y`~WG3=J0?^_I7v7OOrt4VnP6n9ZljQY0&; z=v$&-mg-&f1P}eJ0pM1sAv_s~_@z)4YS3#6JKe#TldQa+YR#lV+nvKR94d|X_J z$5Qz;F{i~$YDFZ_PY}b(FD(<}iboeL1Z>iTQkN#elSI2d;_+7A4=}4Wg~@)RiMrF3 zfa8gC-~;Dr!m(6(T}&;C4EP!&AQHwUgyoysoUktv@(1N=6IN>q5A+QW7d!<|9qynU z2f!v$tI2iT4X4FeG@Z+5K+0^&@r$@C%reFSE-M<}#lw7~{Vm_#3Vnqp!~SqYuCj>r zn!*m(;OK>V2$Lt3W0mwHuHwTp;#xF;k0;^b(9Av#XO@;9D`sb+MQQ1`kzY6*02eN#BD`X#>3R1$}|jP zCKgn|WigR~s2sf*O(fY}zWR}5KnRfRq{?O$oK$NHWA=t~l#Zt3 ztLADba3rCb{m{)5G^Ie0(7cSJHed&%AP@Rd# z;)#nom)AhL%K)dw_G!-GEUovjd|fNF9RO|P^TL&1sMi!nZNpn93S01rgSh>t6JcSm zdPvodgp7FS5=*Pmq<9Qh@`qAu6R?_!ug2C@9?7t>(;_$QWRed|UjP)6fT%MD-Cpd& z-|@nW@%QQiJ-MoI8h>vp9L3*f6rO{>Hxxqn>n+@jzhi~#@OPjvfWM;!vQKbj$fkbe zJpS}Q4}XS_`?~R26_zjQy|{h{u6cE7RvmEDEz zYfZvT+|o%T1{Uu7@YvyRU=KH_+{!1WE+ zmt9Y|KJ9wU^*-0zU2kx`+I87=$+hY_=epZ<(DfXbibl%{6rqkv4Psev1Pdfgp{|o(}?f+Q+`}-g5 ze|`VU`ycGT&>!nR*+1KVYyU+5X#bY}p1vRUeY5XNeV^@nyzhg3kM_O3@0EQI_Fd?U z_nqpS?Yp&aSKnCQ7VnRsEBUhbbKZ}8f7|;GXiZ+>eF%Dz1@ArHJH5AgectQ6*La-+ z|2FWgfxjF0{J&IQ+?)qxi6J4L`daUa`U2p69 zrLLECJ!S$APa9Y3zG0FtOqp{-l#u^MooBwS&)R+>y7-v zFYC{tXc2zNEJ(uZ^hS#Ci~4g~Tdy?>lJE<9BUyNj{+!nA&zl8Fc(vY05?-Z0_o30x znFUFBrQXOVyh3}91tJSCHw%*RGOZCv|E2nKV!1Cd3zG1N-Y6_QtUo7yd&MkB!ezZt zNGR#gag!hv&4MHp^hQD9p?>?Y4dC~Iw?1h8mR{_#Z&RN?pg#q;Uu6E4?$=xR@6(?G z*6ZeP>5|?;S<{~a&=<|$QeJN%=k%vwTv_wCoH2h(7xdOry4QXcW|#8VM{To5=D-~R z_f@_mtx!j;E@{*N^vZd@B)^a^NeQEuFS2ab02fPqNm?}9h2v&TXi{0=OLB}aN$1R7 zp{Q8{{M^fzqIZ5m*kUt zNjhQnQs&GWAoMt2lI}9w$;ZqZES{r$NjhS-lMXxWTN$nn4cNzQ$BrL8ag_Eu<)Eb~ z-)SjIvu^u{(fAIo4kUEIQk3^wiqZ_%Q`*PXVc*+pDav~+MQNJr>3gnOM|5?&r6@ng zQj~U^J$;eexH{}_w_1wIl%*&~EJZ2I4HOP>bs)Z=r6>n1Maj?g3@Kb4q%ql2lqE}1 z@^L+bySO@#j*EJGnYg$o~l*odVlUu!8!!(2~ki1-$x>OoopbdI0!fpb5L zFG<^IL1=Z)G;6>dw(=$E8naz+i&+CqT+NrHtIT$RXP7nM;$FTa4Vdlx9?QY_H2`1_Uy{4|l4Pe|IM;2V|1u`-1Jl6+yZ23k_e(XDOV$?Gbs6 zJA_%m{Re%|a>jbU-1E$?tthtsuQcnpcJyqaf5fIb-^16U&|f6~2hROoo^0CVt69%5 zaeOr;W|tAAa)EQY+pas6*vmY;|Gt*b_i)XFW4`ZwLoFd!n zRg0fV?G4v4x*|@f`h_B$!mex#K^VxDU2|C%Gx^o5N-^!tvjHBfd5Qv$@WJ0TQaY!V(e!lM#5IDsoA- zmm{InUyg*PePLVmnJpIpntoA7k)XQJ(#k-Dp)_&~z*!mrKw>$#kgDsW+3#YkQ}f6r zulXE2g`>IHGVY7kmXV2NEsBUnh{MnTY1Mp#32Dk?BoGOQ16OY98YbUhQvfJlxV)Q;8p)xVG z11{eBriN9l6D2!bsL4J|nmQ}CPUR+Tfgmg8>NcZBn4^MTHyU}x9L%pxQ4ji~Ml2%c z)?9obbEE*t(XEA>GCK#)QV=vfU{;kVWMbxG`N{ew6 zUnpCj9NCB}o{4WS=9373L+O23>L?uA*pW_+=afh;@Oh!Y<#ED8fEX4LLtTRuFJ_%@ zBqk}{&jN)LA%V(FR3Q?jv3y1pfCN*g7&ECgG(%b=7HFfv`U&Dhr<6rV0H2o;%PgRT z{XRPXJB1-(;IupH`jYd;-fXwf)guhqpIKV=4vnT#Tg~%)s-!9gjA(l8MBi#FTvz!f z?r(XNGQ75bZdappuq~G;p3tR5>@rXV3eh)c72hq-mTmH@IAI#^(Sn1hM4-p7%CFKA zT#7H$B?3K28P7!hQy_u>yrd|wS*UbrU}$u0jYIWX=`NmyPWb$j?Zq!^JLp*j%G(<0 zX+?WmGwvwr^^k9ss1Hv1{b7F~ayjjRs>e}q0QoE=AU4qAmeT!Y*k@Z(yZnAbohg`H zedP{F6S7}B!S&1y0-Wru*zz*%7hAi1sO&#udD^k{vRB`}_gL zUz&6ejb^jlp*&lhw$fXBf&4ZQ`>&kaC~b_@F=}9YN=?;p8#8NwuxgPEX?uW0Vjzf+ zd8J@KZ4Vs6)`~Ob2x~8tp+$?@YW%|rq_s8Le-ql;np*?JRg36Ih=a$qFQoWGS8RP? z?iA6HWA587Q_N>h1NKuB)3H9ASBTBG1x1t1gRkJcUlvZYi4dV3qY;WXS|xlYxdDqY=QzWsN+z>A=lulX+5PYU}4S(xB z^h0|&+kpP}X}gvoR&QP)rV!X0*55c*j-9@%AkxukOhv*emqO=m4AT);+MTvO2`*HvOC&IuD z?zi@R(eYsSZ*|=-{0ScZEdDF)$P5k7&lAmUL$pWmn)%{`I;ZwF9u z_l|t%=TfPpIG)So1h~rM^uY!_M)0Ol%l>ff`i5j z5pF40w;E7=DPEgHNK-ysBuFyCbC&H4p(z=V0vU*0skYJ_9}Ii7h{)=10mC;GjdTGVzNGHGzt{9yz zL6y&oXHpZnR6e$>O%GUD!X*dEx2aXVA=yO5GYGi64i2IbNn#ay3^MX$^+vdEQlUU< zbu3YqH)AEQ@)>nM!Xh_Tw%!eQOZ%}?=zZ@ z3EUl7p8^_$+6?LaX)9bw(|Lh10x>A+@59&21_9i28l;e>hUP+6K`N?*^)q+3#RLo_ zISEIqEUv($PeO3^NNLaN(CGa9pm~EeU)ruOtyw+7Ai{*9sz0nj5q-4SNfn8=11bpLolSyFEbIJO0 zx;z>52Sb5MIUUhIBeH^gT&3qFhld#k^b4ZG>7ro^tf7F|I04#NH{$q0d@-6&=0w@I zlHJ~@o<{&DC9%=PGX|c|iVfibDMxP(=GruIcLJ4IV|9U`uD6C?z%FGdOPx-t^|(@l z<)uXAuu_%`3d;+z$HQ26k&wsk#Qpyd1k$8_PE`NivPL)E)P6& zc=*g2a2125v!ycgwotPsBgKIKn};_6OBo z2P_|BSP2Dv<=c#(wwbU`l0spS`q^!Pf>lA zRB}%O%F=bR3}Z#)34w^dq@IBF<~zm9z1xOHS5~+^Y~`|p4Q}W{f%#T}(`tL30L?s9 zp;m`686~^dZ=Z5VnW^s)F`ee=bYMz38=xB1B1??ru`jNO7o$n!g2T3`zercfGL33k zn>^fbpfrFwJi0*n*tm(-r7~&K2X2cTYnG&40Z^GA$;bRsI8<8MN>|^Up-r|#*;=^U zIOxyWo(0pk7BZB!(Zw{ZFo_}9#yB0NhgL+^)*xiaNgWbiYi4;ReETLtLB+3xO3%E8 zEzj3u_WGu9V{Y(Z4w^`dx3|C^ zI)>d?tTsrN@2ik+8=$xiZc~>5llxX>24TWGQpW|ua0x2rt}efoES8}iF~6}T;xyYJ zg>X}ibe&uIopYe6-W@|5?O>_}DQN;!8vuO=Y-%R;z;0weuGsgWv=2(K0tl3BS5<(W zGMaJwUVvPKn@Eh@RHK?;Z>o1k!QPZ}TvO9(fP-qOpNaJ4$pB^54PV~!jN##x6}mQ| zq~6dp7tWaKF!cb@a`NXRbmTwUda~&H#~{!Gx$8DT4JK=(JRWp(YP@hehF%~fPIsb> zGG(jn78h0a*xYx~!KbOOX+Ra#Qfw1dAVa+pz`;kg|4#~@o&E1X1c2xCyu|)Q*EIrV zFYNfg(oN4Rmwt^g^gqUGEwj4Mbak6*%F z0_h-`(z}f=v_F?=Xnp=R#xaR$W4hYfcff5-svb=QV6p{%pp>75gHwt$841IyDqPxq zdT8{)2RW(r!J?Wq6US8@Px7JZ(=*4X=rSfn8GPZ$zNTVB9M3~_mBN+7rR_USOvO^G zz*s(>ZM$PtoV7jLQ;)f}r`7;}xKa#K;!{5JcpykVJrN~>l;kDp9`au2@V8#vUB1qw z;|MD{ef-3fc%F=2tP6_W=4WvS&eU4CScKi8isJTqRmEI;Lp}9qYj0i+aHa&qq4IRz zgfpy0$|D@UqMRayBzGVwZNF@&2DC+YoOJqRy})jRvQwfqRowQ{V%#aJyrRiwCQ`T~ z3VV~LK_{CF+?KXQC$upetO8e-?J^5~99)RD!3;Y=Fzhmg2R!t!xo9T0Q{{L&2_K2Y zb#;Sp6JWICw+|-j-3D+PP^}~d{QmMAR!}Yca{cfs43tjYU18iiXyr7>GsQqPpNuc; zf}IWh*StB@Y%^#l$BtoKY`5M+m-x%~^fq4J34);mS8bcT)<4hCh;+a>&o>od5m173 zEGXYYl*tfog(Civ=j706JWj@W!|G`vUbNnw!@^HKeqax9nM`Fw{9ihnjOXBio>*Yp zhx!}~C6&yRR=1V5HL=;sAf3WvZPBci+03@;9!Yq8%Wa)Yht455uSQ9G(D9DIKVwA4$&2B0tS*H zS&?M_mAmHPPU9_jxifSzd)aV>QujN>xax+)3$u(V&4343Aqy=m7Rn~CFBKRa(ndmD z5wfz%9ydII6=}70cji#jH0;ccm^sn3Li&7H?BT`-H@zKnf`p8_`F63QZtl&)i+1Fe zphO*7hfM>TmLxeOmm7Bk$pA4JmLopZ|KE1C;Q652+4p*v+4r zcuX^YO6O-s#tQZV@`rBQI-+Tq?559$k%3e?fV0FJ49sDY28xGQLo~Alv%bUAdqo7V zfz4h{42tL0={_At{h7IA6Gx7o5*J~Z8{a@zeNl87c3Jw2tb>-GcW`Lz!3Vjoc(8au z=d^m4No7w422?ScE6sa+|NbfK|83WzI6&Gthfl>3E0!c@j%?cfo0OHIwKO5N95b+dU6_EVn5N63g*=S0#l!-bS0V;y8)i#U*F7+x;NB+` z3`mvOhMh}93I!rpLU#<0FnRU{%~h*(>t*ABp|3=p_n$MEEz`9JCunzIj8|KkO*gcz zI8HQDUn!o^UrnlyjldE54`V30nP`KfklbX90l}$l9@`RQh-cF4kf_u)G#c#N<`dpjw`!Xlq!*m5a0Fol>3k+2Wu6qoPVH#~CqFkKdC zn`iO7X-BAS84|7mi1ok&eHn)*7(uGof;gaJr%}C$kJ{TLi@Vt(sLWlBh2LzR#JT;D zMc{0f6SYJX8pEy$HS4a|9Az`=XyH3)du#UN8mxN-THbKE1|Osp#9bkdL=krXZ~Iqa z;5zpo_CMeGUyl2FZ|VL=JlOpAKNSL3GRp8sG)hWLUG&agj#+uVF9gd(&D!0FH(Pi+4u^4uoAJXI8G6i#fwmL#FL2748JPoWubCb z3<6=ia*$Xw9X*#wB4PtZ95=*gnqe|QE&;eEj9!{gM`P#Xx%n*8L~L(~GvTPyBt}#o z)jY-3Q3OLka|?V4!%LJT%4(es!sRwlH5t^TA2w zKr{_1%9Ewa5;S1CfP3v?l;vTedcJ`T5V5mbE+L3Y?xcQOU5M8nXRX^GhqXt$q{npnP!(CU9#gB31GSmWjQ z#Q_%jC=AisSA5~*(3l#3K+_b>7xHC$4$PL>#tI*-i?}LEWe|u4d(V_8P4ATU?3AZ> z%6sSthqx50O*d3M{VMjNK+^-XwB+lni<*9CU0}mt1$XcjaRLBr7_Isdrc^E-pP(Iz zd@MC!SME?y;q5;(ePI6h%!%XEN9GRCo)FI^kcneeT!yP3?spcG(WPt?O=}{FsA46Q9p9GUgx9G%>b>CDqoZ(f6wyteHt0GIKLb zp#g^B{~tgO9_y_eFbt7TflHK3=l?$8N5a5E9(RAB??&e@IeL5kynCYSEy9nw2Izkb z+Z+B~l8=v@!~dr3ux9)$5>a6nn!3om0YGip_!0}4 z!fuM_fJZU$a9<`aVu`DF7a)t$jfH`sVO9LnNc&`In@-tmI&j2I$H^>b0a19Eu6`#> z6%t7ip7cx5sj<*h*=E|9A3(0QZ185fnM9~sowO4euvv%NCce~=%sS*jE0Lcpf&_*` z<+m;2DG8|w0)B)gDY*(BiV{d-vW6#Xlt99NYuInCbrsyTHb5t^So@;BwYk)077Q7| zKv?pZ#oh}58AujD$#$flwna=q@PK~^wf`N<3Gw;OuwE=nXKTP`>!+ECHoy`^(U;0u zIt@XTO8TcZ# zns$QVYdNdddS?TWK>-woC7g8^1jrD18i@j>V98VH8yY@;o&(wW;w{{60FX&@u}mtN zq-`ck;aS%x_FEo5myk~=n!}GU!m=REPi|r{6OV6iv3WHBxAwwLz`Zu3Y4LOFAq2pT z$heiLLc>_hfv{gv!1i6k+itjl7Z(nj>Jdi7T<|^arwK<+d_;RSVkXcCX3-j4Nr==| zjmBuItecGjpHY*#&8vc9tCJ+5v7OZcWCTNqSyJu~7^YweSTUiHQuGv@L&H0EaI0fS zVToTIXt|%HD_p@t*-b$_%ZeseGzrKTE^Dz-0Mp-ZOF7ys>+rFJ=Iy=mKu?(-) zP@vY5iKA+)2P6S!y5@*NXE|6`2m)$dSsTTtMv!_~Q6|VVJ1t@|Wz(s5`r2Ex5AVrysyH)oKjJ z_&??^kxw^lpx-<@3=6vb+UO0tnzXT)jKfUpb_K&_?m(VvmiHP{f-#gLo00iz zOH>WtTyJU7nN0FmMox{8`q?id>7!h_x^TnL*y+<8bWa!ODxjJe$ND-e} zgB~04<12P^JCYXEJK-Q1kzqYY=*ap5ALw#M?~N)gg`0KlFOH;3pWjq95}$4%@og+ z4wo$_S+|A5bP@i1Lbr@W^pb@o!+WV}r4{JB1i@$mXAqS(fYGyJZD`P8MQ;e3-rAZMDP6Xeu0QSGlL!KC$1W#UXr-`9X8<6_@m5lR_E z6hNZYAnvJ(!NT=JW4m{AOLlj`NLH4p$(mscFF!)&#kn> z1Rj+Jq*^^TfT;Q>ty(Xv3%LOFz#*{53ONNgQ3?{3hJ7V}VT=rTILOVKCa(tMq`A4- zeN0MBBT38}u2U_CuKKlgpjda*4Z^ai!&Uv6KTT&UDS)tg{z!%2dYJB)5St&tP%qzJ z5Qj%3iDR3kp3=cGwrSXv!~c_sA=AfnMruB>qNg^4WH!kS;AL`2W`kUdXxTYTTS;UH z(WDT;kB)o|4F$ao5RlF_0j4!%7>7@-rar1mv5a17el%mIO$mi;oQ9v*VCnm1Ib2~z z0TRNEEtY;T@bKxv=JJ!B`EpgnJ=c=W{dPP*n# z;^uB8wMt1+@LZ=Ub}Ud6ymQFoJirXPe$MCQDosB>2!@j=MWV1TwyzYlLJcdLw*#CP#{D&aq3) zLm%)VKtINR)1c z@e$+D7{lQav*nrqdzL2D6e5~o%v4TNKZ7|^z@H&I_yxyv52eOx$%hi&{TW(v<48asH9bUB(0&OtL#u(n4-lsOJz6S*oth`89- zLi&;7STopeP)&**XZbNc+@O<**0v7ubC}pHU@dy1CH{=nD;e!y< zVL4bDDeN8^W8OeI)DM@omTj=%N#zJFRB?UsU# z>Z`-OQLhaGzUADgjpn1_gT*)4cqUY73J41MkXNrz+69m9CSZw1K)sfV=-v-fCTglb<)%BzUKo`{Slgb$w zpo_4!0m@DeLVxw(h$3tiR2#Kw>Xb;8U4vz@5fjZ|f_kuHb01_UY$Z!FindWx2BTCyrv;2{qGb*2tATWz=)XZKXjTK>^n48F z*)fYF5EPP9`zmfEKv0N?i$FPk|CQ0g6!}NdEs*XXHFnuF4b#?Z^8!xuElvED6QwDX zvTLN~Nx~F(=sK59FpZdur3d8Jh8&fj8wA$|8{!6N41#2{G9|m!Y5_{>k*6VOs98gJ zP@@JwsLNwu(HsaNUTB#zBtrhbFjfhLrALMfk&&_D;Uc*J>zjV@@D(GHjmBo+M~94t z~p29ZG(+lSF=!>(?{9+x;9ULv}lTOcJiWt1yq-pLR5EfU09^pbAsAYDS@<+IaqyGtdTG2^P;1By%`~OFT0nz=w{u8d#j-T{?6k32AZ2u%Y(sQW$!`&Yq_`d_EJdb&9 zaKEmnK1tcG?p6(eMnVI1rJYyR1 zX%I%tkaPJ2@(Ys71T-}22a2h4l-IppT{URdYu|Ag)_Yk4$l-(;mMrFv403+Jl>B%7 zm!DlYH9W{%MQ+%l(q=JI3RsErj#O>}S|Z$wvU6M%N#}5ZXHsf*Et+nojR{DOSrc}| zGdhk)#IB7c65q9!5civ-Xs!l-WV2gjJL-!3YLa!`U*#@$0B2M^LkrN+q)7Olhr ztMj>h<{})%;Ka@BZBz&1JiDDI+nWZ}`mIG7|B@SYYXe)vxaufB8@$jszqhIUg!y1d ziNF)F0@}EEjQEi1t2k9SNnDMCb~#tWiGNyOjjK3|Q1~O2nkQd~cm(z8YQec)pEiM9 zTZ2~bWp!y4x$bax9<Mi%^*1Mz~qQr zHT2hOVg-2heS=vI)r5?`XJD=1y1LG4F?I^@Fzhb%xmY`hRo`9jYt_x6=BE(1ulogT z=cqsN2B=4LwgCL$%9#@K;6WiB@%f?fKRz^a@+8NUPMWa`*|LJw!QBYqYTk=7$$_G| z!a#5O*sON-*-RTN_DM^jzM+}EZD6RrDBH|S>q0j~EZ>K7bOm(D5Ks0?w87prG;;PV z2i>y;>$-vk$gy?!ppy;N9yp^MP9ctynx<_r5r=qtQH6aflWn$LEhsnJzvGZ@_QegL z4Sgc~uqvU=EY>7{p!EF0vEji}r#LoNI8!`tVPn$VI+)kWUgq7R!&Z%ZZp`wc+*Bhg zn5(lgX5V8B+yK7@!(w(m&5&msm{L!Enux5?miql^oeVF^D&J~)-o&PwnQjC8`a^QW znk$Tn01Ax6l@P*i)A`>eyiXW7=jrO7>vK3?it~Su{So1Po=@Q^-}beK-tx7FUa#c` zK;Sf}lEocY?kXgQw++T|;Wr4^1xjT3@R%8^^wi9rle4Gd=a7D9dUYwEjAq1zXm
E}PCK(y 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 Ee;cG9OaK4? literal 0 HcmV?d00001 diff --git a/data/planner_s11_complete_2026-03-17.db b/data/planner_s11_complete_2026-03-17.db new file mode 100644 index 0000000000000000000000000000000000000000..c596821906b8a4be16eabb2831340e8ea63dd666 GIT binary patch literal 172032 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 BV;%qi literal 0 HcmV?d00001 diff --git a/data/planner_state_mgmt_2026-03-17.db b/data/planner_state_mgmt_2026-03-17.db new file mode 100644 index 0000000000000000000000000000000000000000..2df7609b6f1a3ffaeb8523beb61ba2f2aa0a7e8c GIT binary patch literal 172032 zcmeFa31A%Ac^+5*i2L$rM9~0-Vu94m03^}us_GLXYJ>p+5*p$d02~fY)0jqg0cf$& z-R$Znh#8Hg?&i$Mx8uln;wxUuiPy2?BVHw1AClw5u{TN_W#ibE6Kj2}Wv{(HBgb*F z-~V16ulnu=#xv43W&n0|y?XWD|NrlQ|NGC4=dQ=Iy1bLh>_@V)CVh$2(ILH8mL;hx zE=kgP{PyDa9r*3T?+N@`KONSG>i?n-&;73&9vqbVJAO+V`P1(7$VdC<27aOAw?QA=s91{dyAL{8nrD+{s_jES0ozOG-y<9w_@9W9z)?VDmrZR^WzUrS}Uzpom zkT>U^xxOG*Xdq7w^v7fJ(#qz-)rEC=Wpz_txpDpaMR}lKY>g(*&#lj2n_Hg>_@-UW z?&}#No=R5iA{);pbo)uwA8&(p%G+cq$Xm9@08;uv@;G(bq^yFjq2%aBpKDWFr2KOiAU_=WOXCEWu*0}`CeF%ueZHj>%Zvs4jYbPABH)Wa^I5ZQ_wMo!;K=JvPsj0kXk zH>0OhHqZ&RG`DC*k7V`O7Ff^~(B0)o_G1le$>dMCE`Wz@!&fVJ*iPECH8!JoC-B^&P#C!4BMcKU1dRbguUs$@jLOqwX zo0iuX78lkRR^}HrTq}eK$6&*dS6AdK3)dI0kmlz$=I5>~3{1bi(%F0J(xr~Cf3|En zvIoV%urIo*E=BW2Y#~-#Wyi6SLT<9JyO0!jdm5&DAt`=$i*zJI?8wb5*b+@;vRkQ4 z%%LL}Rirbi_v=yipvj2XI>|($dQ~ES!`Hyy8dva&i~a~)fwQU-W>|v5dh0Wnj@4Md zwb()prch~Bi|f%Qo0gU$k&3G~o0wePnKV{?K^Hqu^qx|bj#n?S1s%<#3}Y)DPb4Cl ztwcO|7h6^7H{Gr!U4EZ7S0k0nU}JHw2^aO7`$3Hs89fo9P14wlr`;>CbTf8=+J5y# z*?vbEGqfAIpjF1bs_?;5;5+YWjjPUg_nf+^bV%`JOn+e9OJE<`isZ5>c8%S+6a!mI z`FA}d-`(*JV$EL{X|rZam`g{Br~7+PJ^5tEtA`9KyO~IO4>HPODwj1}7eg+pcHOEW zEhZ!Tf~2FC>@}jKXKiuTreB%+v)u*)N}LfsleJu{=6+m1NMl%Yl*4v16w+*VS|C#-&EOV^Wd3GximoTNilt#2Ep;D*jBBm!@xBiTJ$4z_5T-N|JV6>iwaHe>5B zvY)8@d_5v9CBlK}VFJmQ(4Y2C8v<(1t)80&NI< zu|l956YCxQ3{CIJ(Z3%3%h5j>{ln4U8~yC)XSzRf;%B=)(>dA^koJ#h_W8xp@d+j0 zF(#clcd}3F@9Se48T5W`Zf^0$%Dm^s(rQ}IL|~Sdmy$54?!cCFV~N}oWSA#+;=8b4 zl4kFsZ0w~DhkVuVP^qh?+JUA zpx;|QH|tJ5)iHkZsr>S3>0~+eZOnL;wM+_XYnYhHLVleMk0v>1$Psxz2aLocAiO2{ zAOd_uWatK+=E7_k0N6AEjFh~8$R;iWP|V8)u#)3R1{RsXVow>^1-aOIzM5pJ_-DO} z#~08-;ZibHX8i$ANb`9E-u#P0 z-UB_6U`WJVW4S2sngWL7S^2Xc`iPv`HuTK>$aWm&mcxs(p4K@G`W0!>smK+X4Z z5xOf!ve`_0I|o~%L%G~YR(&O6B0f(rjCB|;L8@>k|J)!UbrJ+yK1G^gmD$}SDkbuC z&5WkCV-d1Ll`(ny(53)T12|2}WK)KXK1Q((%t`eG13@6Jd`{M#!r}ntRCr|!gnFtJ zZ5uO+_ZpTIw8zV-WE`B501kq>u|$NLv`1!BDrk_b9&EOI{7KXICAA|*fZ#ogQ!zz3 z^+kXI+htfz!iuS1O6}~>zZXHb-1H~s46t6HJ(B@*23rDQPU(dp&WF0#BOctQ521L6hEZq@6ftu?vc_)+FF9qP#{MwC+^1i+g z=PKAx^?gPwV2&5%q@F!UW$wy^0Atz#Rt|>C48C;sFcVWL7;iY6;G0>qP*OvlP}m<( zDnLo|C|knT#`M+I8M`W0}u6uRKHqIU5dvW@GHJ>;au?z8p&CKC+X;8su%$W?KRd*{|V~sk52K7>qDk^46fw*hTkbm|;O3 zfGVxmNIIRs&Pj5G4L=V1DD$2ou-peXW}X_>dUOPg)}Z9WPNN1itqe*KqP&`-g#3lB zp7E2Dg-2-(mdSkBbp!KUSTp&T&1NrI9MnP^L$*y(Y1PLbW2`CU#%6XI5=LrY=s@GQtoftoP zwQ!qwQzh#0kxVJCMIg56I4;aw$w0JB$_s3VG1!tTlib)NU>ex_t>@t0LqLu)L57w~ zQcr+cQycEbh_^-L4_#po5X&i62(2a9Jx-b-U)5{zPpaBql50W!GUMze`Sh`SMZ zl+to_1jb4$uO7*HgC2hnTVDl|Qz7L0f@+}P?HWJ1z}ZtJ&}j<~aQXq^K-e|mB`*>j zX@jZ^IU4tl)r9yCn*?J=9N+|>Z2-sV*%k-5X&O7jfeAYL`_j9ARkB&Yu`qJoYqsh^ajINKqIP$Zj ze?0oTqn{c5^vF++eB;R7(O(+<#OTLHe{}Stqe~-yG4g{W-#PNRk>bexk^emM>5*R? z*&ewxa(3kTk++ZZ41a$3bHl$e^3=%tdOqFr^F2S&^8-EK((_u+{hpnk7kb{;Guv~q z=dqrl?*HEXhuy!~{cpN|y89n@e^>W6c7Lcl-Th+sdiOKkYWLahw|Ad7@#h0W1D*YU z+5frz-|YW%|G(=0c>j;}|3LqD^nYXj!~O^T$^PyB=lh@Se^0;C|3v>-|4@HN-=FvW zUf*x@eX8#heLvOrkNW<8-#7KW+V`MuzwgDqjlOGrPxX2F&h@>cufO+y^!`cjZ}!83!84t5Xx*MUD6_{_kk20l6P zlLH?e`1XOX8~EVBy#amT=D^axQv;UIr8{^ama41aX^+lIet_yfcH!(TDHI{eJAHvF#Plf!*Oe>L>E zpHxo!{5_t({-p`ATQHQ}2AC^V!b#c50nZbWU{b zfixs(pzBoUjiniHxTANdYl3|YdDM;`=R)b|wi<;xPFUXu(WuM0P&zuTM!}8_>-&HQ z&Oy$FD*aFALXrN5^+Z7W8|!<&N0t7&bD>CoZ8h>s|IPZ|=h39UaxPTq^Uj4L{a5P= zpY)g3M@{+*tBHz-|J=Dyr9X2n6zNZ`CsgTAtnU>(@&7s(s`SUsg(Cft^@Jk*KlJhD z!ZSCPq(2<)I^8+Hcy-1LXi9$|Tq)A$2D`@0d(!XQcK}f7{}!$k>G$k*LFsqxJAkS5 zJHnMJ{kCwWNdK4pR6zQyeTU%sTf&th{TI8PU;5AX9Rlrd3RkN18R1Hie#3ssC;hs8 zN5fOUCS0k~e-f?~=~wNiH0eLucL)>zr*Nf8|3SD?q)*#VsnWlzOb-4qo{b~-?*+7>HnZEtozb03wH_KzvQ}BrGM?ZR-|7P zo>ruPCEWGm>3`|ER;6EXT`SVR5T5o*KkwXytmKnE>AKdWPq?mC>E~S6iuAKiZ{D!< zGs4{vdi!bDwI==Vu4`5Lxa(Sx{<+XwNcv~O-JnO2J|^4^O8=X16_9>PXzRzD|EcR* zlYY{5tx7-Px>lqg7kcwcKPKEIqWmYWYgPJD*R>-3i14&e`p3du4P*IX*R>}7udZuV z`XSe~BK;$wH%@>D#SF zKIz-6?=>{~R_8*IzQt;!N#AUJPiyNV&V?fVU8|8QeUtS)t=YfhTqx2vT8$Lx8?5iW zX!P~Yg(7{O)yOM-t@%9`h$?-JbD>CIZ8iewf0gw;vD^P1?K7d9A=R%S4R-=IQp~0?+4uIba-ugl3v+|YwU8nf>uUOv#+#hg0D=%9u zd@otw0@jDlXXSy_LOZa&1)%RcpOu`|Ld{y=f^iwnXEoz|R_-+j%M&aqa*=FN#;nmeVfq70w-?=#F@$e7V!kf7`hOPV;5rm3m9OQeJSL^4)aq zVEcPsyi#w7SIVaIl(ymA0YcZsE9E(-ox0}S!QxpJuap(1owD55b&}!g`bgJg$J+Yp z<|^%X+Ow`p^?j~OWofAEwB7ica1SJO)pe;Zx-OLk;YsC+a1Z<5yz5eZ#&xO82~T?8 z>)a!{dfIiVzQ=W`Ty~!HhTkpR!~XV^>r$I_U8-T%r4kZ43I&CGAijX>QuVto6`$~A zP!sM!8dF`Ds^Yp-yuy=#OTs;nq{nrs%m_{W7lnJE$_uVb^}KUQq_Md$f6cSDJx?x(62{5D_kg(b|Y1`zelq(!iDlqyODC5dSAOfxAGYO4qf^u?2o>2 z`=d5ye^kHJ{-~UKTi0|4DO)`AtINw9o_Y1!J4(K~rp}*ywCjS{iZn=Xr|COSK1u_j zDZP!L$BY-Ag_Wz1I9IFBYb(lCZ>tTpGrSAvL-n|3- zgE!o7ehBrMAA-H+hd__{!QX9u@SQL}XkF$9wUZA2Wb-8d|K8C7Y4q1de`56OaqeFk zy@<>9Pa6Vl2(%&4hCmwvZ3wg>(1t)80&NJiA<%|E8vdW23J5QV#8{_Z5U9{5N_utiZ;^Z_v z?{zc_q?0IV)yZ3V!!=uVn63P^TG3IzcXVGG{e9T<^P~Ht{|@&3Z~h`haeG>A2(%&4 zhCmwvZ3wg>(1t)80&NJiA<%|E8v<L2uNVv5D=Cn`Z-*fnV1ius*K>7Z^ z6u*=ia16iW_@(#(iX3#iSx60s)tZHcdwiV+PmM zPM%64kPy)pXlaHWBWz;YiX-o0VYvxCvcsa!z3LJBy}I=L!U_w@U|~c?ZrjW`fWW9Y z!g1q>uzLwZp*Rlt#>O-FGM&p_Ld+umktt}@7`S=O?7|E+rl2bQfu4!R4LvrnNq3Jp zN<=PINN?cv&HB8v8oYJAh=UO$<#ETa&H6x(K1~S(U;WCS(-Zlw{F&1oJW&H8$BU0V zdBxzZp6sb%wUtGNIW+8a=P zK{0WHxd6N#Z%FZb{l$gu@iT{qApesS2*3yV9~SPosUN!AIYj8r#6a!{W=Cu(!oni= zBk=@FAHfM6%UzD|P#__~;q_>IC$7g@d&*7c4nI>`>+aa-io(-42oQIC$w1(LlA_j3 zPAq3~NrO{NYvZhg$AF?~fpSKsg*;xuuOG?T$IslkBjE8);X)}MRfLZs9u}|Cx4^2* zkaB$9*+CG$L1_+TDg>Sske~&8p|BWL$rqD`yenZ%)zqTe zRf>e-U?~z5EOjZ9B_`HryQ)ZNw1ZXdw_N29}Htdn=gWFsV_K$d{$9)-G>f@ag6Zh^30zl#3!%HPtyGEg~=2#4lICB+U;{Rd=VS7VfBZfs_ zxA`aBc&xU;Lr%UUHw;31Cx>S-?aGZUe`hlX@4q9r#?X$9s`gy4XWM%%*kh}2fzBER z4eipc0i-6K`ms}kNX1+^UjM8bnDxPn+#68+;nyyAJa%#>|EQJ!2V3>)6NQvWsEna( z#CMa(Rj>hWK}s$ptuiw+F=lhoB^~iJ2*}BT^@+4%{9)&KVG*T}^T(m7J96_> z?$8zplFJS*S+Oa1ldR$8G6z@T^DI`i#{S5&gR1^yDWw8j|yBk{_6?Bn+3m#2%60 z1F|t40U@jMb~`)H!+|^U{Jgb8Q{J&mDvP>dto2uF-fJ&8O@nvjt8Bs05}{;76pp_i zPi|Q+H0PklO4g@9HZsrcL<6IYEr&7{+G4rqaA;#kI?AOK-HRt;VlEqSd894CJecsu z*C2`fhhifnQb6K5WUk?CqCaB=>F5rAL{ypq3obn4UP^3`uF?I{C|ZN zJ1^CRGylKJONC>;SVD}4cOamnGVqZ`utt6=jWSnhIA(jb!Ua1&RV9Faq2{M*1zOee zRB5xggCtdcKKL zRFc(HMW~(P^DExMoSjms;xhW8N~u%{f)ry{6NXiCD}j2v9wmU(n&Cp&&aG4thF=u9 zm0E&bl_X2rtP=7l$V=r{hrdsnH`B0Igk1w^Slfb#S;Dt1h#>hUPia~yQDzARHw3MQ z1Q9!7W zBm=b^shi9c-nN;YX`GN(F~TxAGb_DdQ$`}tEHlOF52BQx80Kj^c{im8E?K`b@r`hwum{@9Z?rXqp(#+OiI9e^H z1w+p;%rQfq?WZ7^U4%}Md^V0u?xc!K!py0oJUZPas~;RO817ghOPUBbW^hdy5?ljD z`;;S;b&MTDhw{+^3G`UpNWohTu-K10i0|k2`BOaAHbBoT|Fi&d z_Zj(lI1Xl@Mm1QvYaQ9-Q!)~os~7O}x=GtFqB-bX=B?$KsC*VWTe8Vp133F4RMt`A z2IdlpAiu4SbkWfS644U%5|L4|cN7%HT)3WhiS_yUd+u#PoYE z%V!CoXB!wcY2M4|k`H-ni#)ovwm@zs!Lzs7gz8}1ybaK0^n*wYmIX%Rw0k7=)J!`5 zKu?Go8E_0}sgSYG7Y>9&>mYmNHa3e1SVXOS*w@KeMx+AE&}^2`X%f&{Q!E23*<@3Z zUEre4I@+wG%{s8H%U>X@0}f7JZzveDgbi3#Ex!5T3msq^{S_q*hi$aPHQ0vG7Bt#| zMqAL}a{3nlw@^Z}KGXp6qo7?0x4`*h_{0GIc8`8c8vO;t{r@TI{{0G_OWHqe2(%&4 zhCmwvZ3wg>(1t)80&NJiA<%|E8v<qhjv;AoS^5qmf2MD4 zj^cA%!4q;i!81!*|H`|WNP3UOg(1QM0UU_1kR$2z%V#M+$k|!><+Bg)H>d?X-iz|t zLwxq*<16^g5{OU|2)mVv;@W~EUhl=7VJ&e zoc9D!{Mu?HzDKpY5nH&sxv^w73I&1{y9rcm;qHc&xwhLWsvjv&P59U&V#DV3uqk-Y zX{{h+#cFMLOOc$FoA`ZU3sO`N%WkA1(XiD`&>Ilj2(1)Vv0H@#_{?F>N^3a)(*i)3 zIRSSor%ezs=~f%}`;hS19+e3ClAchc+#;xu&w;(Wl|w856N0zJA9H|6t>_7f-`i7i z2uaa`&b-~D_1mLR0;)4@yODr56fJiei#^(C6B?tynA2G7*>21-C)jON)$hFBT2*#( z1*e%8xV7MJ6Bnf*x0@)TptBdD>6L5!-Xm3s=?BXTtn zMx=P1CR(t6bBM}^ly_CeCooYYS8#x4V~6*)zPh>UOgF4lAY;~W-T|IZ2|62qySs&r zHM=vP&sWNDUwQfLOR3a8ns_|{Fp6{}X*Zt%gZ3Z?g!2GEN#dCqhLWh{=Y0k1CKE-SyAoy ze-)El_rCdakNv1Ap3PYbx4#_%Cwd`q#gf{>_lxr zqb+E(1&uOYx%+5RJ^r6%S)%j*fb@B3uDzzO#cgQsQUIPSnjxZ3yLG z@PvGuJw~NpmQyStU?PspX!b1ZBjXb{ zZeTnoPx0~GC{pD^R^X2Jt8S!`8SJ8&=85H~LgK1~oY0fIl+O=&(e{l-+Bg%S!dH+l|t4*mc5PT%);$dW*?cR za!Bu%krSziS#PkB;jrXa)F-kJQuamU@j_}hWRGICjF7g9Rk+~wnkbuMh84%CoGTVp zD14}r;Pr$RH54qZ;1!~5(LO(Ai!S!(hsP)4aRE>90#%X}@T4?{{pMO%z+H1490wMw z*YIj?EOn3(@P`zitJn_eLIN29B<@9WL*=cuh|Tqnk~xjg?@z> zpb{t!=Lg0o?%owZcDF$FX)P`TkSQBT6q-m7=R;~w9fj8J>!_ef*W1WYi4P%pDh4K) zok0#seY(l!l>pq_3y%Zt)fr8b)2W3J05gn|B)$qX?DeSrkWbOT_WLJJojV7F*y6%+ z;i?NGHwgiJXB0j7wPWH@hWc94`hw3XF? zX$6A5fEp}Y9o~=!-vxtOVL0D6K5^lKusSZ}cST&F$Z&$)$r8Qv&ja zN?IHhfW$SbQ*X)3>QJXo4TfALR5-hUIa~1u!+w8pBi}ndar?FaklV$m8z9O$vVo@d zQKKu8xtl-=Wuy^?#IzIN&1G0ZXXMvKQrW`>n^gc{gHIj@U}O4DB$p5-KnYq&q9h5@ zs#)FOc-S^*W{Hj>B{yJnKa$8XvDN~R)g>Xdv`r410rm&fOG7nqGefUAzB*_{4W#l~ zXt3P)JPL|l`-6pazGvdp^t8xyE;#jKi3fx>I66(fUSJuSR5x96*rbb{C zVfJ44QHM5m5;_u%BSAJ2mNONDaR@H7PkazseU?riLxQ`9raA}k>U&b%H`iKPbS6`L zfpF=z9iBxAFrSK|k81I4`E%oww{8j0y;ayKgRXWZZmYL@P2F5t zT-sXO*jia#S(t9DRdqNw_WTinO~S|h2ow^mpsAdSMspdnT0mp(x7VPVk(;H&C;aN! zOdMTrL-&))Zc%Lo6ohcIHBx9mLLi)!o}qyi)C@+sO)r&mdEfYC{Ip+uW(x1mKQTV}^wR=H>x(9VSr918fFDNw;Ho#OVo|O7$dM6OuReCwH#3wSsUSAPTi6>6!1>R;Q2}o# z28XreQyp=;=~2tJ1rjgNijHPx)jck;)nu<)nT< zFl(t1C$N6O0-b<1!p!8`=Sv%!ZJcH&&_kas-8Il8SI*n%f-^H>WYz`Nuo+G zha4AEs?VAPljf(K@UNn`V?v-sgt=<|!-c64w65&1p7P$2Bn3W5CD@yW}Vg(Z7A zZx^dJ5saeb8(HXVqw><4Os>SJIp~3mdP((F``%4E;`UbUi6bDY_DQSS3u{8o4?S=Y z?6G9|g#bK{=JP>Je@Ns`G1W)4=g+i7)h5lDX+uIGqr1A(tF!cdsv7c_9})d<@5j;)_+Pt~KQnP^ z<_-j=F`QLUK&{RKDITx6p8vMrjM)Pt%*kvQ#n7vxrBDI zfERcdkcX=A=BDJ(r9cuO9P56??OtdK+L=$p$t)SkK`bywMaLJ&bo&uhjZf-Aa}dqS z^Bb^O8d=s>jQwhX!q>8K2pNFCC7OlSxW!1#!LXpHjk zF`)^t=W0StA)+lkzp%opK9!ObV$2dKg+-@5%qwvyqn1>Hodd0kOOZqZ@_9TF19WUR zawz^V6U#cv7bgdq(1^=&q}F1uj-Vt}IvYVPCse;8$se*8?Pt&;#!;6kb%16huo)=0 zRA+HPc0hDU`M!!1Mko~2wBZv!C-q9#rQyx~pX_~G_j6s*j!#I}@ns7?g-rfE;yVv=yQ{c{gR& z8eCpl*?MMu>FTx3m4%Is%Sxc${mO8!_tX&q?`T*|^%@us_gzTBkpWH}pqW^ln2UML zuQwhOA6UfkNfi3)9x&lEyj~}*z zkQZ96caKj#_nd&B=L(LB(?SrmockjAZXe1DTutsHuGM0(^;=X%QvK(Sh^FdFuKKU8 z16@$RS1Dy+fG&b?{r*5$1$+6%vD2D_B5CYgUP+x2uCS{Rq!Y4oP#A${=OVLxDeCW7_ znd`N20jKen4*p7sQjo3abkjIVI06rubLj+AkIA^6fZSS_ql#mL;96%xJOYhDkZe?@ zWYssDfD%9Q)CCPSs|ycmR0jw(d5q!-`;oVxT!)Ml18`OehLqPQ^5N5yh2?^{=@*t? z9V}rpL65S`<@I$YuFq0+Q+*2@CVde#P?P4T96Uj(66a1Dy1-Z_l}_uit>}IXSJ6y# zYX?4$_-N?&wszq!bWzqbnN(&gn#v`!c<5lutXO$b&KO4gB0Gm}nSwX&!%0%#q8DuC z@GQEi7)#o!t5cWueZ$f zZ*_36eI%Jm&e+r9voz0C*}Y6Ex4Q=kke>l#b(UvP3Beh%0AyXfVG1Z?mi5bE>2S+;OzGe9I{(^#v$SAy9zi3oT%U8yy6H{9p%cGreMv_>Th+R z)S*>hAb=bj<+KWC(V*WK@^SzF*QF78=*I>(`)~FBP0!Cj3vjOE_odgnub=qXiI0u^ z?#RvIj}M<4`pDoP4&EO49g7#rJ=3SW`GHGcZmEs~nY zxQ_2u{$*>$%r9O=G9H*kz>yMEj~nJI#Q*3rbRzNHB(ud&U0u39V?=h~c-c@ZS`{a3 zH3EM_V^2C{Xqy(8cFQ@d3f}pRI07{G+M0+CdxFSq;&&qWcSlB&R_?Ig~rHM`@R~~sntoShs4M0z!s`^Kd!X-YLMtu)Eae zVyz&Szq=Md_~uY`D#Yz;&VcP4wFh1Y^{N{1BOXq06=TZ^S|dH1@42gurC45l53V9y{JWjPgt6@~e#?ZkD6 zxA!^hQyHVtcGaNV=<~-8=|*2%2injlA`Yt@+RS53@%f8io?n|7yLnS!WBJ>KyKZ9M zV1Cf^EV{*lEevR|VA-R*uQt@k4d%+MjQRK29oNCH&9IoCPb1{n0;c%M&pa%dw8iJ2 zw#e|l%K28q<0dxM$Z+f6*B4a7f%0P^lHB^Vpx3X``M+c6PbB=`{%J#?4S_ZU+7M_% zpbddG1lka2L!b?THUz%7An@8y_k=Xo5za?XOD8WBAAc3eq~M;{-`_7ub73lG;n6y+ zpttLA&re1Vq9Nx=~ z<|Wsl`3(j`Vc!iFOaYS~GYM{`bD4C?(1%a_qI9C;d1+*$@8aMZKD2+10Rpe3CmtJn zx-d;F<|LVn`}-fRK16IbI1Rjj8JF>|z#(Znk&500LhLZxwMcf4_zW{xB3U2eU}AJ1 z7LGkL#RNrxAbnq2$Dd>tfhb6Va(&^7nb1Qg@-)s>u72pT!shW#8PC=1*?HR z6hX=QRf8Id87TifA4)*3fB`P=SHN6RG(tQNWgSh#nE~ZhV8I~~SfVN4A{nBXW%^P!V%&vuA+pC1J2l74z=U4r z$hk0)05ioD5#)Se0mlYFG=e-bwiSt-9`PPzL^K(`k&fR*8by0@9RAi_EWTVa!tzw& z6&ZaONJ5Gq^sr;Sh~|{gA0WYxqfgvRfzgxmoS7_wDWqxiBC`3LIBZzNRU|1u4wI$L zC4#}peuQnwu#|8YOYUgGdNPs7p=^Lg2pjv62l4&fzRWTjFhBxkvf>3`fwhjg6yvdXWRz3jJp%mVsmEc5n4_;BBvLaJanHLMx z(Lry>E1Crq4Vil=>mB@<)Ua2Qx}sy|u5K-tLZN1R+}_V-QJ1 z+K}XfRke?W0&)_@#H?0alwpH{PXit@>d-w2M~qr|PeLc{nj>XPt$`In`9%T&ujt2w zwW9=QeOMvLJL1)_LUeIcLZr;YRleC-3#9g*^xEXuJ8LX4N#fFTdyyeFv@gxZjUvefga82TP)6=1^4H?N`5t+t+EyIPr4#54QH~c$>dEM=4?R|2*jwd{o{xee zo1KwiZ{ob1-^2{ez(r?IvL?=6M*<_Tkk06aoy5?Ce~W=jYiz&LlMek;tC`l3R><@TUnwDshVVKqeDYJ8MbAA&5DcU>oLAS55j&EvR;UWVVx=m3yH9LSfF`O*P!4C97_WWGq17$AY zsY8nkVRZ1ZWqt6KgXKVThti4s?F8iFqimlQ3GwX7L@Jf8B{su_SwoO45?cw#QV}{n zOJxO->swRE{y*3`CJp}bz@H7A@4waecZd6XQ(d3xne86O*X^ITIs_gLo|+ib3YxXr zAk`Ib@j+WsonlktQj!#o>!3P&tq~!WE;XXaqUP=wDvZMPG4#vWTgf8k#BZ))q*?)i zWBrmg70ZoSmW%wcukZ|Jw5qrDL!4ga19mLNx}d3Idx+k}3Sl`oYU)z?nd``lTpT~v z_#7mTcoG@_8esX>&X%qEgXu&gv|x$RhGgL#pO$Zy&!f zS6D`_oC}jsJh`LcQ5oCg)OBVfTSX2I5++hIgTgA?-ibKaUB+q?TqR-aT97!09-$Px znH-C~u>$CbF);>ihQgRE`^wx+i`me?ibZzq6qI8oi#@~6xMQa=0-a|mrj~$&t{{v+Yw>U^SV;BLNLWh3ubRkd7E`r*Lw!?i3~; z9iN;o__03Ek(R2;`gDQFAbt{|TtAfiXNNIR8o5Jb(B;B|+%`+;NEQW=4*A##Na8}a z7aP;+AipBo@a4sbEeX3;Dg&C#M3M&OcjB~VDWZgx>0W|@b&8f>^@ZmH1tFmy@_D#4 zd4Q2{CqMtl*ct7xVnPqQ%Ju>)g@epUV6kgl<2}E0Y5odU3}p$3Y{yXx=#Y;e8w(<{ z(u`Hn{B}qod@mQF77ogVNwBw6orS8A))6svT3F0W0!)i{@+)65e&)&i$+1d#N$g_j ztJ`$;F-LnnwF|~7E_yzGM6D(uwmVtN#G~z=i>G)<{>|-|CN`-*A>`v$f=CGMUhgy^ z>rQ@gbbLa|chH0?lkSbBRVagL>9Uh8D@Al;i4NcHm5N*fd%%E`v5ZgC+%!OCSrpo+ zGLxG0h2f%{K{%wLhD6y!>^u4AQTgdo{z)RR$`iSE^O^$m z-LBGmhXy;TJBi6?`UZ|?oKXq9S)jSP6N6_wGO4=~K68>oidufBFgrYc#$TXBc~vI1 zac%YHb+9^lgAlOr0O&#-OjMu4F_7%g@hB1^U6hwl+=Q$KWQjr24KPXM&P0wfWb5Ln zV^#?4QN)qJq4y6tmqB*~E0(wn5&{}XDZ9@8$uX#IQVMzZ4)Oi7n$Pb0&(X4{@2dA zY7E{O5o z_+~BoQJ5X!;0zsx3kH?cKs>ienI4G;-#9ihhG|{g$;MF_}{}W(sdKBjhkmClR83Q*r<|Ao~1(DE$LuD)lVf&EnPs9!dW=yLU%Y|}O zLN4T5(%nch{t_z(2;I*<`A$H2N(6}(tn!^_VBp+2OBj(CA!G~;C?5Iz`FWI$qQJrp z$~meV=g%{_jH$dxqePi-*bNAI@>y&Xk+bfxOfe4gZfuJBdo1RBiLsY=XDLa`d!`3e zde2pecWJCUFpf>B0_dG0>+7H1mkecgk# zmD#0zAclLi>sU?MJlHs~Ul?$RV)ZJG0Uy0+o$nx5gd6e`^4z@n;sug`siQ*~ep|q1 zg@l^oPQ=s)XJaT8c^0Gr;dj7KZ@FS&+U)ij0P?PTfEDt6vW}SyZeZmbJMgo>W`|YF z*b{Q4qlOq@X`iP#hz0_DUQq95&DUMYAmb>{C<-8gYr)7avyLgsk96m5KM=}W$aZR? zN^TXV%Yh*E%*kz&(uSf;RMetErVqbF<#;Z!oLS^u00z!pz>*Jo+}-NS!_QXacKPgi}42tUrs?{2Da!pimeJ{LZ>sFO2{8+gZd$L4=^P>k2GV% zq0CGZltGv1ovkD09EsuWL+-Z7j}ZocB{)ok5f|6?GtfaR6$PiKsB#oj<>E{yadQ0O@kS z%?jPHC2mw&_ff*fZILc)w z7M)JKLZD1*nG>rcqM(UiqIJS38?Un8D>J=Ij7+9PIRfQb?*)ma;*{1Y4L$*O06Mja zE|=sUc96DEO9kd|uIV6gDQ(JqVDJ?x^N2=nt%ocZ(`Y7Qz-2Wlv@0>bGc=oOauOj31VloN z`UM-R>%}He@Jd!gkf_ns4>J3s*f%6~)1#3Z)I|W^POyUXV&oty8V1YoJ}^)+U^H|2 zZbA|;)fF~Th+}m|Xql(s?O}QsVHg&Q59Ml*2N%lOuv(0so*0`e4CA=Pe3;3wNCsicg&8^( z#X7h@Z|(Z9eSs|ki8txwz$o23<60^SGs=Ol3{q8Wv)BqQQks*;v5Yo29F18~HAW8v z2;0+GcTJoQhbifVS!?UzqYrPh;lQZ9w!XT#iiCjI^%$K+nP6tfn!-&5pn@6<9cSwKaE^Gl>9cC+n-YDf}4}J;;|`(E=gC#KarUs3ZnwQ zGE?C4i_Gr!lt!%~X+-vU@?3t0ePK)~0akj;9tINe2u&}0iTJUfki=w~v&_0dtnh~Y z7_#z+kFA)Gv4lNCxqf8Wt9+cCW4!h=Jy4>1s_W8)dicj#!GCuC`=`=sf#skRR>#* zhmH|!HQrJOY{>5yMz&qYF~+1exKw(BptqtbKQ@`mcMIf}?=DP~NH0jWx2{7?m2|;P zA-E}v@|5FJWZ@SQ;97jT{xhHazR!N>dp`T2Z#SQ_F7a%2kRPSjW5mf(hEWG;$jur= zEzL5dA**ySh=RSv*qO&B5AwYt(s~PrWk^#N%@8t?Nq7}t%?qrp#Ytx&V2q=2y3Ruu zj!IBwK3pAbN9wjIywyy&d8A<+TUJiUUodLYhFJm4$o{5jp>XiEk#|n;%#8NIBKXiQ zC1~xKc8&HK7!*^adZ8mXmv<4x2}{v!L(!IEs{a6@zWh& zE&ZzWwDff6z0P}#0apK$f5+t5FJlLvK1T(bQXH&2gG9v%vt)?vX;G;8t$1ct61!rOss z4(BgiD6wKu1Xe1RgAN?0U z4niSke_^HJY-KELuYzrwVzl9pU;z#qavO47nNpK@RS>TlKuP&hVMIk07KFJ6yv5XG zrzi8>B_d?8t3(>XrgkHlCL0b{RdK~RMquR?o`SK2Og=Ci=+V0{e#1Wvj-=Z(17rFq z&sN9sQF?u2aD9{!9T&!Np7Lp;_aQ$sAPmb3kIsPq;S&?U_*i~SSjzdaqFpz@Udv2_ zVd8t1T#pF9Q@PMI%-hE97O9f?)lMR^Yw(X|Lh~)4e=4U0;5{}?dXS^{Qk@Epr~jtt zffNfc;+VkzU;?H$Dr7hAbwpl%j*9Z=v$rKzxUZ7g3Pu?-aD`Zq7_vlri{c$WxJg(Xy#)OpQ&3IP&5l zb^(v5x((c@tuDIqWX#7xO@f5Tk~JS`-8wQX_CLt5Z~We4f|RMiOn&T!`LWa9kpRR* z(w3l7lY&uh$dApfTp6c&Jz=DF4+LI+-`HcP{5WYl<=nsOQi;?8#~B`g0+$>{#4txZ zgQy@_*cg0qiPG+g-0m*#(MZK|x$@fn#}C3;Keg;(NYy{LHOe zV=zEWz$F_#*|&;FG=m>#*2%~{gXk1>g#|!t!#a8Z!#Kh~sNyz!93ha9H=>y#s|qB*%m5G90Mp|D8@5oijbqpjVz?wkD;V6VgS&ayZ4N=r=w9U6Z>R*@?mf3GQk39RK{+isRP$izU`btvHd~nt_cB0@bvn~ zigOgA6<=I_0B(vkH$?%c7*7C>!n2G#_r*gD?ZI1AQJ$szN@mRiacz*$IrS)TsBq40 zYVvicQVRyXa7-$tN@QLN_9y<>LFE7@xd-TTtHKE?zFfa-%+S;=~(prK^p{8XYu^ zIMd^lG@U!~T`TVjwqH`MTIDwz96iGmbK#H3QDpQ(B4_T+jqa@etuBdZO54B@7V%in zxI(>YWkoT^S98f$b|oYqO}__DPeDa{_5F`S;(9s%vVhL~%MVYL;q%S%CM4zho8X>Y zoo~EJ18SOnn=lW&3GjUbIG4{;$%B-LUGw;n6gU*5^M9X|l6tkjJ;K^N3K{Kze*=D0y_JpP5)#U_WAgNx3Lf+%4r5 zY5D<+n0stRlBo<1HQiNHWLVRYs_9qTB$GK}NnI#z=KJIz)2x29Q51-xidxkSTZK5AGns;12V&R~ zEO49Y7fII{iHUI?pl#AWH%Z-1QCKd90AJgF;*MD65_fCBBSE@3h|v?xfQi~S4V#*q zeHi9-KYVpdBsb8s*P|g}V!&T~%s)PPa3Cm-5qGs6^F|{9w!dA#@y2DEX zoKi;ytSNkim`U-q^wEjMoSHtqo~q;W_}9+?ITlFHjA-(w^~c2|4&w z9KGQ?^JoAxaw>mHq>)o)r))$ulv6MQ+j;Um3OAp!O?*Tq36}2zZZ(kyTij5(Wgx7$ z!m4helj@XlEMshnC>qwG9?Q@eZ5-K$E<|nyh^H%K?66Ft;I-EmE?x|ePwwstG_hMi zEWeAfqis_=T2qayqBzu4H%+0E7&vEe#ND=p$&sylfTpsiGA4T@u|LWNGYy`%J$)08M>5~M39_!g??6D&0>!hl-umYoI9RSBB4DQ~9gxEyW>ga2t zlctbXb8l_prN_B>h7mUln^UQI5l9N4pq2s;fZ?G};QSwuPW*W1uXcR(;Ft9M2RLR0 zMsAM$=x}W46S!INr|{e}Rc|Hn&IOm*Cb7K!?(_DGE|kq(;Key&>I8J zIxnpWO)sDsPy(fP7eD275BSw!NG-l_d3^GfR|IIjQh2#!KS2!4B9bwno~!9v$bDnE z_Ye1#pW$i+O0+h=`pgElROIWz-rDMOmEqrNZ%rYl;Z3F0(HzUZ0yPv=mcdeg8|*76 zu@q2zsAlqT_uUhx*+Q|@P{rZmZYgf-_zl*%?ck}pA`0YlM{XHdj(rx3`i@*@H3-@# zY`k~mYY1zB<11AVAY-Zl9VlraDC&izJ<7B-gX1kF(uOw(9N<~zPN?6eo1z)B$J*+o zmO()Jo_h19HmyiiDaj54n&qBNvH3V^|ci~c#1V1#WhTKJ%w(6G%Zd^;lDrlm=Q z+7H=Zcn*4biqczCZYI^3ZlY=J&99-iHHB;12OBkzu${1>A=uK)u?0zx!-t{<;Fsz) zx+AHP7d}%EctQ_D@0~b(`7-ckafrgbQhTi8b-Z*V&YlWq;cDND2&u{yh#uhP(FA~O zMnDpi^{BbJY9?mi!Yudh`sl+Yv>89M$@fluE zqdD5J^~r8fiGoq%HXH7 z{Dl|B&K#BuxH$dX_!+|xAYl}Ombl2@3xqh3F#Vk9AD(SHfsvCh69z{*2^2|TNz{!N z9G!sA6db()E+1u7#T%$fA)fEugjKtXXp6|?gb zzKo_CyU`;^lkCm%ZJS!|pp;V62qPKM>H#7p8yTZ1N*pfK3@|fEUc!`6ZVfWtu6}7- zM<#uUkS28;7w*kCEB%3BE+ybGzmst>TZwtgDV^IQ)m14U#GDn{c6igFu|)w|&NKx<*~D9^e{ zm81}JNJr`PxX7ydetMMA)gcM&oa9mB+8j_*7de%|b-X^UIJi1~CZY@bfNqvfweg7n zcZYK@7b8S5Q~hjvtwXN!e9xk&fpe3Mw$}#huq5yZ+e8 znf&8Ix`z*6E!ZjdXfJUvATN_tx$33{7IsK7(T5s<+LA2Bid;Pl2^N|BHXS?sCE6tZ2-%19HjIk&!fQDi}Ld7m8Hd7 z&NIjw@w}O>oM+8OrUyK1p2-O0G&rX{GDe_~M+BwodH*Y`n+sd3i;FDWa&hT}g)2^5 z7%2YBR?|oBaC}D_NRDyd;Cl) zMT%2Xj!hNpTo4>pNN_f15`#SDot_0uAK(;ign9B@fB~vWO$kX=GwzV*~iHw&%h108-u&-Ex#4Hq^BwuVJzcT8aK)W`-d}V8W;rhZH z5?-0oM$JYQG2PIU$A#bOyuP7f)LCtcrop^jzS^K(QZ)FG74Lq<#02+?p|a*sY35U< z$FD#gA+FAC2gKF*dVu(PL$h@_Gk5?TX-I}_oC_QW$~eK90xgNVTd5t?It8mnB$;(= zZfq{ZUYndjO>D!zJ%m+M>bMyo9|t86_636vAH6ev=HS3uT`-v(ly2sJkLr@Et@1>O zrOu$1<}52j8^J*X)eCVxLKqXy8TT^TmL76Q!{M|3mU=o4)N-fJod_HroFif-Zrjob z-z=URf`Sn_KwYXw`0@s+(6|gEypUn(yJ{-#HjtKhQ5_nh;$9UFh47O`K#s!C|NYVr zOC#Sl^a}&8_I*RoPo4PTt{a`*(hv83tg&PNF|{h(h>xG)?(e4d;Mu~co0h|c8+Yrr z@o1ZIeP$aTn8=ZVG?P@nS@ zG8d{KRlY?5DA+a&%kLjQbM>mQXIw1|mcSnV7pVgRrsZHLGyNKQ7RED1w&u->Fs=E- z8xPT%udfMDgmQ!f$VFBni36TK6jfDx0be7uj8y+Kl%moG=ahFwL9I#BcrtpebGsr; z>pb(uL$%HT>cBOCNZ+!`T7awSL6R+RKzrD`H!;EJ)Yf246dx}k)S9~Bnl~MgR@>y5 z%YU6YI{O!M?5{~@Vq0jZ>a6nPK%Yf!l{MvTKdU!C+Nx^Lidxi}OC3spZUIHP%2ybo zL$Jbd*1fSien!s;94l9xbQ1!Cdzbb76jFxs3d6HdCR6Q$D~9|)*38714^R`8tu~`t z+fNp$Sh>}nUs=yM1mo-#p;kIN4$L+j-_yzf*@@?9QQO#wU~fg%4R}wKY7G52*-(9h zK+)J4KeM|l;B~iX2f^{f3-Z4#&%Ln4>{=Vn;f@SPG~HC=${20t zb;p6qhEK0e&FxJ&wF2u1);@&-A-RA54rOtREmvr2a ziLm)69Q0S)0a*5-=D==SAXw6&R#x9}&=g-7$k|5=r6KTfm%QfYN}Ruy8P?nm!1PsA z&nb=C00H@s#SB4Qf#Pax{0t)?%k+4!RPzb*PxJPV=AUpV$k`Ht&3AJg7_S+S-u#dl z`ykGQ1QYReoU$F6xpa87`)CSrMo|c~Jk?GejB3F^CASej%J8Bpt?Do6QSvGmHi8W& z2#0S3Fab7T`D&m^MVvM8+;JeO_OY*lLDYdWo&U>@Dp)yQQWzp6o!S38Q_{$LhoS?2 z+@I?G+V0;zk?l-%3`#rFKkWOZ-f!>uo1VX6lGPFZGJBKg~mFm2?``UfDPQOJEg+x zbPDEo3Xzw`VIL>AO&TOLryGt2f{#*JfT0*ZFgn#qf!hM&ZHb*`bfI`e15K>qciYbS ztosI@bV!an{lXat<}ye=73x=9o4XE)$V`T1)~%>bZVe2e4vZClsM5(8mAL(3FX~r4 z?0jkBluoX(=N>U_R9i*6G%|<6&JNfJ70)mz^CN0j?dP%#%$z15CmR#)HG@mf!`DM`g~=k zt02`CN1+41-&@!}9G|##OIT>P3U*Fx0xXI$uOOG=UW5Wy-PqsOvj@7KG!Jc!wQ^(L zVi!m})5t51in7`(uCX!Hfj4Em&`KSD0B^s?7ly6SUwr(*_(U!zz&lq^tiv*gw=$pF zM_$8t7J(mDfECgRU=8k>j)}|-FG3D;wuY*eXR6?$m9E}c%(OD1I(X8;L0_m`ZK!D; zA3~M<-r|b~6Q}0pMCND|Z9ObQ6uB>&+HA)9Y^)g7@3=u;iSeOsHW^x)rvQr+V!={I z)W41O1nR2-0nYGRng(&e#)e-9#n}J6YN<~QmZAa~!v`IF;g$R26Rb#uC1XSjXT+rl zXO0!bH9Cy`UJgaB;gyCOt`wieij+arCcojB-b7Opa^3PqGcS5$*IEN$ zond^7`50L79rSKI&pPcx$uGx+slNNes zdj3w&a1T$1&DLv`KZSVy1WEJ)TP>03;qLvso-vEdM^IuE!D;CfzDOmJu?qf2mbXP? zt(-M)VLaH_gU5mC>Q?&3#!z#`!I4XWBY$b0z#yd_K)psa94?;7cTJp{o+fR%b%uVr z@U+NJgU{!dnZpWartT{Tc9r-g^UH5+9u~J`uK0FET(kZfx$-i{8Nq?xWbI9=Ebb#~ z1jQ!tr#UlhaDc=Ls}HOW9+aRD)lbW|H5BFxBU@v@S9~nrIX>~+a{|#n=OiX4X&rv8 z8>TxQWH=U!1W!7+mvYBb4xaig9E4fK9;pA&aS(cg*}&=###S94QxY(6HmnA?LkG)G zQz)kw!a2*U6gTo6;}f@U3;4P1c*k-4C<_nLKnQW|4E^ENfW=reF}zXECU(Y!Qq>d7ZX_V-+2Rb+pG^>{iBwJhft zt_uf4m6#uMUT+jarV@}juh(A!M@4W(oRX>9Ym9aoD(erhCO4!A?|`P1b3@d*Ll$;F z@^{aCaD3wMP>=%;3w{wP5E!pRQys|&4#XxSglW=BRJo06&@9del0P&}5u|5r?WxLg zp})1xj*7h6I>6S(R0m%QPP-KxXna9WFraw-N@33vvSa^-KUz`JteHx{0i-&qr00a6^G^jjI*K~XrA=Y;@W9Dilv)Z>qfy1AQ$ z4HtngqU5f1eic8vK~stEFyHCQ!?uJh<_p~#r5qL7DBEezi*ueTLnU#mwIS9)PQV`w z`buMAf>gu|S)d^k7dJmJ&XYM=TAke@CFWrXbcmRnwJ$bh4RPbwlF@uk^>I`j5r=U~ z*_(nJ1e#njhQ*sqW%hB*0-vQo{1l>MnWJA*BQ#|=W@u!FPuTzW;9qT+?}&g`Py{Y4 zHbxXxs1cEG)In<{gRCEDMS@pV^M=E1gKPuVyE-tgXON}CeZTY<(#YJrlr_P-dMXq?k4m806Q?y0WQgeYz_*hfiLM}Gw z=E!crgSPkd2<*L?3{H+l9I0xV?$6|ZXv<+~V zQ9WVA8` z2wg#NblH(NMCEH?13^BDVqgAk6Q^coL^0-aVa`SAG>$8Taa)#gUpvsNaE>EHA&~){ zn(Lv;5^I>HvHB7^D$sW@7Wf$2(-Dqubw@}Hsrtxj;zEH7SlFkPs*8XNByCry#d#4MNVaz^l8I54SQ(Yx~r9X)&4`jh`(d*2cp*Oi`GvSeG7W%mSzx`(7`UCPrfs>v$Wlf|N=bW?n( z+>%JCs8(y-3SF!!krfrI%Jm>cHyYiyifY(NfFOebIs?oA1J7czOcp_U6(GnWyJQv2 zVg|D?vdSWx!7OIczf&z>C01pyjxRWT^! z`?#eCMtwYW7O3;3OZ!;Mzm($4yd@Nn_u%AcD1o+O=<)QS{*6w5{Bg^w+2Z*X$-upf zL2$}Ebp2psrk6aG{+xcGqkQq`$qXZLbT&9G!l5hJ+f<^~&*CX)M_=>+L5v}3F;pwN z3+}*$$p6Q$;|sL^-#lLpyc4>7c*CI``|#|8RNc z^30{by7bMl&o2J$#l%JB*cVoP8Gk?{dXE|rr*T*skyNc z$Mxy)>GXhQMKYQ6s%W0oY`uO|;o0U0HjcWug@rgwE;o)**uZ|&az+1!u8mJcxHy@I z)DvbK2)G=<^@0xZkBaL>ZF$SJ94C7cWCP&E;*!pBuWCu^3drXFO?)f(#Ow--CF!b= zn?273kL#rZ^8fE<%h^3qvyBC^Eoza$+Bt1=dwEWS*QnIkN2^iHNiXnU_+5ia7!AuZ83_wMrcgY=Jf9;H{eZ{LhX(Y;mu;`Um4sLSjPp*f|U*73S0;hT5Sx`iO%kVTS^W;@b3PoyM*I@a@CB(EbH3um5I)_YNjr{2G z49pL75~grKu5cA|!zNF@%7|cV4z>0D_;v30O)ua>A~GR zf?#wQ$Lhku$+|u{`K?3(EMlg#wVLfuT~aZ+3gDu&>ACi!#{@HH2le`$6N=75%P8`K zc!iDbYg49;E}_@ckfHq|^!OM`teqCIQIt<9pgl*`q5_&yO#MMXd;p%3$rLNSWsQjC zp>{lgk`T^9frVSufwV=KK8$HB;aK|veWEMD|EEquLMYf#A|kD(nUySYQVi*=VooPG zS6WB-z0WpcOou_-@1v(hFi+EmY3Ku}-6^Y*Z0^Je@cLbaW&h`k^(xAXObrO{m74C- zG5fKq@=H~Q;)$jhD*%zh)r}^E4HhwVJlu}!QKJery{_5l=_R&74^K#ez9_;JxZrhHU7IJjpBBfQ zv)pgrO6K}rKEm@Sk7% z?}61ze{tzAL~Q!_<77tM z!bLIBounX^Ix4S~#V!Pqfyq}e%{m!_G%Iy-WsasRYkWBcS2LGGyHXl*wE2WO%$I;b zHp|*>rP4q@c+_@rSq$-HOHI_;NozZ8CLum&7N_8ZkjWF)B5w#rcMwl;u;MU zq_m;;&xeNHv?rM^hEnn9;$o-ZNwG{N6<>hAxc#XfoS0TM%yj#}@6)s=IvdzP9qm0B zO&DEAj1=nCog)P7cF;y(ho=^Z0;;;|EN|HU+RRq$#wgF2z7a*Ga_lRLijDF%Tl5S~ z5hEpjjj~4Lyg9S{FSIPi{N6~c(>DpEv%w;RN3DmGKIMSFKGlBMMa~g{Yi4En-o5m_jU}y!;wL7< z8zQVOrwiL~SF-sW&H^V=?$c?6Qw+@n4(ez1o&o4PgO#2VyB4oAO9Q$lC>34+BQKSs zHPi`tR`7TUnm4MMqES@ScGC~?&toXPSa@-$-}ATu%SC zVYi`*9r{*b;0p;3-Je z7;P7b5N7)@LTBRf)Nw1r#0e zWu*l~L$UQr?TFbT%+7+!9K5&r$RQ+Uq$KC0GM}qds10|w$rNvP#toyxz2_9vRU!etlxPQgNJnm6p}3*U&qV2em4BN)K1J zV7bu)N1ekcKAMSZ^-_i5xpF~zkUuL_8#ji2pg$Iee$^=uNsYa~1~~Le25?toAr|f$ z8kFL6T+TsXTvYl0p9S8DUVeP(R~O@NtX%lT`HScNIq+HFe+QEI_fOB=zWmkYuipCd z(*L=1XY9X?y*0*;MEVE%()d(1aK$5}ytvULd`g3MPeRSDs-B#flKNcpMEkg)>gn>xgmg^ENsb6c2YEY(8J`((vx{`(D_F-4qR{l3fw4)KVPcF^WLt zV(Xjw!o+ki=-|bmzSE&ZplukNzOLL!uie|uXg9TJxbx?}oP=1i`^lFEu+9&8LI(cJ zL}C&1E;^+YJe$Z;OU4#q9VG@@M+nU?Gjl*Sr-7pZ)&1yYAywnGLZewL!?%TG1h(S# zePe%E`hLYr1G4WG41hiYCD>}j-AJ^(^3J8~9EKFaW-~0N*-tQYu33{KVi{I5)Cwt?p64;q2mcZQaj+B-} zilsA=R5+PjKuSw&V*2RFfxM&kd>8T}+c*v|Rg)-NLNF4;#!y(=fP1b`##l#;9%G1y z9#SOn;zzpZFs)^jhdj_a%Im%+9FMZJmx6GN6AX8yd;;NcxyLcnJN6==M<=H~{16B? zPwsZo9zJKNbP4lFBgTR6jyx_Q1jn!4IO{x~nc%53eTmnacTb*qFYB zqU(dOH#?84Q_$6(k*(JOTV+9x1|6DrzCps9{lkHGcW&P6#DLE-MWNG~b_^OsGG~Nh zr#+=dCZ-P#m_2S-g$Is?6N*+a{(9Ir#jy9=p(;lZAU^C{PC*GOpRS?W3(Yg=Byl6u z2r=fRA&z7u0}Q*S0SMxe-s@BdBc~^xfZ6^~4|gKSp4DS8GXk{3MxTbgzc+%0eak5k z|8CA^N{pELTq-VhkB!U zq*t91&$U9SaF|88AagexS|x`$g$DbLv>Xe;bpjKO!(dp@$qi$pR>YS$SiI8M;pp;4 zq3p&PvyPF;n2JMSy01WaL;uFqBF0Nf{{L9uX5j7UTc3}8b@9h<{N06Lo_~1mF9SDU zn-Uey>yOr=oWjc86m|Fbs ztRzR6@4XEm8z`1YXzL=IFcOGR02NENFX=0c6*(BM*s{lqDBT1q4$`}dtnops8aa_R z%eRmLhU}mN^pQaf8EJ;nD2aJs#r$x0+tlT-2vWnn;}j_N4lfM1i~-n)MxyAy)r}4G zRKb)+^h|qAzcn#kC^)cDu!lGqCIC9KuQtKIEWEPdURV9*n3l)*J^Tl?N~y#gyJ7C_ zjfr92@Y3)w%pyFofCRb3VhT;rIz=gZp5ZLQ7|r%IeVM6q4lFcV*WHp7;h?CI8GwSs zvdrH>49@g2nDui{kpgskdwWaESB}a=%qY+F#>yyfdTH1hWibP=gf4>7Uc7=XEJ^s( zBJJ!{K^J$UsVLW2~}hy6xu2V85}QB*+E0Gxa%EIQ-+aTu4mx@9q#lw1=Unw zCU*c|={KW+k($p)u;YV60gMj}2c-ej97!R{-<@5YT_#m#He{d&!JNe zO4oV6YvENi2FlhD*if*2qWRi8`B@%-ELm<;4uDI?cO~3B$$B8cGN1C(;+|G`&ao2n zMbyL98rJoNC)!4-vI{kn-6FB(U`~m`T^ykMjWRst*1bu-s{~D_iaC^Us~JTZ*7ZeP zUxA?@6{wD44i|DX1z`H>SP^opVL>HnV^GMeQVy0VAR`l$?;s+~xK!>jX*8xpmdbK* z+@zI==scs{RQ9tdZmBk8G8<70S6dcKuh&t&Qp6ZVXll}z6+JQwxd9Z4S)nk$yHrU` zuZpF@VN6dd>Gvk4A3tU(yWw?u+_DFhDZd6%xmLinGDl8=d?<)k&_>&)Ci0bQ1*rhK z;qNY0&)b=J7xDRxQ=B2*hLpd>d%_z{r;||(lIVgrr&Yx<);NmZI(O+`b-;V4{Vh`< zC$%E<0FT;e@`<-~|X7$_nz zqvNUroqwoUk!?IE`k^1bJq2o6k7X7z7+Y0B&i|5?F6L*Q1DG$4?hjFyW~8tO=lcT8 z=5T9O|G~uc+M0v;)^xkCl@fN4jOe0m*j{FZ#*!A9(-!8mNW|-!6QQ=J$orX7fZki2 zc`bAR9O3X@Kyp{7|A7;xsO&_*1s}aPeXO5z08&5KigmC7k{2_aYq+Fkt2?<$6XCPx z6lGzm6ZLu#nL(Vsrv*6=S^<1#w^DA_JD>WS_E$gukG`Jlhk?=G_|ot*`jR}chbUko zo(Q|fttc*lzK-eO@nm~ePftv%UR=hJ|EO*B;xZaFp#~qyJ#{KoWGUK55!rcC0Xu_? zJTz=9p#0>9ubcW|#n;29M1ap@$JbI`!jZ#a6tE$>nT>(oSTY(}I0@>vS&HpAa#~qO zS4~4|xy~0eXQbyZo!IDq?0^7N2xK4$P>nVe7wOhFI$^us1PjAUgq?ycTt}7(Ds^c2 zCxcV$lINY*)&}r*JdPTgZvIBy2Dlyr+26kK&w)#UUkBd#{mVanYh~;oUb}z(*~LE( z{Q8YwzJBn=7XKAF{jc+n)|Q@~n7V(TCllQku(YQrs&ut3aFCNZ$#v z!}JY*cex0-^&LGoF?I8%gEwy4qF~Fra=(-KK z-W(+30Q+sw;vpFTs2p4loY6tJBpqqY4r$K0cc@*d)rxuBL%I-x29rSngzGNpxm9a4 zt0SOIt=q%jDa_b+cVx#=H^pcB@5Pfc&^aFAYNb;nYz-_xH= zOl5KoIg@Ky6W0Ys$h^L5Mh2|*mBG(|Hto;7{Xb*P|n1}RXI^Qxj<@ZB3H zBniOlS}(YI-GAnQ^!nHU$i=f*LE$LFOB7fpfJaSkn_EC%Ms9Knjwm?`i00u%zJK%;(D09@cv#~ z7(fhB^zKA-snbs=^RzjWv{lpp{lrwxOK8Q=8aut3M%b%XfH8*3K@@)$j!ag99Z@bk zZ(uf0y^f~gL)>aph9$g)`O*l;^F9@ZSh^R~5lDiT6>(RDD;C6*Y>e%VO8o!a*#8aS zf3N;|1%X!(cm;u15O@WFR}gpwfmaat*9w6@ycl}_>i6^vx;M>E(i1&4b~5!xo3dSG z8lKzOUR${x+DeBKE3DytfM7gI(pV0~KKbQbu~1)T`Wi2yz+5h(^2)l1bab@ZW;#04 z?NlxvYgI!U727#OU&k08TsP(HxK!}>82D~)W!H1VWeZyw5j)h4xIPP?X%V}kNSjDM;ZY7O(}(9RCf zLBGY$4wrmQ;C$`cbrk@m5rfeK=f1W}xxw{3MCv>-?S~ zwy*PxCJkHoHh&TN;o5^t8u3md8`S%?qS|JjuM-v~OAhr-u$r4WRZ=h5jE;%kjH-Bs zdA~`d$mE{EMWOP2jeK6=9fRE{A~8;oqlfFgE&NCVMf^+QB&xiSEb8>DL>87J;U$D> z6ASF||2nv{hS6?$%uJo9R0|@7x?I5gv*PnYiTVF;oZAh&b^o2~7e9Rc`1~*N!>fPJ z5CSLBe>Zt0*t(Cyb}Dc&fJt{3PgHj-j=JMvwq3~WmyvS5cdTVtuO;zbX<>}EF6Nco zeg#$hNGC@Q9OW)gQeyI6p|V9pYTK)ryHQ{(2%NKPNVhW5%TXGJzQ3R}4WnHec>F-) zD=L)|U55F}0mlOi?J+|f37R>%hzw$b7V$RAe2{bnK|jfa=@1*tnj;`df@ zPR%cv(;njB->Ov_6-*wU(^k?qwEZlS#KaA*Hj5=;&9co)(GUxwRhI~x6(l`AU&%EY zi-meYU|7rfju5TeWQ6;I+Tn0rFclYS3%0W1$i8$ z*?gtS?-5KG0f7|7Dl$+BKS{#pVN1ke5rDN+4)769A0YL8p7%IkYn4Rv{kq zmCz4SJB>M#AQ>u#0L(!(q+)D{cw%b0OPS+Vg4#0Qs^LlWF2|yBkfH)^uAbj?FWX)- zt2VaPn#`xmxl%I^ayQn)I+S^1=%%$CJaxy|MZSW2fh^q5!AGDch+MP~M_Gzu3j1Rq zqjH{1qtvH97zTRdtU5v`2n-{4xVwp7$CVe~yY}AHhkAV~a79Ip#>QGtPEhe_qL|Zc z5u~HP`>t|mYmdm>SbC+p2HreYC^fv@>g17Dl!TZX=Vl@WikWH=L#povs7cnlCFrii zt^Ec$&r0}W$gCeC66qz+Fz+)O)YMYERsoE9eID3Q8?&-6fxU_#MHbX+HFyS0YJ60k zqV;D*aAlq-ELBiul5Yxw^2^wgt&8sSB9;V&SUP11^;!&~;6~zg(H!b%V4|o)UQhz; z7lL3k_Y#;PVnEn<5uy><=Q65X9+L(>($1fvD-uQw1MHVKdIYIbpKeeZ?-HSdmAH69 zmj79yR^hGz39_Aqe+zruFsWPsTFCpLV5A0f@QSkq8!32U2|g$9?JeHtAxtHGmpb{T!qSJF%hi>S+%)0}gCy0?W*P$`nGm zTDHE=&Dr;L5H3!hQZc8J0P#Kn0y$R5ps%2}M7*M45{T0A=kkbY=oW+FGgNuuD&;wYRx;j;wNoPt#Qb8 z$W51Ae?X&wVtpCwq}i^7ImNEI=1Q}Uy_288yu8SgLcQg`Dc>y#VgAAr z_!R)GW&#mNRk~8WH?u@=VE11FD;P!YSZh&e1i8yTWmkYd_Q9?0*D7VTIbz5Q%aNTyQGCL)ma@%U0A8baUfa3aXmoWLMbrh~4#D`FtB1N)Z408`NHrA2??_Qfq#`l2;<2+Xh*-xJvHt@{fw%ws zt^Yap*_*o;{u)1gzmNI*#czH0{j1A*&gnOP{Dsx0d(841qt>5pRN8yujm+e-PoGvw z`QqoB-<`U;oW1h-2LDT&*m6J*_oDKi@d?w0LurQkgn1Y?4+lDs-7XxuFzcod(|qkg zp~1oLSvfx}8xyCHc(~;pCia_$A?nWVSM)GUgW4*(TCKood!yX;Zi8ESzSeGH#8CJMeaojN88Cv z!G2xY#%R(Gw{+gzwc!NQLTHeA==iHsnK3%Nn1ws25wxYQ04 zd9?d?*%*EpSGWa(VV&GIIMA)C(lB9CEYq*jAQ;^Ara)o(3ZKYHkK+q_g3W4qUzGC& z${6ggDs6kID>I)8StN;=%o73x)sbkV-jKTtzeYW%=t7*j1n4!Vj@D<{bV_#7rCRdN z4xJo3J0LfM;N@!;q1xCsF848&l?M~u!eTgMoPES1N%}TX)E13nTyPSo?(e{S@jm8a zCG;Xh$p?m~TrD3^37ryqd#lF+fk+V4C>FPO*XG*>yUNyxX0LA+hyi(HqrS9 zU8?JONKQ zeG`bF)Mso*(8LXLo#JtQeO;Sb&ps&>$AP_3KLx*u;P1qL*6P)J zl_UJ+-eFy>hp)@-7E9P=U+Rs->3>HLhls1Q7hQRdD<@ZGCMRe0hYkWgQ6pDO|8Hm| zy^@|^S=GLyZ7a9z2K+o(ZCM}%mM&Hc@H@$%TdMrZO;Byt_m$U`>5ve!25G>A#@c~B zxF%Q93Wqrs93b8R9hYJpCvn|_Al+XVBEYY1H+Rhy1z~$&&`A*?UG4Gm-8)2(64&e) zhvV#6uz^{;=67dt_&j}zQtpG}>M?Ft{Ak==q`MW^3l<>y1i3Scyk>=POp4+?Laoc? zy(YrT!jc&H33nBrp7}wxc95O_2!AJmu9%;;whWR1!5}26&9K!ihD(uoV5#S8m8!gW z%LFtyU;HdKqxJ)EQ7jGn*#BdX*<^{TNTsisO>yy(7{lGyNFVb$o zwe?_gdlNd8_{>Qim@i$YTlQA5oP`E~b532V@2hBgu0|h_feWs5Wp*~C-NxU7rU#CS z#eB2O|0vqn1+s#0>cKRIfep0+uLzpNq&V1vR+JJGa=sOGr&fUGLxqD}^?1Ho5vMBV z^44XPC@FK68p$p9w+$bUia{$1?zJ3Z_rXdqvJhNZNpEj601~p?L5D)@QOdOj-FWGEEjUH2EVSyP3IRqTzDZ*ZBnG8lG zrI+vQtZd%hy0iA9^!DS>f#@aTlK``VL+X_#s7{ooTC=f_)nGhlkYE?N%Q9Ic1cfJ0otsmASX6~%8Cf>{r8p^W8k4`DKfB4}b(W{WA5 z^zro~LjYgA5_mvwrZPRH*%}%k(O(7~*oY8FzDM^7X)t^wrQQV5T+U}j6}8?*EN_;# zAQX+pB0v=1P2UehK&83I$PI3~ll|;C$a+)k1iyytz%Mf^nT#AK^~SNp#bm{Kbc5xl zq?HvQm66mA0RzKIJbqw?mx7W2qk8=DWws!aTdXB2W*UH1%=(O>v8uyhGqI|!oZR{L z$qnYJ zh>IybziB4!7!AnKRS4h%jVl#Zh(pl;0{OtYhtx16lqmOb15wi=3;{c;FF5n5s;ZnG zT;=O5B?I@EnLW6)oY_b#vgBBIsO}Y}^gxH11JR_`iB{9 z->7HvLvo4S$-~puv&7muw2iSMoLCG=68mz{@k5{^k#>~B`1v5);E!A;&OmuF=$QTR z8plpTeF^cA$f@r6~*W+$s*>WZSBWrvSE^9&;y8kO>0 zm@Ef4-pqTz0t%uSELehikK8pmrRFT^QeaoKy|VcrL;DOJ<{+!`A(S^l64HYyGVdwc z)xb$WQA5PjuSXhI(#2+jj?kx-$L3rU>_64Z4X2pPC4dFP-U4-OtZi%aNPC+%R;~um z)wQ2oBRMte*Os(vm1oy}id#)a0cBEK;3+KK_dYQk0Oobm zPy{nd3i&U7BzJ3z;l;4S3n3B~S_=bT00(v^nL|odwI1jP920>;rzU(%=R^m#4aAg6 zm7|>sS|rjdfrx{OtY7=?HOU~67!f2SWFPuYGVsb91%luJJPF^~eg*Lg>q(F8);^lzS2wOHYLy-IpqpLN^TQEkclXIErc%s2>o3HU&Q9#znaj0;E)_IUjL#; znrqCExFTXr!@>BS2{vd8eIzi9Fg9X_AFiYXw$Xi&2sZIgBMJ)wZ z*CCZ#upa)8kc5$nYn>P&$T(u=V=A=t1>zKePpB)8)ZP;LyxUHq+fbQEDz35^{x&Tt zQ);Lee?nGMRA}s1z=H?~k^^vh(NE}vit|(I@G>Io5TUgHYKUe5fCSopOm});k$m9#TewFtJa8g!MZ!VgW3J*W7#HQ4HaLtfdmvNss}TsJ1M$&cs(- zManDg8aeBgUkMV!H%$Slehku74GJD^TEYOgDRbTcBtBe4vI7_AfTgU?gr&nLhr3NO z%*#Dg+o7cPPUA)MPV;Nf$%GUP$=B`Bd~|#5o@VAiI*5>jxq}QU9FX-VO16+TIV;s6 z9j@_rb(e2)c;PPRo58yKjYjpkwoK>minM$6FZp*X`D4JO%y?qA>>nVtVlfQq>CVVx zh6DFOEU3Sz2uSz5IVYo7;ClH z*uM!g2(iFyZ4UyedW?<-kO;D3$IiONo4f{oi6Wft?2haZUHr1rw6)MD1`{i>E&*29 zKIlK)dB1`e*^w%`{h;GMSrpZ^zAqg?COH@D&;mR-6;5a(SyTJC;nV~?RWqj~l&^Mf z+27~0B?hI%w4t$6$d9%H3z+`CicO0F)v;+m(n$zW2?Yn)Vd6CiPf13hJu9AuoOmHa zOGf$m2)APhJ%cE3;$XzsqMn1>V4f+}2dN}6SGhdWx*52I5Q(BkO7t~PArc6@cB8Ap p$MXHyHq)yDANUJ?!wY-{5K4Fce_|<~LNK3*Pzv{~r(<+8Yi)Z-nf6eg7=Y{V6Sz+YQ2M0$!);~M&i?w?k z-8q855d@APa0G!P2pmD+2m)_<2$VXH^`4NVj)&)Sv8|*Q&!%;KGm}UrW7*AQB6U}< z{HA+uZGLuRUfh_ycx_&+yf01;^y}$-Hm+?Zw#DaX*XFLwu1$u5Q{wW1y zKz}Zl-PLmST4c3kESE^9^gD^nX6_)P)qK|K+Tzme+D-B5{7rGvdi6QcddC#Kabaa` ze(~}$wXz#ciEHx<^K0|VbMxzBR=bx^WVJmlmD^0Ew>4dyOl(hyE6d`g`D^o-^4#qD z-0Y?K@~7GFY2|4HQ*+%tCvHd`LL#-Ty`^*V* ze8)G$7$z&`Qt?HX3y&4I361sem&he;jL2%pja~pRo=s#3f8xgc3mXKoOg53uCUOTB z9wY8PnL9CE6Bm~^<}c5$Q3pL2%jI>exg@#U6N=n~A*t>bnOK%^>7E9AaypyaOlP;X ztoc3{ab~jV4`^}L(J`D{HWt@tm53KtR<6y@F1z~dX)!&Yt$8~RLQd#845w;uJ(1c? zYMYsSHj@Srs=Xizl-0Cb{YQhAuXf zvD9upwrk^33b5s35H)-9PQrm}Rg#?z*kh|VV1ufU!+2JU<+SZhoZc>hscbAR&AXtQ z&urJd?^^m;tmRA7qJXw6bek?*k2Aim#V!V>X1jV$Tt8oHfmbZOO{wB5I=S}IZc-fV z=sht$-cd@hWp2LO{I}D!sLgMUwQSI`3&5$^o{f4>%Q&lL(wmuB?vAxEO^RT5^4VmS z8x~{LHxFWa$?E+Z5kon$2d0JzByXWVM?XgpID)_t1dbqZ1c4(696{g+0!I)yg1`|3 zjv#OZfg=cfxk8{D6YCoNGn(G<(f>O7SEGM65bJ-YudC;`yFYU5=emBobF?EQ?6qn3 zg@sYym{jOEDV#Wcyie%w>th-jJpJ_S?85csIsf&=m5i2+<3tMg0H@!aZu5%chh5J#9OY-`mc{c5)7V z5WOgs&TZ~P2MBed9FRlP0cBbWi;^@WD>H$pKN?ZgNU-*u!t*1(vGav@9tsh zvfKMmpNgx{!jpf6^@Xm%PQYgoPwP1vt`5~`B9+tmG%|M%^h7+C6t}fZGJQbvV~r9} zw{nfX|4w4}PSX<$`=g4YM#Gg8Gw&2`4*O22g|YFP6I;Ksa^qU!z9y~{0_I-=x)K^x zstZs<=CjcF=k7oixvlLaQW`#MunEMH)42qcWJ`;e7N>EExubs)Gstl?YSvTyq?o0t zH$6Ai9}P%hHBdP>^G+e%;XD3JVQE}AUP*oH(*bEUo8HaF_V$$NjW8z*O z7)iuHc#GnG4ETtN&<)zn9kXEoVABLJ(&F9$o45!-F)tgyN=&2}SVRJgHDzEIXNCX&IBZXFr-AHsNr@eJz<}NW@&X^Ksxc4Gbr8;^#i}5iz}`YuWp;tpv<12j@gBwF{Jk8dAFi znz*Ken(yUfbXSb!a@oXI9=dRwa=DSL_DaM=g8pz6>o8h|RL7mdbAyD`2@q`M6lsPv zW_N?Al*rRHGn&?pNys);Zi`z776pJBz-iJ3o6;@xF^cstC)pnkg@L%rIhl8g3j>%_ z@s*Px)Dz`sTc4H!SFog@Jzh$u65x~sa1hk>MIzLcH8P7*L4)@b+CH1@9p2Nh3#E6& z7!Z8NMfc^#}5XwPQBoWYhr7}Q=*V}-;q z4n3335p-eh+0(@>4aCnF0*GN}2mFt{gt)lDrpw5mk!MZfkfJk6z#j!=grqV+$}^Jc zm!wcoRf?zkeaDsJvo!!2h;D!r?`gU00VqEMixxmx3y4-rm{Vea0yWpR#hq+=uN;7r zbF0_SiF?{!I*WCI4T;eTnBzGyrRDb1*}EbkK%cUKm51Rni!bdx%)nIoJ}hjEP4LaE zSt!X7e1hXd5I49fkF9;iAh)&q zT9Q@;(H;1}Hu1h(LKE>l;7i0Kg5H?0*6C+;V5chY&#m0{*1fSHYq2W*Q8T})~_IWeDNofyYy8gAgB!HBHNmDDBq10huk zMvJF=eaEM2!!10wcz*7ZxSGglH2X>@8T-gi9&3=d%@}P7IAp(uPo~CZzRh5S$&xjZ zK4TZXBPL@9pbkKl)@v-2Nn+<@24fR`9QN@$uux*t(Z&1V#>^aOu18zIXbnn1>@;#n zQ7WJWAu6CqQba9w_4tlY6d$8CSRwOa*A2~aVa?!Q7Mne9a#)Lx#fT*VFotwT|Jni} z_L@D;4mn?8f}kNy<@Gz_%Eqj?lgREdiHzGs+3xJ-ebre}qn%ii{2?U}ltPtI2>Szp zKv<0y)o$PMaB;I1CsHlpaG5O5*V4P#Oc^hNC}k|HN{A$RGq86(!M9;-1b`4D4%f_V zPP3he?WV{=p3wEY<^UUKI2>dScC=3$C3_(44n#!TaI0WD4oP;c^Cn0zQGZB+m{Q4` z0zrQ?q{@L{F>=gz{BrRY@uq6j6C{~ZT#Z3&({NmvzLbS%nG)yO4x_UrS0TBvE?^qi z`^|nZJQI-POpu}FlG2i3*3>4I-`gUlrsd-PDI&jZdWER*J{eW=bnP9+6Tp&2o22~w$H0~X<3Gp2^3C4~%zzIHE0FM1+OB~>)Y3vO92JE;T z#hPxT2V6VoLFp9a2$Re2rMDA&d!RSRNr2!dyp}+w`qiKsRH}e1fITu22}^}cr|9ge0$GpJ@~UZ2vFyf1>{<`hT$hyZXPm|3Uvt{i*(~{^$Fz_P@Jd>VLZb zWdBfqN8exe{bAp4^?j!AQ++?v_YeENyYE~29`?P|x7YV#-+JGbzGwRUeW&~0*4N+r z-+TY8_xF1L?ci??erE9J2Y+(#V}ljLXjC{|?H;sH~ zL?786xjwQm5+0cx85kddm5C*zVbY5Sa4n#Y8hq}ht$B19<=&>)Pj&8G2q~nZ!CgugYvSN*c^-|P!X_^)OoRroLF_d&lR{EdAf z3tzM^B;l{k9zo%+%#VujpUoyRI{&48Aq#(DUr55An>}RV&&=;7^!R`63t9MreIW^d zYW9$X|A#)_n7?>^QTUVLuJO*fh0D_cKvVc*$CV^}ez5DLaZmUo>ka@a{3pkiB>bV( zE-d_kbq6pNe&2B=3%}>Ml7#=;>J<_`XWb#V{;uOn68@vrP8I%xb%#LvJB}+^_-)6P zB>a}uD=7S?bw@$3fA6@Gg@5O`l7!!|dMU!cweAom{x8RsEc_eCl_Y%D>Lm;R+PWiQ zCco~ul7xR{wUdN@+1K?L!B+UpXxBvN!nOJNjcG|nmw(~9mV{rUN0|48Uv=CibpML$ zS{DAf>sk_i+0k1Pe#vn+fZqSibuA0O=(?7K|I^VsApC-T7qU`N__XU<5kBR*mW7{p zT}#5x*-r~Zg`aiYjo@jYbX_aL|KYlpg-^JyCE=eso)!`QiQ{hAF9{!a+zku=yW=V( z{EVZmiZ}mb*R>-2wCh?He#&(%2|ww0nkxK+<1P{9KXP5m!jHSICE>>$y@SG!I_@eM z%a6FO72$t#UCY7`yRIeSA3C0<2>-xwmx%Urv;UKQ zAqn4XHj;#IGQSU?(Kp%`lJE^?qk!=B#`jntvha2Gg(Q5f(FmmfHRkuka=+TXkc1DL zjUvKp=J&*JAKDj^@W5;o7E0##Av7x57m`pg8-;`q4R(!n0Q>>))>rM%(pU9&o#5ZU zVtxy7f2I9ddf99d{GjSjDEsEO0Q7zPvy?Ym$T{;{FfQHxENAV{(mk`a zl%5Xb;JlI&R zJ9fK>X5R_>m2Kyh9CuzxTXwH-%)SHsyy(1=Htlwy_uF@XqT9|Z=_~AZ>Mi>YIL-T< zSMp8gmGpw$D|o}cgYEBm=aqcjc_nSwy_9wP4iLKLypo=?+sUi;9W0&|=asZt7hRXqtfOb(J@!4Kt7l!8^1EG^(gnL`Ao?!HJ?wAKxGt3$*QFeFT}lzhgCb$a zJrG~Wbt$W^ODX8+8CD$kAdShcOIdPVN&!dD(0Ru_kfh&rDNQ??s^=W{K$T}*m+~3c zr8MQ}8Ju+717*I`btymXx|E)B^i-a7+>`z4Y1d`&3D>1E;kuMX*QIpI@m%>Gj(ec6 zao45%xa(3HbM%ya#J5)0h{#v3%`QJa&~>r{wh_s%j#(drKI@}$()uXB-TEk6!>Js+ci(EA^y1<8yT{`uw0kJwi$ z7uVlH%airFzBp=qzH(!PTCwlRx;jiVV^0{Ohtl_h#?u1>{6iqxZ+wXK86U#E#)nXk z@j>l2J_L^$ACxZRgWO4nf3kU!|9{u$HwvTwe)OkCzi~8A2fw4ABM2Np;0OXo5IBOs z5d@APa0G!P2pmD+2m(hCID)_t1ljwBpVo5!{$-U(gn*L-bZxx8vZPj74QtP!Ez;wDsKQ4^^(dfT|eg7ykUmMVS z^oSz}96{g+0!I)yg1`|3jv#OZfg=bULEs1iM-VuIz~LZp0(yfEt~WS=(>*yHlxYgO zjPJ{J1D$U}`+ivb%d`Q;cuwJ8%Jom`016Cv8-Cx8UvdwiIsiWW4&j#~2L|!`IDW_R z+mGLO;FtQ3;8(=&1b#>H`viVZo0u}= z$h%lrZc>Zwu;}xEe8}fsUVMIjnFVFAFd{v_W#k+{U{nI(xCunqeGo&TI1cgp`bB)1 z$>+`^W)c606f~+2+_+*q!U#2{pepSpEgMhh+V;Q(-96+e5xG<)y+I&26Aa8K@YW3= z4#pWNk2|U|6O{c@P?196hhNn*K33=|oEq=oi5d_&UV7xgD?koQ)nI^w5Z@&u?l%*SBBlpHY$i528Y{aZOoqT^0Lt@&+<11xkg_p~w~V3srQ3&v>A~`ZhRijKFV#xsX6G@pyw3DXdj?Z$QPi5jiN$O_rE`jDZJZ787?Knv zRLRJch(AF1Rgs+Cck1?S2Oe)1&z9p+M))Y=Veu+$6RgSzDaYrX9R%^~l;%LB?CHAK zb~SP3^#N_gPVG&`>u?$%K?wyTQD;;oUrY+}u0$1CkxOz{ITA`FLpZh)$uA-A4{}&w z2Vg6Q?V4=6L~t{*uOzn9=d(FtVqV+TM1t1`v=s>m|J4%rX=P}s?F<5_!50t!i@YoM zHNHN)4vVo){x~KZAsGd-1jO%9*er!=@5gjeN5Y^6NRy;M1lY0TLJg!T{-}yrSoPsk zonvG7?l}a2;=Kpw%dmEZLSc=u=p1pzDmu^q#R|gqhP*}$i@%zR|AiZTZ`qIe7nVu{DO)HmbVgf<4>b>%ksdeG7Ev88oy@w+4{9bgE*f29Y{* z81)rO8ty8$S9~Lx*Xe3I2?31Ko3Se$}KEsYU zVtOx?;2GKQbyAx)n7+NBfjEKVV_Y$d1^M%G7wC%F>=^z`C z=XRojQO1@7k?JTlh^j<_f#JTaFxdIS z!m%&*hPwZ}^M||scqlg*9O&u$>Ctl|i-V&Bq5i#oBmZA<*~&|G_SFBc@>1cLzv5j<=$C4Ks#c&?D^Ha&gF8r4r2<+PtQv_5E7eO?V7w_(y;Pg|l^gYD9;lM>g=aOD;b$fVRVh%MwNffoT}EG4DV3^0 zkYeoW!mvheB~VYmFNKg=Gg^#Vxs|HI@XI2%QcJL_kz`4kks^Kxd8t%+`1^%9BMoa+ z*m+39dL)P#MfQ#a5hUN_DNU;-$}FQ`Ptfuth*$|Tt5WcnO~TBo@IzE{ZSEN*aiePD zOo+Wfe~?l%OU1KR;>@b>`w~r@*%GvBB+vx3;13>Bm1q>IjE{W@q-Q{9>H)rZlvykf1J97N2aMq_$n{PAI!Q4hrV&^Mi0Lr!;47Ui@jo`9MnS`HSE*d9{gX_+l zgcKR3yhDhYv6_NRU1~;tK^Upzaz~T!jD)=4P$VeIYwt@x{>VEE{Z8{$x|pzu*&xDs zR$OD1!!MSFm4UoYSV@7XnNRr~-vV^132{y&KOz{0vx%2XtBgsCZle{WBa;9RxnfMZ z1XaFI%cfB?Adf8d2J*AoE{q(!evR3Xe4>!Go9D4N+9gm7hiY^)GZBpr<|YmW9hk%t zWi>1I@|@4`{RoTPTG{5aVOv_jPGP>^{2ErHwFFY-n<>1_?_nZ(KEFgs#Ek))HzK#i zbx`5miXPG1;*FS<3fyQe1z?q8W!U+2%&)P=*HM#*l4G;AV15r!EkNkFMCE_VZUHz8 zIOmsr3uq8(vn-`Qw!*y(^S8mU85GUuV)n8{059AGDP*Xjy^uLbMNU{>a<(ldU`SBp zy2{^h0Ru@d0}?z^qNR%|->J>bleQPvW{I3lnSimlY-BiGznjR21{We{Ei!@v7u(ua z8ijGRO*k>`CsNz#ea{VfrH*6Pwr9M`bAKC2v=*{?R{x1Cr%93tlj#_|DY^WE+5icZ z6LM8Tu-!}3tnLW>vZxzAQ~4qEWXb*6V>n0kJT6N@#O6*!hu6}AHk?^Z6DN1r0V4-@ zqLJ6-kTNo_heNK4R0a}}#y+HkrC{;ZJ>RLNB?l6hiZ{!VC{a0*yI27*ikhe-34~_N z9J?CbuYs~g-P(mI+WGENRVikO7t`^0K5NMDjfP+$MF-mlc3F8E;2Xyq6y`#Crli&Y z1EF9DYFW3$1vwqrKclh|h^nRcCCA1X23QFOoT>^piH8p2yt) zxk^+n<7|?&?Eju)h|DKR>iH@P)p=?tOoEO8Dz84VePX+I(!WeVP4v zF!Hvsv0JymRVdZT6h> zo1Pji1#c6oy$%A1dO(??a46s`d`R0&a0Ur+BLPVYM?l}VPpE}nQxM1Lw?L{_{Gc`^ z%btsMn?g}DxO6`)uEN(S9Zx4OCQ>oD^1w5UUBZbBBKj^r0h`Uwi4S^C_)hKZIrgx< z2fgOAP1H%v0;k#bP6D(Y$ivM(#%7c>avUyNr5x@$&i9D8N%O3Hl_ljIf@-)&mKe`t zU)&Q>_$tqe7nv_I18ry!W1w_sD9Hbf*C%SB z3sn#;kB$np@SL{NLo*_4a}cuRq&5k!jH{qXEETVEdr<@50n~U@f=ambwzv9D-MHbv z_l+WVrUO&q3qRy{3a^+G!>}d1S{Qan4YeCp#i^&BZN;KDaklm}EL^oB(=x;;K^axo zDi8y)Ck$7zXrPpTY;0_C5s0z&|NRmLE;@n|LKvVC|544mwEkTi6SekwAl@a-)&YmL z!Gk#%A}v}s!5-R%-FUnFLcXmtCrij}ZYFgZFokc0aAK*%d&nOOMGz8H%somA zl57q}VAqXe8QPJ39a|zf9+TVg+|s4ZwfSrFv+MI_%|wKqQmHaejZB?eIipoQ^==#5 zXqV$5HKYmP#d*yg(9=ljfn9+wu>zo1N>@m?paXZmE5 z6L`uB%Q9D@r$#lw?x}ZM!R{$I&eO0O;6RO__;nQ=AYFeLB1z=IV{aK7+uI|v36%q+ zWBe^!9mY2!b(xOvO?cxs+un_sTC5SYK;gP9u(Cci*k>QA<3UHK#tXM?=mkPh)3$Nw z0&j=V1tYZE%e;s^dYTO$UOUtq1vHeP3L8YZgc1n&RYk?YhXVZjgrqQhZt#Z&-ro1_ z?yv6pZ0B3?#q01>dg^`U(r+X3+k zKvbDoFOn&}Tj)ajbD4%V5NzQ&HZdJeS6kZ#+`^#h(S#Z%&5x{EReV|Hz0M!ZW-FBY0O3MtD5J7E&s1wVtv%}XM_X&-YJf8(6bWO0 zuD}^qBLz=K9x69TA?Y}fl%^h7ssV#Ju8~fktmUY6jX1g)(HJVMy|ft5iJVt>Y-S_H zu@AF185(r*xq!8_B|4#v*<|V1m|I>7&gZ}&+5$6d2f;97iWBhA!{%by+&OM^rP||E zshDr@Edq>o{MNxly;}fI1FDrIH5e>+UjU~;Ty79ig=*=>^S)CoO58k7-78VLAlt$U zKQM)XY7>!}=b^l!e{IX5;hRC$MC=%rVY~SrreImVr?>F(b`T65xN7_4H9tK|BeH#X z=ez(m3(9~JqGLh%9-{ceunI+krQz$oQ<~;DC~HNx?Hrhu*DhZKE|b_SSXL&a{y`3n z1Xob<@3BzQNt~DAZPi5En%HdSA??CrZPBcm(bQdOP)`KHQlQG7E4c>hkqcEQ0CGd2*<4Qpb;^AlcwGd~^2> zFWQltf)aIT9ySeVT9Tj>DtGP(kpp5VB1Z!OcK+{pR2cs0p}v8S_RBpl9sAnOFLXTG zbF+tg*)Zbv@~3onaeM+v<4`~J#PM-M!_-ykFjt3FfmFJTv&23e%;Ay-iid7JmfeM0 z-_q=yh~zcQ%oP&14#=L4qyGH*>h$u;4RHsKx!NJRnv0^-^2;(uBkhG{ zTCll5TNZm@q_;3t|8$q+5SDhxNLbkN5G=_C{^W@aK?pJ-^DHN$3;DLO=mW6K1M+;U zOYS(Y$aFZaVBr+_5soT>Na^kG^-bjS4&>&GRxT=&9$)}qsKTK*M-c}Y2)x{qICfEP zFp9H4N)P1q*oEsG@K5PDKm)Dxsg2TBp4JpL3AVK*n^s<57c~mBPI4Ibaks>&&Zwx{ ztprp#_~pVsn&W3yI#CY`&~)w7kxa%&GfD&rO@Q?U-=y3Ot*HUA

K`YI_=@Vup-G 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 WfA>ovQhswqS5^q0fXK99SN<>TDH>n^ literal 0 HcmV?d00001 diff --git a/data/planner_tech_stack_2026-03-17.db b/data/planner_tech_stack_2026-03-17.db new file mode 100644 index 0000000000000000000000000000000000000000..a219045279d5f99a307048e82fbbc1190be1a979 GIT binary patch literal 172032 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 tag. + +**Repo:** https://theres-a-git-in-this-tea.andrewawesomo.net/andrewadmin/neopixel-command-calculator +**First hardware profile:** ESP32-WROOM-32D + +## Project Structure +``` +neopixel-command-calculator/ +├── index.html # Dev server demo +├── package.json / tsconfig.json / vite.config.ts / vitest.config.ts +├── src/ +│ ├── index.ts # Entry point — exports + registers all components +│ ├── data/ +│ │ ├── protocols.ts # WS2812B, SK6812, APA102, WS2811 timing specs +│ │ └── hardware-profiles.ts # ESP32-WROOM-32D (8 RMT channels, 2 SPI buses) +│ ├── engine/ +│ │ ├── types.ts # TimingBreakdown, MaxLedsResult, StrandPlanResult, etc. +│ │ ├── timing.ts # Pure functions: calcDataTimeUs, calcMaxLeds, calcRefreshRate +│ │ └── strand-optimizer.ts # Multi-strand distribution across hardware channels +│ ├── components/ +│ │ ├── max-leds.ts # +│ │ ├── refresh-explorer.ts # +│ │ ├── strand-planner.ts # +│ │ └── shared/ +│ │ ├── timing-bar.ts # Stacked bar chart (data | reset | idle) +│ │ ├── math-breakdown.ts # Expandable "show the math" section +│ │ └── styles.ts # CSS custom properties (--npc-accent, --npc-bg, etc.) +│ └── utils/ +│ └── format.ts # Number/unit formatting helpers +└── test/ + └── engine/ + ├── timing.test.ts + └── strand-optimizer.test.ts +``` + +## Three Calculator Components + +### 1. `` — Max LED Count Calculator +- **Inputs:** protocol, strand count, target refresh rate (Hz), hardware profile +- **Outputs:** max LEDs per strand, total LEDs, data rate utilization %, timing breakdown +- Shows the math: bit time x bits per LED x LED count vs available frame time + +### 2. `` — Refresh Rate Explorer +- **Inputs:** LED count per strand, protocol, hardware profile +- **Outputs:** achievable refresh rates, timing budget visualization (bar chart: data | reset | idle) +- Interactive slider for LED count with real-time refresh rate updates + +### 3. `` — Multi-Strand Planner +- **Inputs:** total LED count, protocol, hardware profile +- **Outputs:** optimal strand split across RMT/SPI channels, per-strand timing, aggregate refresh rate +- Shows which ESP32 pins/peripherals each strand uses + +## Key Timing Math + +### One-wire protocols (WS2812B, SK6812, WS2811) +``` +frameBudgetUs = 1,000,000 / targetRefreshHz +maxLeds = floor((frameBudgetUs - resetTimeUs) / (bitsPerLed * bitPeriodNs / 1000)) +``` + +**Example:** WS2812B at 30Hz: +- frameBudget = 33,333 us +- dataTimePerLed = 24 bits * 1250ns / 1000 = 30.0 us +- maxLeds = floor((33,333 - 50) / 30.0) = **1,109 LEDs** + +### SPI protocols (APA102) +Clock-dependent, includes start/end frame overhead. At 8MHz SPI / 30Hz -> ~8,203 LEDs (raw timing). + +### Multi-strand parallel output +All RMT channels transmit simultaneously. Aggregate refresh = 1M / max(frameTimeUs). +- 300 WS2812B on 3 strands -> 327Hz vs 110Hz single strand + +## Protocol Data + +| Protocol | Bits/LED | Bit Period (ns) | Reset (us) | Signal | +|---|---|---|---|---| +| WS2812B | 24 | 1250 | 50 | one-wire | +| SK6812 RGB | 24 | 1250 | 80 | one-wire | +| SK6812 RGBW | 32 | 1250 | 80 | one-wire | +| WS2811 800kHz | 24 | 1250 | 50 | one-wire | +| WS2811 400kHz | 24 | 2500 | 50 | one-wire | +| APA102 | 32 | varies (SPI clock) | 0 | spi | + +## ESP32-WROOM-32D Hardware Profile +- 8 RMT channels (one-wire protocols, assignable to most GPIOs) +- 2 SPI buses: HSPI (GPIO 13), VSPI (GPIO 23) for APA102-style +- DMA-enabled operation assumed (ESP-IDF 4.4+) +- Default GPIO suggestions: 16, 17, 18, 19, 21, 22, 23, 25 for RMT + +## Technology Stack +- **Framework:** Web Components via Lit 3.x +- **Build:** Vite library mode (ES + UMD bundles) +- **Language:** TypeScript +- **Testing:** Vitest for engine unit tests +- **Distribution:** CDN/script tag — single JS bundle, zero external deps for consumers + +## Build Output +- `dist/neopixel-command-calculator.es.js` — ES module +- `dist/neopixel-command-calculator.umd.js` — UMD for `