diff --git a/.dockerignore b/.dockerignore index 8d1e39b4..3ae2d75f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,82 +1,44 @@ -# ============================================================================= -# Git -# ============================================================================= .git -.gitignore -.gitattributes .github +.gitignore +.pre-commit-config.yaml +.dockerignore +.claude + +# Archives +archive-legacy-*.tar.gz -# ============================================================================= # Environment & Secrets -# ============================================================================= .env .env.* -**/.env -**/.env.* -*.key -*.pem -# ============================================================================= -# Python -# ============================================================================= -__pycache__ -*.pyc -*.pyo -*.pyd -.ruff_cache -.mypy_cache -.pytest_cache -.coverage -htmlcov -.venv -venv -env -*.egg-info +# IDE +.idea +.vscode +*.swp +*.swo -# ============================================================================= -# Node.js / Frontend -# ============================================================================= +# Node.js node_modules -.next -out -.pnp -.pnp.js -.pnp.cjs -.yarn *.tsbuildinfo .eslintcache -.turbo npm-debug.log* -yarn-debug.log* -yarn-error.log* -# ============================================================================= -# Docker -# ============================================================================= -Dockerfile -Dockerfile.* -docker-compose*.yml -.dockerignore +# Go build artifacts +backend/bin -# ============================================================================= -# IDE & Editor -# ============================================================================= -.idea -.vscode -*.swp -*.swo -.claude +# Frontend build output +frontend/dist -# ============================================================================= -# Documentation & Tests -# ============================================================================= -docs/ -tests/ +# Documentation *.md !README.md -# ============================================================================= -# CI/CD & Config -# ============================================================================= -.pre-commit-config.yaml -.github/ +# Docker +Dockerfile +docker-compose*.yml +deploy/ + +# Scripts & Config +scripts/ +Makefile diff --git a/.env.example b/.env.example index 29cfc889..692eca54 100644 --- a/.env.example +++ b/.env.example @@ -1,57 +1,36 @@ # MomShell Environment Configuration -# Copy this file to .env and fill in your values -# NOTE: Do not use quotes around values - Docker --env-file includes them literally - -# Application -APP_NAME=MomShell -DEBUG=false - -# Server -# Local dev: uncomment PORT=8000 -# Docker: leave PORT commented (Dockerfile sets 7860) -HOST=0.0.0.0 -# PORT=8000 - -# Database -# Local development: sqlite+aiosqlite:///./data/momshell.db -# Docker/ModelScope: sqlite+aiosqlite:////mnt/workspace/momshell.db (persistent storage) -# PostgreSQL cloud: postgresql+asyncpg://user:pass@host:5432/dbname -DATABASE_URL=sqlite+aiosqlite:///./data/momshell.db - -# ModelScope API Configuration -# Get your API key from: https://modelscope.cn/ -MODELSCOPE_KEY=your_modelscope_api_key_here -MODELSCOPE_MODEL=Qwen/Qwen2.5-72B-Instruct -# For image generation model (optional) -# MODELSCOPE_IMAGE_MODEL=your_image_model_here - -# MediaPipe Configuration -POSE_MODEL_COMPLEXITY=1 -MIN_DETECTION_CONFIDENCE=0.5 -MIN_TRACKING_CONFIDENCE=0.3 - -# TTS Configuration (Microsoft Edge TTS) -TTS_VOICE=zh-CN-XiaoxiaoNeural -TTS_RATE=-10% - -# Safety Thresholds -MAX_DEVIATION_ANGLE=30.0 -FATIGUE_DETECTION_THRESHOLD=0.7 -REST_PROMPT_INTERVAL=300 - -# JWT Authentication -# IMPORTANT: Change this secret key in production! -JWT_SECRET_KEY=your-secret-key-change-in-production -JWT_ALGORITHM=HS256 +# Copy to .env and fill in your values + +# ==================== Database ==================== +# Local development +DATABASE_URL=postgres://momshell:momshell@localhost:5432/momshell?sslmode=disable +# Docker deployment (uses container name "postgres" as host) +# DATABASE_URL=postgres://momshell:momshell@postgres:5432/momshell?sslmode=disable + +# Docker Postgres container settings +POSTGRES_USER=momshell +POSTGRES_PASSWORD=momshell +POSTGRES_DB=momshell + +# ==================== JWT ==================== +# IMPORTANT: Change this in production +JWT_SECRET_KEY=change-me-in-production JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30 JWT_REFRESH_TOKEN_EXPIRE_DAYS=7 -# Web Search (Firecrawl API for reducing AI hallucinations) -# Get your API key from: https://www.firecrawl.dev/ -# FIRECRAWL_API_KEY=your_firecrawl_api_key_here +# ==================== OpenAI Compatible API ==================== +OPENAI_API_KEY= +OPENAI_BASE_URL=https://api-inference.modelscope.cn/v1 +OPENAI_MODEL=Qwen/Qwen2.5-72B-Instruct + +# ==================== Server ==================== +PORT=8000 + +# ==================== Frontend ==================== +# Backend API URL (defaults to http://localhost:8000, leave empty for Nginx proxy in production) +VITE_API_BASE_URL=http://localhost:8000 -# Initial Admin Account (optional, created on first startup) -# Set these to automatically create an admin account when the app starts -# ADMIN_USERNAME=admin -# ADMIN_EMAIL=admin@example.com -# ADMIN_PASSWORD=your_secure_password +# ==================== Initial Admin (optional, created on first startup) ==================== +ADMIN_USERNAME= +ADMIN_EMAIL= +ADMIN_PASSWORD= diff --git a/.github/ISSUE_TEMPLATE/ai_provider_issue.yml b/.github/ISSUE_TEMPLATE/ai_provider_issue.yml index c4b70225..3a9e3e05 100644 --- a/.github/ISSUE_TEMPLATE/ai_provider_issue.yml +++ b/.github/ISSUE_TEMPLATE/ai_provider_issue.yml @@ -1,12 +1,12 @@ name: AI Provider Issue -description: Report issues with AI responses, ModelScope API, or LLM behavior +description: Report issues with AI responses, LLM API, or content moderation title: "[AI]: " labels: ["ai", "triage"] body: - type: markdown attributes: value: | - This template is for issues related to AI/LLM functionality, including the Soulful Companion chat and AI-powered coaching feedback. + This template is for issues related to AI/LLM functionality, including the Soul Companion chat, Echo memoir generation, and content moderation. - type: dropdown id: feature @@ -14,8 +14,8 @@ body: label: Affected AI Feature description: Which AI-powered feature is affected? options: - - Soulful Companion (Chat responses) - - Recovery Coach (Voice feedback) + - Soul Companion (Chat responses) + - Echo (Memoir generation) - Content moderation - Other validations: @@ -29,10 +29,9 @@ body: options: - API connection failed - No response / timeout - - Incorrect/inappropriate response + - Incorrect / inappropriate response - Response parsing error (malformed JSON) - Conversation memory not working - - TTS (Text-to-Speech) not working - Other validations: required: true @@ -66,7 +65,7 @@ body: label: Error Logs description: | Please provide backend logs showing the API error. - Look for errors related to ModelScope, OpenAI client, or JSON parsing. + Look for errors related to OpenAI client or JSON parsing. placeholder: Paste relevant logs here... - type: checkboxes @@ -75,9 +74,9 @@ body: label: API Configuration Checklist description: Please confirm the following options: - - label: MODELSCOPE_SDK_TOKEN environment variable is set + - label: OPENAI_API_KEY environment variable is set + - label: OPENAI_BASE_URL is correct - label: API endpoint is accessible from my network - - label: I have verified my API token is valid - type: textarea id: additional diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 5f3f4d06..f1d75224 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -14,13 +14,15 @@ body: label: Affected Component description: Which part of MomShell is affected? options: - - Soulful Companion (Chat) - - Recovery Coach (Exercise/Pose Detection) - - Community (Q&A/Posts) - - Frontend/UI - - Backend/API + - Soul Companion (Chat) + - Sisterhood Bond (Community) + - Echo / Memoir + - Admin Panel + - Auth / Login / Registration + - Frontend / UI + - Backend / API - Database - - Deployment/Docker + - Deployment / Docker - Other validations: required: true @@ -61,7 +63,7 @@ body: label: Error Logs / Screenshots description: | If applicable, add error logs or screenshots to help explain the problem. - - For backend errors: Check terminal/Docker logs + - For backend errors: Check terminal or `make docker-logs` - For frontend errors: Check browser console (F12 → Console) placeholder: Paste logs or drag-and-drop screenshots here... @@ -71,9 +73,8 @@ body: label: Deployment Method description: How is MomShell deployed? options: - - Local Development (uvicorn + npm run dev) - - Docker Compose - - Single Container + - Local Development (make dev-backend + make dev-frontend) + - Docker Compose (make docker-up) - Other validations: required: true @@ -83,7 +84,7 @@ body: attributes: label: Operating System description: What OS are you running on? - placeholder: "e.g., Ubuntu 22.04, Windows 11, macOS 14" + placeholder: "e.g., Ubuntu 22.04, Arch Linux, Windows 11, macOS 14" validations: required: true diff --git a/.github/ISSUE_TEMPLATE/coach_issue.yml b/.github/ISSUE_TEMPLATE/coach_issue.yml deleted file mode 100644 index 2ccaa6bc..00000000 --- a/.github/ISSUE_TEMPLATE/coach_issue.yml +++ /dev/null @@ -1,102 +0,0 @@ -name: Recovery Coach Issue -description: Report issues with pose detection, exercises, or voice feedback -title: "[Coach]: " -labels: ["coach", "triage"] -body: - - type: markdown - attributes: - value: | - This template is for issues specifically related to the Recovery Coach feature, including pose detection, exercise tracking, and voice feedback. - - - type: dropdown - id: issue-type - attributes: - label: Issue Type - description: What type of coach issue are you experiencing? - options: - - Camera/Video not working - - Pose detection inaccurate - - Exercise not recognized - - Voice feedback not playing - - Progress not saved - - Performance issues (lag/slow) - - Other - validations: - required: true - - - type: dropdown - id: exercise - attributes: - label: Exercise (if applicable) - description: Which exercise were you doing when the issue occurred? - options: - - N/A - - Diaphragmatic Breathing - - Kegel Exercise - - Dead Bug - - Bridge - - Cat-Cow Stretch - - Wall Push-up - - Standing Balance - - Gentle Squat - - Shoulder Roll - - - type: textarea - id: description - attributes: - label: Issue Description - description: Describe the issue in detail. - placeholder: What happened? What did you expect? - validations: - required: true - - - type: textarea - id: steps - attributes: - label: Steps to Reproduce - description: How can we reproduce this issue? - placeholder: | - 1. Start the Recovery Coach - 2. Select exercise '...' - 3. ... - validations: - required: true - - - type: dropdown - id: camera - attributes: - label: Camera Type - description: What type of camera are you using? - options: - - Built-in webcam - - External USB camera - - Virtual camera - - Other - validations: - required: true - - - type: input - id: browser - attributes: - label: Browser - description: Which browser are you using? - placeholder: "e.g., Chrome 120, Firefox 121" - validations: - required: true - - - type: textarea - id: logs - attributes: - label: Error Logs / Console Output - description: | - Please provide any relevant logs: - - Browser console (F12 → Console) - - Backend terminal output - - WebSocket connection errors - placeholder: Paste logs here... - - - type: textarea - id: additional - attributes: - label: Additional Context - description: Add screenshots, screen recordings, or any other helpful information. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 55f6d31c..43f625d7 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ blank_issues_enabled: false contact_links: - name: Question / Discussion - url: https://github.com/Poiesis-Inc/MomShell/discussions + url: https://github.com/koishi510/MomShell/discussions about: Ask questions or start a discussion about MomShell - name: Security Vulnerability - url: https://github.com/Poiesis-Inc/MomShell/security/advisories/new + url: https://github.com/koishi510/MomShell/security/advisories/new about: Report security vulnerabilities privately diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index df8bdf18..309fbdbf 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -14,11 +14,13 @@ body: label: Related Component description: Which part of MomShell would this feature affect? options: - - Soulful Companion (Chat) - - Recovery Coach (Exercise/Pose Detection) - - Community (Q&A/Posts) - - Frontend/UI - - Backend/API + - Soul Companion (Chat) + - Sisterhood Bond (Community) + - Echo / Memoir + - Admin Panel + - Auth / User System + - Frontend / UI + - Backend / API - New Component - Other validations: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 572472f6..cf89cf80 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,52 +4,46 @@ ### Summary - + ### Change Type - - - [ ] New Feature (feat) - [ ] Bug Fix (fix) - [ ] Refactoring (refactor) -- [ ] Performance Improvement (perf) - [ ] Documentation (docs) - [ ] Dependency / Configuration (chore) ### Self-Check Checklist - +> Run `make check` to execute all checks at once. -**Backend**: -- [ ] Code runs correctly in local environment -- [ ] Ran `uv run ruff format .` and `uv run ruff check . --fix` -- [ ] Ran `uv run mypy app/` -- [ ] (If dependencies changed) Ran `uv lock && uv export > requirements.txt` and committed both files +**Backend (Go)**: +- [ ] `go build ./...` passes +- [ ] `go vet ./...` passes +- [ ] `gofmt` produces no diff +- [ ] `golangci-lint run` passes (or no new issues) -**Frontend**: -- [ ] (If frontend changed) Ran `npm run lint` in `frontend/` -- [ ] (If frontend changed) Ran `npm run build` in `frontend/` without errors +**Frontend (Vue)**: +- [ ] `npm run lint` passes +- [ ] `npm run typecheck` passes +- [ ] `npm run build` succeeds **General**: -- [ ] Removed all temporary debug output (print/console.log) -- [ ] No sensitive data (API keys, credentials) in the code +- [ ] Removed all temporary debug output +- [ ] No sensitive data in the code +- [ ] CI checks pass ### Test Steps - - -1. Pull branch and sync environment: +1. Pull branch and install dependencies: ```bash - uv sync - cd frontend && npm install && cd .. + make install ``` -2. Run the application: +2. Start the application: ```bash - # Terminal 1 - Backend - uv run uvicorn app.main:app --reload --port 8000 - # Terminal 2 - Frontend - cd frontend && npm run dev + make dev-backend # Terminal 1 + make dev-frontend # Terminal 2 ``` 3. Verification steps: - ... diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8058d267..2314710b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,14 +1,10 @@ # Dependabot configuration # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates -# -# Note: This project uses uv (Python) and nvm (Node.js) for package management. -# Dependabot's pip ecosystem works with pyproject.toml (uv compatible). -# Dependabot's npm ecosystem works with package.json (nvm just manages Node version). version: 2 updates: - # Python dependencies (backend, managed by uv) - - package-ecosystem: "pip" + # Go dependencies (backend) + - package-ecosystem: "gomod" directory: "/backend" schedule: interval: "weekly" @@ -16,9 +12,9 @@ updates: prefix: "chore(deps)" labels: - "dependencies" - - "python" + - "go" - # npm dependencies (frontend, Node version managed by nvm) + # npm dependencies (frontend) - package-ecosystem: "npm" directory: "/frontend" schedule: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2bca3c4..5dac7175 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,57 +8,14 @@ on: branches: - "**" -jobs: - python-cq: - name: Python Code Quality - runs-on: ubuntu-latest - - if: | - (github.event_name == 'push' && - !contains(github.event.head_commit.message, 'docs:') && - !contains(github.event.head_commit.message, 'chore:') && - !contains(github.event.head_commit.message, '[skip ci]')) || - (github.event_name == 'pull_request' && - !contains(github.event.pull_request.title, 'docs:') && - !contains(github.event.pull_request.title, 'chore:') && - !contains(github.event.pull_request.title, '[skip ci]')) - - defaults: - run: - working-directory: backend - - steps: - - uses: actions/checkout@v6 - - - uses: astral-sh/setup-uv@v7 - with: - enable-cache: true - cache-dependency-glob: "backend/uv.lock" - - - run: uv python install 3.11 - - - run: uv sync --frozen - - - run: uv run ruff format --check . - - - run: uv run ruff check . --output-format=github - - - run: uv run mypy app/ +permissions: + contents: read +jobs: frontend-cq: name: Frontend Code Quality runs-on: ubuntu-latest - if: | - (github.event_name == 'push' && - !contains(github.event.head_commit.message, 'docs:') && - !contains(github.event.head_commit.message, 'chore:') && - !contains(github.event.head_commit.message, '[skip ci]')) || - (github.event_name == 'pull_request' && - !contains(github.event.pull_request.title, 'docs:') && - !contains(github.event.pull_request.title, 'chore:') && - !contains(github.event.pull_request.title, '[skip ci]')) - defaults: run: working-directory: frontend @@ -81,8 +38,42 @@ jobs: - name: ESLint run: npm run lint - - name: TypeScript type check + - name: Type check run: npm run typecheck - name: Build run: npm run build + + go-cq: + name: Go Code Quality + runs-on: ubuntu-latest + + defaults: + run: + working-directory: backend + + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-go@v5 + with: + go-version-file: backend/go.mod + cache-dependency-path: backend/go.sum + + - name: Format check + run: test -z "$(gofmt -l .)" + + - name: Vet + run: go vet ./... + + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: latest + working-directory: backend + + - name: Build + run: go build ./... + + - name: Test + run: go test ./... diff --git a/.gitignore b/.gitignore index b78f2fad..76ebaa94 100644 --- a/.gitignore +++ b/.gitignore @@ -1,294 +1,59 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[codz] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -/lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py.cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -#uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock -#poetry.toml - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. -# https://pdm-project.org/en/latest/usage/project/#working-with-version-control -#pdm.lock -#pdm.toml -.pdm-python -.pdm-build/ - -# pixi -# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. -#pixi.lock -# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one -# in the .venv directory. It is recommended not to include this directory in version control. -.pixi - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.envrc -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -# Abstra -# Abstra is an AI-powered process automation framework. -# Ignore directories containing user credentials, local state, and settings. -# Learn more at https://abstra.io/docs -.abstra/ - -# Visual Studio Code -# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore -# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore -# and can be added to the global gitignore or merged into this file. However, if you prefer, -# you could uncomment the following to ignore the entire vscode folder -# .vscode/ - -# Ruff stuff: -.ruff_cache/ - -# PyPI configuration file -.pypirc - -# Cursor -# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to -# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data -# refer to https://docs.cursor.com/context/ignore-files -.cursorignore -.cursorindexingignore - -# Marimo -marimo/_static/ -marimo/_lsp/ -__marimo__/ - -# Claude -.claude -CLAUDE.md -*/CLAUDE.md - # ============================================================================= -# Node.js / Frontend +# Go Backend # ============================================================================= +backend/bin/ +backend/.cache/ -# Dependencies +# ============================================================================= +# Node.js / Vue Frontend +# ============================================================================= node_modules/ -.pnp/ -.pnp.js -.pnp.cjs -.yarn/ - -# Next.js -.next/ -out/ - -# Build -build/ dist/ - -# TypeScript *.tsbuildinfo -next-env.d.ts - -# Testing -coverage/ -.nyc_output/ - -# Misc -*.pem -.vercel - -# Debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* -# Local env files -.env*.local - -# Turbo -.turbo - -# ESLint -.eslintcache +# ============================================================================= +# IDE & Editor +# ============================================================================= +.idea/ +.vscode/ +*.swp +*.swo +*~ # ============================================================================= -# MomShell Project Specific +# Environment & Secrets # ============================================================================= +.env +.env.* +!.env.example -# MediaPipe models (downloaded at runtime) -backend/models/* -!backend/models/.gitkeep -backend/app/models/* -!backend/app/models/.gitkeep +# ============================================================================= +# OS +# ============================================================================= +.DS_Store +Thumbs.db -# TTS cache (generated audio files) -backend/tts_cache/* -!backend/tts_cache/.gitkeep +# ============================================================================= +# Logs & Debug +# ============================================================================= +*.log +npm-debug.log* -# SQLite database -*.db -*.sqlite -data.db -backend/data/* -!backend/data/.gitkeep +# ============================================================================= +# Testing & Coverage +# ============================================================================= +coverage/ -# OS files -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -Thumbs.db -ehthumbs.db +# ============================================================================= +# Archives (legacy backups) +# ============================================================================= +archive-legacy-*.tar.gz -# Temporary files +# ============================================================================= +# Misc +# ============================================================================= *.tmp *.temp tmp/ temp/ +.claude diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 83764492..addc588a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,49 +12,39 @@ repos: - id: check-added-large-files args: ["--maxkb=10000"] - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.0 - hooks: - - id: ruff - args: [--fix] - files: ^backend/ - - id: ruff-format - files: ^backend/ - - repo: local hooks: - - id: mypy - name: mypy - entry: bash -c 'cd backend && uv run mypy app/' + - id: vue-eslint + name: Vue ESLint + entry: bash -c 'cd frontend && npm run lint' language: system - types: [python] - require_serial: true - files: ^backend/ + files: ^frontend/.*\.(ts|vue|js)$ + pass_filenames: false - - id: uv-export - name: Export requirements.txt - entry: bash -c 'cd backend && uv export --no-dev --no-emit-project --output-file requirements.txt' + - id: vue-typecheck + name: Vue typecheck + entry: bash -c 'cd frontend && npm run typecheck' language: system - files: ^backend/uv\.lock$ + files: ^frontend/.*\.(ts|vue)$ pass_filenames: false - - id: uv-lock-check - name: Check uv.lock consistency - entry: bash -c 'cd backend && uv lock --check' + - id: go-fmt-check + name: Go format check + entry: bash -c 'cd backend && test -z "$(gofmt -l .)"' language: system - files: ^backend/pyproject\.toml$ + files: ^backend/.*\.go$ pass_filenames: false - - id: frontend-eslint - name: Frontend ESLint - entry: bash -c 'cd frontend && npm run lint' + - id: go-vet + name: Go vet + entry: bash -c 'cd backend && go vet ./...' language: system - files: ^frontend/.*\.(ts|tsx|js|jsx)$ + files: ^backend/.*\.go$ pass_filenames: false - - id: frontend-typecheck - name: Frontend TypeScript check - entry: bash -c 'cd frontend && npm run typecheck' + - id: go-build + name: Go build + entry: bash -c 'cd backend && go build ./...' language: system - files: ^frontend/.*\.(ts|tsx)$ + files: ^backend/.*\.go$ pass_filenames: false diff --git a/CHANGELOG.md b/CHANGELOG.md index c42f0f10..341fb0c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,99 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.0] - 2026-03-05 + +### Major Rewrite + +Complete rewrite of the backend (Python → Go) and frontend (Next.js → Vue 3). The old codebase has been archived. + +### Added + +#### Go Backend + +- **Tech stack**: Go 1.23, Gin, GORM, PostgreSQL, JWT (golang-jwt), OpenAI SDK +- **Architecture**: Handler → Service → Repository layered design +- **Authentication**: JWT access tokens (30 min) + refresh tokens (7 days), multi-source token extraction (header, cookie) +- **Content moderation**: Keyword-based detection with categories (pseudoscience, violence, self-harm, spam), crisis keyword auto-rejection +- **Embedded admin panel** (`/admin`): Single-file HTML (Tailwind CSS + Alpine.js) via `go:embed` + - Dashboard with user stats and role distribution + - User management: search, filter, paginate, create, edit (role/status), delete + - Runtime config management: view and edit API keys, token expiration (mutex-protected) + - Self-protection: prevents admin from demoting/banning/deleting themselves +- **Community (Sisterhood Bond)**: Q&A with dual channels, verified professionals, likes, collections, comments +- **Soul Companion**: AI chat with session persistence and conversation memory +- **Echo / Memoir module**: Identity tags (music, sound, literature, memory) and AI-generated memoir stickers using Qwen3, SVG gradient covers, full CRUD with auth + +#### Vue 3 Frontend + +- **Tech stack**: Vue 3, Vite, TypeScript, Pinia, Axios +- **Beach scene UI**: Parallax layers (sky, ocean, sand), wave animations, interactive elements +- **Overlay system**: Auth, chat, community, profile panels +- **Community panel**: Full backend API integration — question detail fetch, question/answer likes, bookmarks, nested comments, pagination, tag selection, hot posts tab +- **Chat panel**: Session-based conversation continuity, visual effects from `visual_metadata` (5 animations + 7 color tones), memory update toast, personalized greeting from companion profile +- **User center**: Inline nickname editing, 4-stat grid, tabbed my-questions/my-answers with pagination and status badges, change password form +- **ESLint + vue-tsc**: Full lint and type checking configured + +#### Docker Deployment + +- **Root Dockerfile**: Multi-stage build (Node → Go → Nginx Alpine), frontend + backend in single image +- **docker-compose**: App container (Nginx + Go) + PostgreSQL 16 with health checks and data volume +- **Nginx config**: SPA fallback, gzip, static asset caching, `/api/` and `/admin` reverse proxy to backend +- **Entrypoint script**: Starts Go backend in background + Nginx in foreground + +#### Developer Experience + +- **dev-setup.sh**: Interactive setup script + - Auto-detects package manager (pacman/apt/dnf/brew) and offers to install missing dependencies + - PostgreSQL setup: start service, create user and database, verify connection + - Interactive environment variable configuration with sensible defaults + - Auto-generates JWT secret, auto-fills DATABASE_URL from PostgreSQL step + - Installs Go/npm dependencies, pre-commit hooks, verifies build +- **Pre-commit hooks**: gofmt, go vet, go build, ESLint, vue-tsc, trailing whitespace, YAML validation +- **GitHub Actions CI**: Go (gofmt, vet, golangci-lint, build, test) + Vue (ESLint, typecheck, build) +- **Makefile**: Unified commands for dev, lint, typecheck, build, docker, postgres, deps, clean +- **Frontend Makefile**: Standalone dev/build/lint/typecheck/docker commands + +#### Configuration + +- **`.env.example`**: Full environment template with English comments + - Database, JWT, OpenAI, server, frontend, Docker Postgres, initial admin settings + - Separate local/Docker DATABASE_URL examples +- **`.env` auto-generation**: dev-setup.sh creates `.env` interactively with all variables + +#### Documentation + +- **README.md**: Project overview with Go/Vue/TypeScript badges +- **docs/architecture.md**: Tech stack, project structure, architecture layers, design decisions +- **docs/getting-started.md**: Prerequisites, setup, first run +- **docs/development.md**: Dev workflow, make commands +- **docs/configuration.md**: All environment variables with descriptions +- **docs/deployment.md**: Docker quick start, architecture diagram, compose setup +- **docs/features.md**: Soul Companion, Sisterhood Bond, Admin Panel +- **CONTRIBUTING.md**: Go/Vue code standards, hooks, commit conventions, branch workflow + +### Changed + +- **Backend language**: Python (FastAPI/SQLAlchemy) → Go (Gin/GORM) +- **Frontend framework**: Next.js (React) → Vue 3 (Vite) +- **Package name**: `beach-scene` → `momshell-frontend` v1.0.0 +- **Frontend port**: 3000 → 5173 (Vite default) +- **API base URL**: Hardcoded → `import.meta.env.VITE_API_BASE_URL` with fallback +- **Env loading**: `godotenv.Load()` → `godotenv.Overload()` so `.env` overrides shell environment +- **OpenAI client**: Replaced go-openai library with raw HTTP client supporting Qwen3 `enable_thinking` field + +### Removed + +- Python backend (FastAPI, SQLAlchemy, LangGraph, MediaPipe) +- Next.js frontend (React, Framer Motion) +- Recovery Coach module (pose estimation, exercise library) +- Guardian Partner module +- Web search / Firecrawl integration +- Chain-of-Verification (CoVe) service +- Slider CAPTCHA + +--- + ## [0.6.0] - 2026-02-09 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4bca9bc7..019a5bb7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,182 +1,98 @@ # Contributing -Thank you for your interest in contributing to MomShell! This guide covers the collaboration standards and development workflow for contributors. +Thank you for contributing to MomShell! ## Getting Started -For environment setup and running the project, see: +- [Getting Started](docs/getting-started.md) — Prerequisites and setup +- [Development Guide](docs/development.md) — Workflow and commands -- **[Getting Started](docs/getting-started.md)** - Prerequisites, installation, and running -- **[Development Guide](docs/development.md)** - Development environment setup +## Code Quality -## IDE Configuration - -We recommend VS Code for development. Configure it as follows: - -1. **Select Python Interpreter**: - - Open Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) - - Type and select `Python: Select Interpreter` - - Choose the option containing `backend/.venv` - -2. **Install Recommended Extensions**: - - **Python** (Microsoft) - - **Ruff** (Astral Software) - Code formatting and linting - - **Mypy** (Microsoft) - Static type checking - -## Development Workflow - -### Daily Sync - -Before writing code, update your local branch and sync dependencies: +Before committing, run checks locally: ```bash -git checkout dev -git pull --rebase origin dev - -cd backend && uv sync && cd .. -cd frontend && npm install && cd .. +make check # lint + typecheck (Go + Vue) +make format # go fmt ``` -### Code Quality Checks - -Before committing, run these checks locally: +Pre-commit hooks run automatically if installed: ```bash -# Using Make (recommended) -make lint # Run all linters -make format # Format all code -make typecheck # Run type checkers -make check # Run lint + typecheck +pre-commit install ``` -Or manually: +### What the hooks check -```bash -# Backend -cd backend -uv run ruff format . -uv run ruff check . --fix -uv run mypy app/ +- **Go**: `gofmt`, `go vet`, `go build` +- **Vue**: ESLint, `vue-tsc` +- **General**: trailing whitespace, YAML validity, merge conflicts, large files -# Frontend -cd frontend -npm run lint -npm run build -``` +## Code Standards -### Dependency Management +**Backend (Go)**: +- `gofmt` formatting +- `go vet` passes +- Follow standard Go project layout -**Backend (Python)** - Do not use pip directly: +**Frontend (Vue/TypeScript)**: +- ESLint with `eslint-plugin-vue` + `typescript-eslint` +- `vue-tsc` type checking passes + +## Dependency Management + +**Backend**: ```bash cd backend -uv add numpy # Production dependency -uv add --dev ruff # Development dependency +go get github.com/some/package +go mod tidy ``` -**Frontend (Node.js)**: +**Frontend**: ```bash cd frontend -npm install axios # Production dependency -npm install -D @types/node # Development dependency +npm install some-package ``` -## Collaboration Standards +Lock files (`go.sum`, `package-lock.json`) must be committed. -### Lock File Management +## Commit Convention -**Backend** (`backend/` directory): -- `uv.lock` - Auto-generated. **Do not edit manually**. Must be committed. -- `requirements.txt` - Auto-generated by `uv export`. **Do not edit manually**. -- **Conflict Resolution**: Run `cd backend && uv lock && uv export > requirements.txt` +All commits follow [Conventional Commits](https://www.conventionalcommits.org/): `type: description` -**Frontend**: -- `package-lock.json` - Auto-generated. **Do not edit manually**. Must be committed. -- **Conflict Resolution**: Delete `package-lock.json`, run `npm install` - -### Large File Handling - -- Verify large binary files (.pt, .parquet, .db) are listed in `.gitattributes` for LFS -- **Never commit** files larger than 10MB without LFS - -### Sensitive Files - -**Never commit**: -- `.env` - Use `.env.example` as template -- Credential files (`credentials.json`, `*.pem`, `*.key`) -- `__pycache__/`, `node_modules/`, `.next/`, `out/` - -### Code Standards - -**Backend (Python)** - Enforced by Ruff and Mypy: -- PEP 8 + Black formatting (double quotes, 88-char lines, 4-space indent) -- Imports ordered: standard library → third-party → local -- Type hints required, must pass Mypy - -**Frontend (TypeScript)** - Enforced by ESLint: -- Prettier defaults (single quotes, 2-space indent, semicolons) -- Strict TypeScript, no unjustified `any` types -- Follow React hooks rules +| Type | Description | +|------|-------------| +| `feat` | New feature | +| `fix` | Bug fix | +| `refactor` | Code restructuring | +| `chore` | Build, config, tooling | +| `docs` | Documentation | +| `ci` | CI/CD changes | +| `test` | Tests | -### Commit Message Convention +## Branch & PR Workflow -All commits must follow Conventional Commits: `type: description` +1. Create feature branch from `main`: `feat/feature-name` or `fix/issue-name` +2. Make changes, commit with conventional messages +3. Push and create PR targeting `main` +4. Ensure CI passes and request review -| Type | Description | CI Behavior | -|:-----|:------------|:------------| -| **feat** | New feature | Triggers build/test | -| **fix** | Bug fix | Triggers build/test | -| **refactor** | Code refactoring | Triggers build/test | -| **perf** | Performance improvement | Triggers build/test | -| **test** | Add or update tests | Triggers build/test | -| **style** | Code style changes | Skips CI build | -| **docs** | Documentation only | Skips CI build | -| **chore** | Build config or tooling | Skips CI build | -| **ci** | CI/CD configuration | Skips CI build | -| **build** | Build system changes | Skips CI build | -| **revert** | Revert a previous commit | Triggers build/test | +## Sensitive Files -### Branch Management & Pull Request - -This project uses a dual main branch strategy (`main` for production, `dev` for development). - -1. **Branch Naming**: - - `dev` - Main development branch. Create feature branches from here. - - `main` - Production releases only. Direct pushes prohibited. - - Feature branches: `feat/feature-name` or `fix/issue-description` - -2. **Before Pull Request**: - ```bash - git fetch origin - git rebase origin/dev - ``` - -3. **Submission**: - - Ensure all quality checks pass locally - - Push branch (`git push --force-with-lease` after rebase) - - Create PR with **target branch set to `dev`** - -4. **Merge Criteria**: - - All CI checks must pass - - Code review approval from a CODEOWNER required +**Never commit**: `.env`, `*.pem`, `*.key`, `credentials.json` ## Troubleshooting -**Q: uv/nvm command not found?** -- Restart terminal after installation. Windows: try running PowerShell as administrator. - -**Q: VS Code showing errors?** -- Verify Python interpreter is set to `backend/.venv`, not system Python. - -**Q: Git rejecting commits?** -- Pre-commit checks failed. Run `uv run ruff format .` and `uv run ruff check . --fix`, then commit again. +**Pre-commit hook failing?** +Run `make check` to see which check fails, fix, then commit again. -**Q: Frontend build failing?** -- Run `cd frontend && npm install`. If errors persist, delete `node_modules` and `package-lock.json`, then `npm install`. +**Frontend build failing?** +Delete `node_modules` and run `npm install`. -**Q: Port already in use?** +**Port already in use?** ```bash -lsof -i :8000 # Find process on port -kill -9 # Kill by PID +lsof -i :8000 +kill -9 ``` diff --git a/Dockerfile b/Dockerfile index b0c6ee16..af18972c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,104 +1,37 @@ -# syntax=docker/dockerfile:1 -# ============================================================================= -# MomShell - ModelScope Deploy (Single Container) -# ============================================================================= -# 前后端合并部署,监听 0.0.0.0:7860 -# ============================================================================= - -# ========================================== -# Stage 1: 前端构建 (Frontend Builder) -# ========================================== -FROM node:25-alpine AS frontend-builder -WORKDIR /build_frontend - -# 安装依赖 (利用缓存) -COPY frontend/package*.json ./ -RUN npm install - -# 复制代码并构建 -COPY frontend/ ./ +# ---- Stage 1: Build frontend ---- +FROM node:24-alpine AS frontend-builder +WORKDIR /app +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci +COPY frontend/ . +ARG VITE_API_BASE_URL= +ENV VITE_API_BASE_URL=$VITE_API_BASE_URL RUN npm run build -# ========================================== -# Stage 2: 系统库安装 (绕过 apt 缓存空间限制) -# ========================================== -FROM python:3.14-slim-bookworm AS lib-builder - -# 直接下载 deb 包并安装,绕过 apt 缓存 -RUN apt-get update -o Acquire::Check-Valid-Until=false -o Acquire::AllowInsecureRepositories=true && \ - cd /tmp && \ - apt-get download --allow-unauthenticated libglib2.0-0 libgomp1 libxcb1 libxau6 libxdmcp6 libbsd0 libmd0 && \ - dpkg -i *.deb || true && \ - rm -rf /var/lib/apt/lists/* /tmp/*.deb - -# ========================================== -# Stage 3: 后端运行 (Backend Runtime) -# ========================================== -FROM python:3.14-slim-bookworm - -# 环境变量设置 -ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 \ - PORT=7860 \ - MPLCONFIGDIR=/tmp/matplotlib \ - DATABASE_URL=sqlite+aiosqlite:////mnt/workspace/momshell.db - +# ---- Stage 2: Build backend ---- +FROM golang:1.23-alpine AS backend-builder WORKDIR /app +COPY backend/go.mod backend/go.sum ./ +RUN go mod download +COPY backend/ . +RUN CGO_ENABLED=0 go build -o /server cmd/server/main.go -# 从 lib-builder 复制系统库 -COPY --from=lib-builder /usr/lib/x86_64-linux-gnu/libglib* /usr/lib/x86_64-linux-gnu/ -COPY --from=lib-builder /usr/lib/x86_64-linux-gnu/libgthread* /usr/lib/x86_64-linux-gnu/ -COPY --from=lib-builder /usr/lib/x86_64-linux-gnu/libgobject* /usr/lib/x86_64-linux-gnu/ -COPY --from=lib-builder /usr/lib/x86_64-linux-gnu/libgio* /usr/lib/x86_64-linux-gnu/ -COPY --from=lib-builder /usr/lib/x86_64-linux-gnu/libgmodule* /usr/lib/x86_64-linux-gnu/ -COPY --from=lib-builder /usr/lib/x86_64-linux-gnu/libgomp* /usr/lib/x86_64-linux-gnu/ -COPY --from=lib-builder /usr/lib/x86_64-linux-gnu/libxcb* /usr/lib/x86_64-linux-gnu/ -COPY --from=lib-builder /usr/lib/x86_64-linux-gnu/libXau* /usr/lib/x86_64-linux-gnu/ -COPY --from=lib-builder /usr/lib/x86_64-linux-gnu/libXdmcp* /usr/lib/x86_64-linux-gnu/ -COPY --from=lib-builder /usr/lib/x86_64-linux-gnu/libbsd* /usr/lib/x86_64-linux-gnu/ -COPY --from=lib-builder /usr/lib/x86_64-linux-gnu/libmd* /usr/lib/x86_64-linux-gnu/ - -# 安装 Python 依赖 -COPY backend/requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ - -# 替换 OpenCV 为 headless 版本(不需要 libGL/X11 系统库,节省 200MB+) -RUN pip install --no-cache-dir --force-reinstall \ - opencv-python-headless==4.13.0.90 \ - opencv-contrib-python-headless==4.13.0.90 \ - -i https://mirrors.aliyun.com/pypi/simple/ - -# 复制后端代码 -COPY backend/app/ /app/app/ +# ---- Stage 3: Final image ---- +FROM nginx:alpine +RUN apk --no-cache add ca-certificates tzdata -# 创建数据目录和模型目录 -RUN mkdir -p /app/data /app/models +# Nginx config +COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf -# 预下载 MediaPipe LITE 模型(更快,适合低配服务器) -# 添加重试逻辑,避免网络不稳定导致构建失败 -RUN python3 << 'PYEOF' -import urllib.request, time -url = "https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_lite/float16/1/pose_landmarker_lite.task" -path = "/app/models/pose_landmarker_lite.task" -for i in range(5): - try: - urllib.request.urlretrieve(url, path) - print("Downloaded successfully") - break - except Exception as e: - print(f"Attempt {i+1} failed: {e}") - if i < 4: - time.sleep(3 * (i + 1)) -else: - raise Exception("Failed to download after 5 attempts") -PYEOF +# Frontend static files +COPY --from=frontend-builder /app/dist /usr/share/nginx/html -# 从 Stage 1 复制前端静态文件 -# Next.js export 输出在 out 目录 -COPY --from=frontend-builder /build_frontend/out /app/frontend_dist +# Backend binary +COPY --from=backend-builder /server /app/server -# 暴露端口 -EXPOSE 7860 +# Entrypoint: start backend + nginx +COPY deploy/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh -# 启动命令 - 必须监听 0.0.0.0:7860 -CMD ["python", "-m", "app.main"] +EXPOSE 80 +CMD ["/entrypoint.sh"] diff --git a/Makefile b/Makefile index e2995d86..5ebdfd06 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ .PHONY: install install-backend install-frontend dev dev-backend dev-frontend \ lint lint-backend lint-frontend format format-backend format-frontend \ - typecheck typecheck-backend typecheck-frontend build build-frontend \ - docker-up docker-down docker-logs docker-build docker-build-backend docker-build-frontend \ - db-reset deps-lock deps-update clean clean-all help + typecheck typecheck-backend typecheck-frontend check build-frontend build-backend \ + docker-up docker-down docker-logs docker-build \ + postgres-up postgres-down postgres-logs db-reset deps-lock deps-update clean clean-all help # Colors for terminal output CYAN := \033[36m @@ -17,7 +17,7 @@ install: install-backend install-frontend ## Install all dependencies install-backend: ## Install backend dependencies @echo "$(CYAN)Installing backend dependencies...$(RESET)" - cd backend && uv sync + cd backend && go mod download install-frontend: ## Install frontend dependencies @echo "$(CYAN)Installing frontend dependencies...$(RESET)" @@ -39,49 +39,44 @@ dev-tmux: ## Start both servers in tmux split panes split-window -h 'make dev-frontend' \; \ attach -dev-backend: ## Start backend development server +dev-backend: postgres-up ## Start backend development server @echo "$(CYAN)Starting backend server on http://localhost:8000$(RESET)" - cd backend && uv run uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 + cd backend && go run cmd/server/main.go dev-frontend: ## Start frontend development server - @echo "$(CYAN)Starting frontend server on http://localhost:3000$(RESET)" - cd frontend && npm run dev + @echo "$(CYAN)Starting frontend server on http://localhost:5173$(RESET)" + cd frontend && npx vite ##@ Code Quality lint: lint-backend lint-frontend ## Run all linters @echo "$(GREEN)All linting passed$(RESET)" -lint-backend: ## Run backend linter (ruff) +lint-backend: ## Run backend linter (go vet) @echo "$(CYAN)Linting backend...$(RESET)" - cd backend && uv run ruff check . + cd backend && go vet ./... lint-frontend: ## Run frontend linter (eslint) @echo "$(CYAN)Linting frontend...$(RESET)" cd frontend && npm run lint -format: format-backend format-frontend ## Format all code +format: format-backend ## Format all code @echo "$(GREEN)All code formatted$(RESET)" -format-backend: ## Format backend code (ruff) +format-backend: ## Format backend code (go fmt) @echo "$(CYAN)Formatting backend...$(RESET)" - cd backend && uv run ruff format . - cd backend && uv run ruff check . --fix - -format-frontend: ## Format frontend code (prettier) - @echo "$(CYAN)Formatting frontend...$(RESET)" - cd frontend && npx prettier --write "**/*.{ts,tsx,js,jsx,json,css,md}" + cd backend && go fmt ./... typecheck: typecheck-backend typecheck-frontend ## Run all type checkers @echo "$(GREEN)All type checks passed$(RESET)" -typecheck-backend: ## Run backend type checker (mypy) +typecheck-backend: ## Run backend type check (compile) @echo "$(CYAN)Type checking backend...$(RESET)" - cd backend && uv run mypy app/ + cd backend && go build ./... -typecheck-frontend: ## Run frontend type checker (tsc) +typecheck-frontend: ## Run frontend type checker (vue-tsc) @echo "$(CYAN)Type checking frontend...$(RESET)" - cd frontend && npx tsc --noEmit + cd frontend && npm run typecheck check: lint typecheck ## Run all checks (lint + typecheck) @echo "$(GREEN)All checks passed$(RESET)" @@ -92,50 +87,64 @@ build-frontend: ## Build frontend for production @echo "$(CYAN)Building frontend...$(RESET)" cd frontend && npm run build +build-backend: ## Build backend binary + @echo "$(CYAN)Building backend...$(RESET)" + cd backend && go build -o bin/server cmd/server/main.go + ##@ Docker -docker-up: ## Start Docker containers (multi-container) +docker-up: ## Start all services (app + postgres) @echo "$(CYAN)Starting Docker containers...$(RESET)" cd deploy && docker compose up -d --build -docker-down: ## Stop Docker containers +docker-down: ## Stop all services @echo "$(CYAN)Stopping Docker containers...$(RESET)" cd deploy && docker compose down docker-logs: ## Show Docker logs cd deploy && docker compose logs -f -docker-build: ## Build combined Docker image (single container) - @echo "$(CYAN)Building combined Docker image...$(RESET)" +docker-build: ## Build application Docker image + @echo "$(CYAN)Building Docker image...$(RESET)" docker build -t momshell . -docker-build-backend: ## Build backend Docker image - @echo "$(CYAN)Building backend Docker image...$(RESET)" - docker build -t momshell-backend backend/ - -docker-build-frontend: ## Build frontend Docker image - @echo "$(CYAN)Building frontend Docker image...$(RESET)" - docker build -t momshell-frontend frontend/ - ##@ Database -db-reset: ## Reset database (delete and recreate) +postgres-up: ## Start local PostgreSQL (systemd) + @echo "$(CYAN)Ensuring local PostgreSQL is running on localhost:5432...$(RESET)" + @if pg_isready -q 2>/dev/null; then \ + echo "$(GREEN)PostgreSQL is already running$(RESET)"; \ + else \ + echo "$(YELLOW)Starting PostgreSQL via systemctl...$(RESET)"; \ + sudo systemctl start postgresql; \ + until pg_isready -q 2>/dev/null; do sleep 1; done; \ + echo "$(GREEN)PostgreSQL is ready$(RESET)"; \ + fi + +postgres-down: ## Stop local PostgreSQL (systemd) + @echo "$(CYAN)Stopping local PostgreSQL...$(RESET)" + @sudo systemctl stop postgresql + @echo "$(GREEN)PostgreSQL stopped$(RESET)" + +postgres-logs: ## Show local PostgreSQL logs + @journalctl -u postgresql -f + +db-reset: postgres-up ## Reset PostgreSQL schema (drop and recreate public schema) @echo "$(YELLOW)Resetting database...$(RESET)" - rm -f backend/data/momshell.db + psql -U user -d momshell -c "DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public;" @echo "$(GREEN)Database reset. It will be recreated on next server start.$(RESET)" ##@ Dependencies -deps-lock: ## Lock backend dependencies and export requirements.txt +deps-lock: ## Sync backend dependencies @echo "$(CYAN)Locking dependencies...$(RESET)" - cd backend && uv lock - cd backend && uv export > requirements.txt + cd backend && go mod tidy @echo "$(GREEN)Dependencies locked$(RESET)" deps-update: ## Update all dependencies @echo "$(CYAN)Updating backend dependencies...$(RESET)" - cd backend && uv lock --upgrade - cd backend && uv export > requirements.txt + cd backend && go get -u ./... + cd backend && go mod tidy @echo "$(CYAN)Updating frontend dependencies...$(RESET)" cd frontend && npm update @echo "$(GREEN)All dependencies updated$(RESET)" @@ -144,15 +153,13 @@ deps-update: ## Update all dependencies clean: ## Clean all caches and temporary files @echo "$(CYAN)Cleaning caches...$(RESET)" - rm -rf backend/.mypy_cache backend/.ruff_cache backend/.pytest_cache backend/__pycache__ - rm -rf frontend/.next frontend/node_modules/.cache - find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true - find . -type f -name "*.pyc" -delete 2>/dev/null || true + rm -rf backend/bin backend/.cache + rm -rf frontend/node_modules/.cache @echo "$(GREEN)Caches cleaned$(RESET)" -clean-all: clean ## Clean everything including node_modules and .venv - @echo "$(YELLOW)Removing node_modules and .venv...$(RESET)" - rm -rf frontend/node_modules backend/.venv +clean-all: clean ## Clean everything including node_modules + @echo "$(YELLOW)Removing node_modules...$(RESET)" + rm -rf frontend/node_modules @echo "$(GREEN)All cleaned$(RESET)" ##@ Help diff --git a/README.md b/README.md index ea856ea2..88d7965e 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,74 @@ # MomShell - - [![CI](https://img.shields.io/github/actions/workflow/status/koishi510/MomShell/ci.yml?branch=main&style=flat&label=CI)](https://github.com/koishi510/MomShell/actions/workflows/ci.yml) -[![Version](https://img.shields.io/github/v/tag/koishi510/MomShell?style=flat&label=Version)](https://github.com/koishi510/MomShell/tags) [![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL%203.0-blue?style=flat)](LICENSE) -[![Python](https://img.shields.io/badge/Python-3.11-3776AB?style=flat&logo=python&logoColor=white)](https://www.python.org/) -[![Node.js](https://img.shields.io/badge/Node.js-22-339933?style=flat&logo=node.js&logoColor=white)](https://nodejs.org/) +[![Go](https://img.shields.io/badge/Go-1.23-00ADD8?style=flat&logo=go&logoColor=white)](https://go.dev/) +[![Vue](https://img.shields.io/badge/Vue-3-4FC08D?style=flat&logo=vue.js&logoColor=white)](https://vuejs.org/) [![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) -[![FastAPI](https://img.shields.io/badge/FastAPI-009688?style=flat&logo=fastapi&logoColor=white)](https://fastapi.tiangolo.com/) -[![Next.js](https://img.shields.io/badge/Next.js-000000?style=flat&logo=next.js&logoColor=white)](https://nextjs.org/) [![Docker](https://img.shields.io/badge/Docker-2496ED?style=flat&logo=docker&logoColor=white)](https://www.docker.com/) -An AI-powered postpartum recovery platform with emotional support, exercise coaching, community connection, and partner engagement for new mothers. +AI-powered postpartum recovery platform with emotional support, community connection, and an embedded admin panel. ## Features -| Module | Description | -| -------------------- | ---------------------------------------------------------------------------------------------------------------- | -| **Soul Companion** | AI chat companion providing emotional support with conversation memory and web search for fact-checked responses | -| **Sisterhood Bond** | Community connecting mothers with verified healthcare professionals and fellow moms | -| **Recovery Coach** | Real-time pose detection with voice-guided postpartum exercises and progress tracking | -| **Guardian Partner** | Gamified system engaging partners in the recovery journey with tasks and level progression | -| **Echo Domain** | Meditative space for mothers to reconnect with their pre-motherhood self, with a partner observation window | - -[View detailed feature documentation](docs/features.md) +| Module | Description | +|--------|-------------| +| **Soul Companion** | AI chat companion with conversation memory and emotional support | +| **Sisterhood Bond** | Community Q&A with verified healthcare professionals and content moderation | +| **Admin Panel** | Embedded single-page admin at `/admin` — dashboard, user CRUD, config management | ## Quick Start ```bash -# Clone and setup git clone https://github.com/koishi510/MomShell.git cd MomShell ./scripts/dev-setup.sh # Start (in separate terminals) -make dev-backend -make dev-frontend - -# Access at http://localhost:3000 +make dev-backend # http://localhost:8000 +make dev-frontend # http://localhost:5173 ``` [Full getting started guide](docs/getting-started.md) +## Project Structure + +``` +MomShell/ +├── backend/ # Go (Gin + GORM + PostgreSQL) +│ ├── cmd/server/ # Entry point +│ ├── internal/ # App code (handler/service/repository/model/dto) +│ │ └── admin/ # Embedded admin panel (go:embed) +│ └── pkg/ # Shared utilities (JWT, password, OpenAI) +├── frontend/ # Vue 3 (Vite + TypeScript + Pinia) +│ └── src/ +├── deploy/ # Docker Compose + Nginx +├── docs/ # Documentation +└── Makefile # Development commands +``` + ## Docker Deployment ```bash -cp .env.example .env -# Edit .env, set MODELSCOPE_KEY - -cd deploy -docker compose up -d --build -# Access at http://localhost:7860 +cp .env.example .env # Edit with your config +make docker-build-backend ``` [Full deployment guide](docs/deployment.md) ## Documentation -| Document | Description | -| ------------------------------------------ | ----------------------------- | -| [Features](docs/features.md) | Detailed feature descriptions | -| [Getting Started](docs/getting-started.md) | Installation and setup | -| [Development](docs/development.md) | Development environment | -| [Deployment](docs/deployment.md) | Docker deployment | -| [Configuration](docs/configuration.md) | Environment variables | -| [Architecture](docs/architecture.md) | Technical overview | -| [Contributing](CONTRIBUTING.md) | Contribution guidelines | -| [Changelog](CHANGELOG.md) | Version history | -| [Code of Conduct](CODE_OF_CONDUCT.md) | Community guidelines | -| [Security](SECURITY.md) | Security policy | +| Document | Description | +|----------|-------------| +| [Getting Started](docs/getting-started.md) | Installation and setup | +| [Development](docs/development.md) | Development workflow | +| [Configuration](docs/configuration.md) | Environment variables | +| [Architecture](docs/architecture.md) | Technical overview | +| [Deployment](docs/deployment.md) | Docker deployment | +| [Features](docs/features.md) | Feature descriptions | +| [Contributing](CONTRIBUTING.md) | Contribution guidelines | +| [Changelog](CHANGELOG.md) | Version history | ## License -This project is licensed under the AGPL-3.0 License - see the [LICENSE](LICENSE) file for details. +[AGPL-3.0](LICENSE) diff --git a/backend/.python-version b/backend/.python-version deleted file mode 100644 index 2c073331..00000000 --- a/backend/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.11 diff --git a/backend/Dockerfile b/backend/Dockerfile index 3b01a0ca..10996bc7 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,62 +1,13 @@ -# syntax=docker/dockerfile:1 -# ============================================================================= -# MomShell Backend - Standalone Container -# ============================================================================= - -FROM python:3.11-slim-bookworm - -# Environment variables -ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 \ - PORT=8000 \ - MPLCONFIGDIR=/tmp/matplotlib - +FROM golang:1.23-alpine AS builder WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 go build -o /server cmd/server/main.go -# Install system dependencies (MediaPipe/OpenCV requirements) -RUN apt-get update && apt-get install -y --no-install-recommends \ - libglib2.0-0 \ - libgl1-mesa-glx \ - libsm6 \ - libxext6 \ - libxrender1 \ - libgomp1 \ - libgcc-s1 \ - libstdc++6 \ - libx11-6 \ - libxcb1 \ - libxau6 \ - libxdmcp6 \ - && rm -rf /var/lib/apt/lists/* - -# Install Python dependencies -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -# Copy backend code -COPY app/ /app/app/ - -# Create data and models directories -RUN mkdir -p /app/data /app/models - -# Download MediaPipe LITE model with retry logic -RUN python3 << 'PYEOF' -import urllib.request, time -url = "https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_lite/float16/1/pose_landmarker_lite.task" -path = "/app/models/pose_landmarker_lite.task" -for i in range(5): - try: - urllib.request.urlretrieve(url, path) - print("Downloaded successfully") - break - except Exception as e: - print(f"Attempt {i+1} failed: {e}") - if i < 4: - time.sleep(3 * (i + 1)) -else: - raise Exception("Failed to download after 5 attempts") -PYEOF - +FROM alpine:3.20 +RUN apk --no-cache add ca-certificates tzdata +WORKDIR /app +COPY --from=builder /server . EXPOSE 8000 - -CMD ["python", "-m", "app.main"] +CMD ["./server"] diff --git a/backend/Makefile b/backend/Makefile new file mode 100644 index 00000000..9de880f6 --- /dev/null +++ b/backend/Makefile @@ -0,0 +1,13 @@ +.PHONY: run build test clean + +run: + go run cmd/server/main.go + +build: + go build -o bin/server cmd/server/main.go + +test: + go test ./... + +clean: + rm -rf bin/ diff --git a/backend/app/__init__.py b/backend/app/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/app/api/v1/__init__.py b/backend/app/api/v1/__init__.py deleted file mode 100644 index 639b145c..00000000 --- a/backend/app/api/v1/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""API v1 routes package.""" - -from app.api.v1 import exercises, progress, websocket - -__all__ = ["exercises", "progress", "websocket"] diff --git a/backend/app/api/v1/exercises.py b/backend/app/api/v1/exercises.py deleted file mode 100644 index 54e07d0d..00000000 --- a/backend/app/api/v1/exercises.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Exercise library REST API routes.""" - -from fastapi import APIRouter - -from app.schemas.exercise import Exercise, ExerciseCategory, ExerciseSession -from app.services.coach.exercises.library import ( - get_all_exercises, - get_all_sessions, - get_exercise, - get_exercises_by_category, - get_session, -) - -router = APIRouter(prefix="/exercises", tags=["exercises"]) - - -@router.get("", response_model=list[Exercise]) -@router.get("/", response_model=list[Exercise]) -async def list_exercises() -> list[Exercise]: - """List all available exercises.""" - return get_all_exercises() - - -@router.get("/category/{category}", response_model=list[Exercise]) -async def list_exercises_by_category(category: ExerciseCategory) -> list[Exercise]: - """List exercises by category.""" - return get_exercises_by_category(category) - - -@router.get("/{exercise_id}", response_model=Exercise | None) -async def get_exercise_detail(exercise_id: str) -> Exercise | None: - """Get details of a specific exercise.""" - return get_exercise(exercise_id) - - -@router.get("/sessions", response_model=list[ExerciseSession]) -@router.get("/sessions/", response_model=list[ExerciseSession]) -async def list_sessions() -> list[ExerciseSession]: - """List all available training sessions.""" - return get_all_sessions() - - -@router.get("/sessions/{session_id}", response_model=ExerciseSession | None) -async def get_session_detail(session_id: str) -> ExerciseSession | None: - """Get details of a specific training session.""" - return get_session(session_id) diff --git a/backend/app/api/v1/progress.py b/backend/app/api/v1/progress.py deleted file mode 100644 index 49b422d5..00000000 --- a/backend/app/api/v1/progress.py +++ /dev/null @@ -1,123 +0,0 @@ -"""Progress tracking REST API routes.""" - -from typing import Annotated - -from fastapi import APIRouter, Depends -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession - -from app.core.database import get_db -from app.schemas.progress import UserProgress -from app.services.auth.dependencies import get_current_user_jwt_optional -from app.services.coach.models import CoachProgress -from app.services.coach.progress.tracker import create_progress_tracker -from app.services.community.models import User - -router = APIRouter(prefix="/progress", tags=["progress"]) - -# Shared progress tracker instance (for anonymous users) -_progress_tracker = create_progress_tracker() - - -async def _get_db_progress(db: AsyncSession, user_id: str) -> CoachProgress | None: - """Get progress from database for a user.""" - result = await db.execute( - select(CoachProgress).where(CoachProgress.user_id == user_id) - ) - return result.scalar_one_or_none() - - -async def _save_db_progress( - db: AsyncSession, user_id: str, progress: UserProgress -) -> None: - """Save progress to database for a user.""" - db_progress = await _get_db_progress(db, user_id) - if db_progress is None: - db_progress = CoachProgress(user_id=user_id) - db.add(db_progress) - db_progress.set_progress(progress.model_dump(mode="json")) - await db.commit() - - -@router.get("/{user_id}", response_model=UserProgress) -async def get_user_progress(user_id: str) -> UserProgress: - """Get progress data for an anonymous user (legacy).""" - return _progress_tracker.get_or_create_progress(user_id) - - -@router.get("/{user_id}/summary") -async def get_progress_summary( - user_id: str, - db: Annotated[AsyncSession, Depends(get_db)], - current_user: Annotated[User | None, Depends(get_current_user_jwt_optional)], -) -> dict: - """Get a summary of user progress for display.""" - # If authenticated and the user_id matches, try to load from database - if current_user and current_user.id == user_id: - db_progress = await _get_db_progress(db, user_id) - if db_progress and db_progress.progress_data: - progress_dict = db_progress.get_progress() - if progress_dict: - # Reconstruct UserProgress and get summary - progress = UserProgress.model_validate(progress_dict) - # Store in memory cache for session recording - _progress_tracker._progress_cache[user_id] = progress - return _progress_tracker.get_summary(user_id) - - return _progress_tracker.get_summary(user_id) - - -@router.get("/{user_id}/achievements") -async def get_user_achievements( - user_id: str, - db: Annotated[AsyncSession, Depends(get_db)], - current_user: Annotated[User | None, Depends(get_current_user_jwt_optional)], -) -> list[dict]: - """Get user's achievements.""" - # If authenticated and the user_id matches, try to load from database - if current_user and current_user.id == user_id: - db_progress = await _get_db_progress(db, user_id) - if db_progress and db_progress.progress_data: - progress_dict = db_progress.get_progress() - if progress_dict: - progress = UserProgress.model_validate(progress_dict) - _progress_tracker._progress_cache[user_id] = progress - return [ - { - "id": a.id, - "name": a.name, - "description": a.description, - "icon": a.icon, - "is_earned": a.is_earned, - "earned_at": a.earned_at.isoformat() if a.earned_at else None, - } - for a in progress.achievements - ] - - progress = _progress_tracker.get_or_create_progress(user_id) - return [ - { - "id": a.id, - "name": a.name, - "description": a.description, - "icon": a.icon, - "is_earned": a.is_earned, - "earned_at": a.earned_at.isoformat() if a.earned_at else None, - } - for a in progress.achievements - ] - - -@router.post("/{user_id}/save") -async def save_user_progress( - user_id: str, - db: Annotated[AsyncSession, Depends(get_db)], - current_user: Annotated[User | None, Depends(get_current_user_jwt_optional)], -) -> dict: - """Save current progress to database (for authenticated users).""" - if not current_user or current_user.id != user_id: - return {"saved": False, "message": "未登录或用户ID不匹配"} - - progress = _progress_tracker.get_or_create_progress(user_id) - await _save_db_progress(db, user_id, progress) - return {"saved": True, "message": "进度已保存"} diff --git a/backend/app/api/v1/websocket.py b/backend/app/api/v1/websocket.py deleted file mode 100644 index 070d12d5..00000000 --- a/backend/app/api/v1/websocket.py +++ /dev/null @@ -1,409 +0,0 @@ -"""WebSocket route for real-time coaching communication. - -Optimized with: -- Async image processing for better performance -- Shared variable pattern for latest frame (simpler and more reliable) -- Non-blocking TTS generation -""" - -import asyncio -import base64 -import json -from concurrent.futures import ThreadPoolExecutor -from datetime import datetime -from typing import Any, cast - -import cv2 -import numpy as np -from fastapi import APIRouter, WebSocket, WebSocketDisconnect -from numpy.typing import NDArray - -from app.schemas.progress import SessionRecord -from app.services.coach.progress.tracker import ProgressTracker, create_progress_tracker -from app.services.coach.workflow.graph import CoachWorkflow, create_workflow -from app.services.coach.workflow.state import SessionState - -router = APIRouter() - -# Thread pool for image encode/decode operations (increased for better parallelism) -_image_executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="image_proc") - - -def _decode_frame(frame_data: str) -> NDArray[np.uint8] | None: - """Decode base64 frame to numpy array (CPU-bound).""" - img_bytes = base64.b64decode(frame_data) - nparr = np.frombuffer(img_bytes, np.uint8) - frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR) - if frame is None: - return None - return cast(NDArray[np.uint8], frame) - - -def _encode_frame(frame: NDArray[np.uint8], quality: int = 60) -> str: - """Encode frame to base64 JPEG (CPU-bound).""" - _, buffer = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, quality]) - return base64.b64encode(buffer.tobytes()).decode("utf-8") - - -async def decode_frame_async(frame_data: str) -> NDArray[np.uint8] | None: - """Decode base64 frame asynchronously.""" - loop = asyncio.get_event_loop() - return await loop.run_in_executor(_image_executor, _decode_frame, frame_data) - - -async def encode_frame_async(frame: NDArray[np.uint8], quality: int = 60) -> str: - """Encode frame to base64 JPEG asynchronously.""" - loop = asyncio.get_event_loop() - return await loop.run_in_executor(_image_executor, _encode_frame, frame, quality) - - -class ConnectionManager: - """Manages WebSocket connections for coaching sessions.""" - - def __init__(self) -> None: - """Initialize the connection manager.""" - self._active_connections: dict[str, WebSocket] = {} - self._workflows: dict[str, CoachWorkflow] = {} - self._progress_tracker = create_progress_tracker() - - async def connect(self, websocket: WebSocket, session_id: str) -> None: - """Accept a new WebSocket connection.""" - await websocket.accept() - self._active_connections[session_id] = websocket - - def disconnect(self, session_id: str) -> None: - """Remove a disconnected session.""" - self._active_connections.pop(session_id, None) - workflow = self._workflows.pop(session_id, None) - if workflow: - workflow.end_session() - - async def send_json(self, session_id: str, data: dict[str, Any]) -> None: - """Send JSON data to a session.""" - websocket = self._active_connections.get(session_id) - if websocket: - await websocket.send_json(data) - - def get_workflow(self, session_id: str) -> CoachWorkflow | None: - """Get the workflow for a session.""" - return self._workflows.get(session_id) - - def create_workflow(self, session_id: str, use_llm: bool = True) -> CoachWorkflow: - """Create a new workflow for a session.""" - workflow = create_workflow(use_llm=use_llm) - self._workflows[session_id] = workflow - return workflow - - @property - def progress_tracker(self) -> ProgressTracker: - """Get the progress tracker.""" - return self._progress_tracker - - -manager = ConnectionManager() - - -@router.websocket("/ws/coach/{session_id}") -async def coaching_websocket(websocket: WebSocket, session_id: str) -> None: - """WebSocket endpoint for real-time coaching. - - Architecture: Uses shared variable pattern with Event for latest frame. - This is simpler and more reliable than queue-based approaches. - - Protocol: - - Client sends: { "type": "start", "exercise_id": "...", "user_id": "..." } - - Client sends: { "type": "frame", "data": "" } - - Client sends: { "type": "control", "action": "pause|resume|rest|end_rest|end" } - - Server sends: { "type": "state", "data": {...} } - - Server sends: { "type": "feedback", "text": "...", "audio": "" } - """ - await manager.connect(websocket, session_id) - print(f"[WS] Connected: {session_id}") - - # Session state - workflow: CoachWorkflow | None = None - user_id: str = "default_user" - session_start_time: datetime | None = None - is_connected = True - - # Shared frame data (no lock needed in single-threaded async) - latest_frame: str | None = None - frame_ready = False - - # Shutdown signal - shutdown_event = asyncio.Event() - - async def safe_send(data: dict[str, Any]) -> bool: - """Safely send data, return False if connection is closed.""" - nonlocal is_connected - if not is_connected: - return False - try: - await websocket.send_json(data) - return True - except Exception as e: - print(f"[WS] Send failed: {e}") - is_connected = False - return False - - async def frame_processor() -> None: - """Process frames - polls for new frames, always uses latest.""" - nonlocal latest_frame, frame_ready, workflow, is_connected - - while not shutdown_event.is_set() and is_connected: - try: - # Check if there's a frame to process - if not frame_ready or latest_frame is None: - await asyncio.sleep(0.01) # 10ms polling - continue - - # Take the frame and clear flag - frame_data = latest_frame - latest_frame = None - frame_ready = False - - if workflow is None: - continue - - # Decode frame - frame = await decode_frame_async(frame_data) - if frame is None: - continue - - typed_frame = cast(NDArray[np.uint8], frame) - - # Process frame through workflow - state, _ = await workflow.process_frame(typed_frame) - - # Build response - response: dict[str, Any] = { - "type": "state", - "data": { - "session_state": state.session_state.value, - "progress": state.get_progress(), - "analysis": state.analysis_result.model_dump() - if state.analysis_result - else None, - }, - } - - # Add keypoints - if state.current_pose: - keypoints_data = { - str(idx): { - "x": pt.x, - "y": pt.y, - "visibility": pt.visibility, - } - for idx, pt in state.current_pose.keypoints.items() - } - response["keypoints"] = keypoints_data - - if state.analysis_result: - if state.analysis_result.is_correct: - response["skeleton_color"] = "green" - elif state.analysis_result.score >= 60: - response["skeleton_color"] = "yellow" - else: - response["skeleton_color"] = "red" - else: - response["skeleton_color"] = "white" - - # Handle feedback - TTS in background - if state.pending_feedback and state.should_speak: - feedback_text = state.pending_feedback.text - feedback_type = state.pending_feedback.type.value - state.should_speak = False - - async def send_audio( - text: str = feedback_text, - ftype: str = feedback_type, - ) -> None: - try: - if workflow is None: - return - audio = await workflow.get_speech_audio() - if audio and is_connected: - await safe_send( - { - "type": "feedback", - "text": text, - "feedback_type": ftype, - "audio": base64.b64encode(audio).decode( - "utf-8" - ), - } - ) - except Exception as e: - print(f"[WS] TTS error: {e}") - - # Fire and forget - asyncio.create_task(send_audio()) - - if state.session_state == SessionState.COMPLETED: - response["completed"] = True - - # Send response - if not await safe_send(response): - break - - except Exception as e: - print(f"[WS] Frame processor error: {e}") - import traceback - - traceback.print_exc() - - async def message_receiver() -> None: - """Receive messages from WebSocket.""" - nonlocal \ - workflow, \ - user_id, \ - session_start_time, \ - is_connected, \ - latest_frame, \ - frame_ready - - try: - while not shutdown_event.is_set() and is_connected: - raw_data = await websocket.receive_text() - message = json.loads(raw_data) - msg_type = message.get("type") - - if msg_type != "frame": - print(f"[WS] Received: {msg_type}") - - if msg_type == "start": - exercise_id = message.get("exercise_id") - user_id = message.get("user_id", "default_user") - use_llm = message.get("use_llm", True) - - print(f"[WS] Starting: {exercise_id}") - await safe_send({"type": "ack", "message": "Starting..."}) - - try: - workflow = manager.create_workflow(session_id, use_llm=use_llm) - state = workflow.start_session(exercise_id) - session_start_time = datetime.now() - - await safe_send( - { - "type": "state", - "data": { - "session_state": state.session_state.value, - "exercise": state.current_exercise.model_dump() - if state.current_exercise - else None, - "progress": state.get_progress(), - }, - } - ) - except Exception as e: - print(f"[WS] Start error: {e}") - await safe_send({"type": "error", "message": str(e)}) - - elif msg_type == "begin": - if workflow: - new_state = workflow.start_exercise() - if new_state: - await safe_send( - { - "type": "state", - "data": { - "session_state": new_state.session_state.value, - "progress": new_state.get_progress(), - }, - } - ) - - elif msg_type == "frame": - if workflow is None: - continue - - frame_data = message.get("data", "") - if not frame_data: - continue - - # Update latest frame (always overwrites, no lock needed) - latest_frame = frame_data - frame_ready = True - - elif msg_type == "control": - if workflow is None: - continue - - action = message.get("action") - state = None - - if action == "pause": - state = workflow.pause() - elif action == "resume": - state = workflow.resume() - elif action == "rest": - state = workflow.rest() - elif action == "end_rest": - state = workflow.end_rest() - elif action == "end": - summary = workflow.end_session() - - if session_start_time: - record = SessionRecord( - session_id=session_id, - user_id=user_id, - exercise_id=summary.get("exercise", "unknown"), - started_at=session_start_time, - ended_at=datetime.now(), - duration_seconds=summary.get("session_duration", 0), - average_score=summary.get("average_score", 0), - completed_sets=summary.get("completed_sets", 0), - completed_reps=summary.get("completed_reps", 0), - ) - - progress, new_achievements = ( - manager.progress_tracker.record_session(user_id, record) - ) - - summary["new_achievements"] = [ - { - "name": a.name, - "description": a.description, - "icon": a.icon, - } - for a in new_achievements - ] - summary["progress_summary"] = ( - manager.progress_tracker.get_summary(user_id) - ) - - await safe_send({"type": "session_ended", "summary": summary}) - workflow = None - continue - - if state: - await safe_send( - { - "type": "state", - "data": { - "session_state": state.session_state.value, - "progress": state.get_progress(), - }, - } - ) - - except WebSocketDisconnect: - print(f"[WS] Disconnected: {session_id}") - except Exception as e: - print(f"[WS] Receiver error: {e}") - finally: - is_connected = False - shutdown_event.set() - - # Run receiver and processor concurrently - try: - await asyncio.gather( - message_receiver(), - frame_processor(), - return_exceptions=True, - ) - finally: - shutdown_event.set() - manager.disconnect(session_id) diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/app/core/config.py b/backend/app/core/config.py deleted file mode 100644 index e7d798db..00000000 --- a/backend/app/core/config.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Application configuration.""" - -import secrets -from functools import lru_cache -from pathlib import Path - -from pydantic import model_validator -from pydantic_settings import BaseSettings, SettingsConfigDict - -# Security constants -DEFAULT_JWT_SECRET = "your-secret-key-change-in-production" -_generated_jwt_secret: str | None = None - - -def _find_env_file() -> str | None: - """Find .env file, checking multiple locations for Docker compatibility.""" - # In Docker: /app/app/core/config.py -> parents[3] = / - # In local dev: backend/app/core/config.py -> parents[3] = project root - config_path = Path(__file__).resolve() - - # Try project root (local dev: backend/app/core/config.py) - project_root = config_path.parents[3] / ".env" - if project_root.exists(): - return str(project_root) - - # In Docker, env vars are passed via --env-file, no file needed - return None - - -class Settings(BaseSettings): - """Application settings loaded from environment variables.""" - - model_config = SettingsConfigDict( - env_file=_find_env_file(), - env_file_encoding="utf-8", - extra="ignore", - ) - - # Application - app_name: str = "MomShell" - debug: bool = False - - # Database (use /app/data for Docker) - database_url: str = "sqlite+aiosqlite:///./data/momshell.db" - - # ModelScope API Configuration - modelscope_key: str = "" - modelscope_base_url: str = "https://api-inference.modelscope.cn/v1" - modelscope_image_base_url: str = "https://api-inference.modelscope.cn/" - modelscope_model: str = "Qwen/Qwen2.5-72B-Instruct" - modelscope_image_model: str = "" - - # MediaPipe Configuration - pose_model_complexity: int = 1 # 0, 1, or 2 - min_detection_confidence: float = 0.5 - min_tracking_confidence: float = 0.3 # Lowered to reduce re-detection frequency - - # TTS Configuration - tts_voice: str = "zh-CN-XiaoxiaoNeural" - tts_rate: str = "-10%" - - # Safety thresholds - max_deviation_angle: float = 30.0 # degrees - fatigue_detection_threshold: float = 0.7 - rest_prompt_interval: int = 300 # seconds - - # JWT Authentication - jwt_secret_key: str = DEFAULT_JWT_SECRET - jwt_algorithm: str = "HS256" - jwt_access_token_expire_minutes: int = 30 - jwt_refresh_token_expire_days: int = 7 - - @model_validator(mode="after") - def _auto_generate_jwt_secret(self) -> "Settings": - """Auto-generate JWT secret if using default value.""" - global _generated_jwt_secret - if self.jwt_secret_key == DEFAULT_JWT_SECRET: - if _generated_jwt_secret is None: - _generated_jwt_secret = secrets.token_hex(32) - print("[Config] Auto-generated JWT secret (not persisted)") - object.__setattr__(self, "jwt_secret_key", _generated_jwt_secret) - return self - - # Web Search (Firecrawl API for reducing AI hallucinations) - firecrawl_api_key: str = "" - - # Initial Admin Account (created on first startup if set) - admin_username: str = "" - admin_email: str = "" - admin_password: str = "" - - -@lru_cache -def get_settings() -> Settings: - """Get cached settings instance.""" - return Settings() diff --git a/backend/app/core/database.py b/backend/app/core/database.py deleted file mode 100644 index 38325649..00000000 --- a/backend/app/core/database.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Database configuration and session management.""" - -from collections.abc import AsyncGenerator - -from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine -from sqlalchemy.orm import DeclarativeBase - -from app.core.config import get_settings - -settings = get_settings() - -engine = create_async_engine( - settings.database_url, - echo=settings.debug, -) - -async_session_maker = async_sessionmaker( - engine, - class_=AsyncSession, - expire_on_commit=False, -) - - -class Base(DeclarativeBase): - """Base class for all database models.""" - - pass - - -async def get_db() -> AsyncGenerator[AsyncSession, None]: - """Dependency for getting async database sessions.""" - async with async_session_maker() as session: - try: - yield session - await session.commit() - except Exception: - await session.rollback() - raise - - -async def init_db() -> None: - """Initialize database tables.""" - async with engine.begin() as conn: - await conn.run_sync(Base.metadata.create_all) diff --git a/backend/app/main.py b/backend/app/main.py deleted file mode 100644 index 9525d674..00000000 --- a/backend/app/main.py +++ /dev/null @@ -1,260 +0,0 @@ -"""MomShell Recovery Coach - Main FastAPI Application (ModelScope Deploy).""" - -import asyncio -import os -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager -from pathlib import Path - -from fastapi import FastAPI, Request -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import FileResponse, HTMLResponse -from fastapi.staticfiles import StaticFiles -from fastapi.templating import Jinja2Templates - -from app.api.v1 import exercises, progress, websocket -from app.core.config import get_settings -from app.core.database import init_db -from app.services.auth import auth_router - -# Import models to register them with SQLAlchemy Base -from app.services.chat import models as chat_models # noqa: F401 -from app.services.chat import router as companion_router -from app.services.coach import models as coach_models # noqa: F401 -from app.services.community import community_router -from app.services.community import models as community_models # noqa: F401 -from app.services.echo import echo_router -from app.services.echo import models as echo_models # noqa: F401 -from app.services.guardian import guardian_router -from app.services.guardian import models as guardian_models # noqa: F401 - -settings = get_settings() - - -def check_security_settings() -> None: - """Check for insecure settings in production mode.""" - from app.core.config import _generated_jwt_secret - - if settings.debug: - return # Skip checks in debug mode - - if _generated_jwt_secret is not None: - print("\033[93m" + "=" * 60) - print("WARNING: JWT secret was auto-generated (not persisted).") - print("User sessions will be invalidated on restart.") - print("Set JWT_SECRET_KEY in environment for persistent sessions.") - print("=" * 60 + "\033[0m") - - -# Paths -BASE_DIR = Path(__file__).resolve().parent -STATIC_DIR = BASE_DIR / "static" -TEMPLATES_DIR = BASE_DIR / "templates" -# Frontend static files (built from Next.js export) -FRONTEND_DIR = Path("/app/frontend_dist") - - -def preload_mediapipe() -> None: - """Preload MediaPipe to avoid blocking during WebSocket handling.""" - print("[Startup] Preloading MediaPipe...") - try: - from app.services.coach.pose.detector import PoseDetector - - # Create and close a detector to trigger model download and initialization - detector = PoseDetector() - detector.close() - print("[Startup] MediaPipe loaded successfully") - except Exception as e: - print(f"[Startup] MediaPipe preload warning: {e}") - - -async def preload_mediapipe_background() -> None: - """Preload MediaPipe in background to not block startup.""" - loop = asyncio.get_event_loop() - await loop.run_in_executor(None, preload_mediapipe) - - -def ensure_db_directory() -> None: - """Ensure database directory exists (for SQLite on /mnt/workspace).""" - db_url = settings.database_url - if db_url.startswith("sqlite"): - # Extract path from sqlite URL: sqlite+aiosqlite:////mnt/workspace/momshell.db - # The path starts after the driver specification - path_part = db_url.split("///")[-1] - db_path = Path(path_part) - db_dir = db_path.parent - if not db_dir.exists(): - db_dir.mkdir(parents=True, exist_ok=True) - print(f"[Startup] Created database directory: {db_dir}") - - -async def ensure_admin() -> None: - """Create initial admin account if configured via environment variables.""" - if not ( - settings.admin_username and settings.admin_email and settings.admin_password - ): - return - - from sqlalchemy import select - - from app.core.database import async_session_maker - from app.services.auth.security import get_password_hash - from app.services.community.enums import UserRole - from app.services.community.models import User - - async with async_session_maker() as db: - # Check if admin already exists - result = await db.execute( - select(User).where( - (User.username == settings.admin_username) - | (User.email == settings.admin_email) - ) - ) - existing = result.scalar_one_or_none() - - if existing: - if existing.role != UserRole.ADMIN: - existing.role = UserRole.ADMIN - await db.commit() - print(f"[Startup] User '{existing.username}' promoted to admin") - else: - admin = User( - username=settings.admin_username, - email=settings.admin_email, - password_hash=get_password_hash(settings.admin_password), - nickname="管理员", - role=UserRole.ADMIN, - is_active=True, - is_banned=False, - ) - db.add(admin) - await db.commit() - print(f"[Startup] Admin account '{settings.admin_username}' created") - - -@asynccontextmanager -async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: - """Application lifespan manager.""" - # Startup - check_security_settings() - ensure_db_directory() - await init_db() - # Seed guardian task templates - from app.core.database import async_session_maker - from app.services.guardian.seed_data import seed_task_templates - - async with async_session_maker() as session: - await seed_task_templates(session) - # Seed echo domain data - from app.services.echo.seed_data import seed_echo_data - - async with async_session_maker() as session: - echo_counts = await seed_echo_data(session) - if echo_counts["scenes"] > 0 or echo_counts["audio"] > 0: - print(f"[Startup] Echo data seeded: {echo_counts}") - # Create initial admin if configured - await ensure_admin() - # Start MediaPipe preloading in background (non-blocking) - asyncio.create_task(preload_mediapipe_background()) - yield - # Shutdown - - -app = FastAPI( - title=settings.app_name, - description="AI-powered postpartum recovery coaching application", - version="0.1.0", - lifespan=lifespan, -) - -# CORS middleware -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# Backend static files (for backend-specific assets) -STATIC_DIR.mkdir(parents=True, exist_ok=True) -app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") - -# Templates -templates = Jinja2Templates(directory=str(TEMPLATES_DIR)) - -# Include API routers -app.include_router(websocket.router, prefix="/api/v1") -app.include_router(exercises.router, prefix="/api/v1") -app.include_router(progress.router, prefix="/api/v1") -app.include_router(companion_router, prefix="/api/v1") -app.include_router(auth_router, prefix="/api/v1") -app.include_router(community_router, prefix="/api/v1/community") -app.include_router(guardian_router, prefix="/api/v1") -app.include_router(echo_router, prefix="/api/v1") - - -@app.api_route("/health", methods=["GET", "HEAD"]) -async def health_check() -> dict: - """Health check endpoint.""" - return {"status": "healthy"} - - -# Mount frontend static files if exists -if FRONTEND_DIR.exists(): - # Mount _next directory for Next.js static assets - next_static = FRONTEND_DIR / "_next" - if next_static.exists(): - app.mount("/_next", StaticFiles(directory=str(next_static)), name="next_static") - - # SPA fallback - serve frontend for all non-API routes - @app.api_route( - "/{full_path:path}", methods=["GET", "HEAD"], response_class=HTMLResponse - ) - async def serve_spa(request: Request, full_path: str): - """Serve frontend SPA for all non-API routes.""" - # Try to serve the exact file first - file_path = FRONTEND_DIR / full_path - if file_path.exists() and file_path.is_file(): - return FileResponse(file_path) - - # Try with index.html for directory paths (Next.js trailingSlash) - if file_path.exists() and file_path.is_dir(): - index_file = file_path / "index.html" - if index_file.exists(): - return FileResponse(index_file) - - # Try adding .html extension - html_file = FRONTEND_DIR / f"{full_path}.html" - if html_file.exists(): - return FileResponse(html_file) - - # Fallback to index.html for SPA routing - index_html = FRONTEND_DIR / "index.html" - if index_html.exists(): - return FileResponse(index_html) - - # Final fallback to backend template - return templates.TemplateResponse("index.html", {"request": request}) -else: - # No frontend build, serve backend template - @app.api_route("/", methods=["GET", "HEAD"], response_class=HTMLResponse) - async def root(request: Request) -> HTMLResponse: - """Serve the main application page.""" - return templates.TemplateResponse("index.html", {"request": request}) - - -if __name__ == "__main__": - import uvicorn - - # Try to use uvloop for better async performance - try: - import uvloop - - uvloop.install() - print("[Startup] uvloop installed for better async performance") - except ImportError: - pass - - port = int(os.environ.get("PORT", 8000)) - uvicorn.run(app, host="0.0.0.0", port=port) diff --git a/backend/app/models/.gitkeep b/backend/app/models/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/app/schemas/exercise.py b/backend/app/schemas/exercise.py deleted file mode 100644 index d100bf78..00000000 --- a/backend/app/schemas/exercise.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Exercise-related Pydantic schemas.""" - -from enum import Enum - -from pydantic import BaseModel, Field - - -class ExerciseCategory(str, Enum): - """Categories of postpartum recovery exercises.""" - - BREATHING = "breathing" - PELVIC_FLOOR = "pelvic_floor" - DIASTASIS_RECTI = "diastasis_recti" - POSTURE = "posture" - STRENGTH = "strength" - - -class Difficulty(str, Enum): - """Exercise difficulty levels.""" - - BEGINNER = "beginner" - INTERMEDIATE = "intermediate" - ADVANCED = "advanced" - - -class ExercisePhase(str, Enum): - """Phases within an exercise.""" - - PREPARATION = "preparation" - INHALE = "inhale" - EXHALE = "exhale" - HOLD = "hold" - RELEASE = "release" - REST = "rest" - - -class AngleRequirement(BaseModel): - """Angle requirement for a joint during an exercise phase.""" - - joint_name: str = Field(..., description="Name of the joint angle") - min_angle: float = Field(..., description="Minimum acceptable angle") - max_angle: float = Field(..., description="Maximum acceptable angle") - ideal_angle: float = Field(..., description="Ideal target angle") - - -class PhaseRequirement(BaseModel): - """Requirements for a single phase of an exercise.""" - - phase: ExercisePhase - duration_seconds: float = Field(..., ge=0, description="Duration of this phase") - angles: list[AngleRequirement] = Field( - default_factory=list, description="Angle requirements for this phase" - ) - description: str = Field(..., description="Description of what to do in this phase") - cues: list[str] = Field( - default_factory=list, description="Verbal cues for this phase" - ) - - -class Exercise(BaseModel): - """Complete exercise definition.""" - - id: str = Field(..., description="Unique exercise identifier") - name: str = Field(..., description="Exercise name in Chinese") - name_en: str = Field(..., description="Exercise name in English") - category: ExerciseCategory - difficulty: Difficulty - description: str = Field(..., description="Exercise description") - benefits: list[str] = Field(default_factory=list, description="Health benefits") - contraindications: list[str] = Field( - default_factory=list, description="When NOT to do this exercise" - ) - phases: list[PhaseRequirement] = Field( - default_factory=list, description="Exercise phases" - ) - repetitions: int = Field(default=10, ge=1, description="Recommended repetitions") - sets: int = Field(default=3, ge=1, description="Recommended sets") - rest_between_sets: int = Field( - default=30, ge=0, description="Rest time between sets in seconds" - ) - video_url: str | None = Field(default=None, description="Demo video URL") - thumbnail_url: str | None = Field(default=None, description="Thumbnail image URL") - - -class ExerciseSession(BaseModel): - """A training session containing multiple exercises.""" - - id: str - name: str - description: str - exercises: list[str] = Field( - default_factory=list, description="List of exercise IDs" - ) - total_duration_minutes: int = Field(default=15, description="Estimated duration") - focus_areas: list[ExerciseCategory] = Field( - default_factory=list, description="Target areas" - ) diff --git a/backend/app/schemas/feedback.py b/backend/app/schemas/feedback.py deleted file mode 100644 index 1a6d36e9..00000000 --- a/backend/app/schemas/feedback.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Feedback-related Pydantic schemas.""" - -from enum import Enum - -from pydantic import BaseModel, Field - - -class FeedbackType(str, Enum): - """Types of feedback.""" - - ENCOURAGEMENT = "encouragement" - CORRECTION = "correction" - PHASE_CUE = "phase_cue" - REST_PROMPT = "rest_prompt" - COMPLETION = "completion" - SAFETY_WARNING = "safety_warning" - - -class FeedbackMessage(BaseModel): - """A feedback message to be displayed or spoken.""" - - type: FeedbackType - text: str = Field(..., description="The feedback text") - priority: int = Field( - default=1, ge=1, le=5, description="Priority 1-5, higher = more important" - ) - should_speak: bool = Field( - default=True, description="Whether to speak this message via TTS" - ) - - -class VisualFeedback(BaseModel): - """Visual feedback data for overlay rendering.""" - - keypoint_colors: dict[int, str] = Field( - default_factory=dict, - description="Keypoint index to color mapping (hex)", - ) - highlight_joints: list[str] = Field( - default_factory=list, description="Joints to highlight" - ) - score_display: float = Field(default=0.0, description="Score to display") - phase_text: str = Field(default="", description="Current phase text") - countdown: int | None = Field(default=None, description="Countdown seconds if any") diff --git a/backend/app/schemas/pose.py b/backend/app/schemas/pose.py deleted file mode 100644 index e126aca5..00000000 --- a/backend/app/schemas/pose.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Pose-related Pydantic schemas.""" - -from enum import Enum - -from pydantic import BaseModel, Field - - -class Keypoint(Enum): - """MediaPipe pose keypoint indices.""" - - NOSE = 0 - LEFT_EYE_INNER = 1 - LEFT_EYE = 2 - LEFT_EYE_OUTER = 3 - RIGHT_EYE_INNER = 4 - RIGHT_EYE = 5 - RIGHT_EYE_OUTER = 6 - LEFT_EAR = 7 - RIGHT_EAR = 8 - MOUTH_LEFT = 9 - MOUTH_RIGHT = 10 - LEFT_SHOULDER = 11 - RIGHT_SHOULDER = 12 - LEFT_ELBOW = 13 - RIGHT_ELBOW = 14 - LEFT_WRIST = 15 - RIGHT_WRIST = 16 - LEFT_PINKY = 17 - RIGHT_PINKY = 18 - LEFT_INDEX = 19 - RIGHT_INDEX = 20 - LEFT_THUMB = 21 - RIGHT_THUMB = 22 - LEFT_HIP = 23 - RIGHT_HIP = 24 - LEFT_KNEE = 25 - RIGHT_KNEE = 26 - LEFT_ANKLE = 27 - RIGHT_ANKLE = 28 - LEFT_HEEL = 29 - RIGHT_HEEL = 30 - LEFT_FOOT_INDEX = 31 - RIGHT_FOOT_INDEX = 32 - - -class Point3D(BaseModel): - """3D coordinate point.""" - - x: float = Field(..., description="X coordinate (normalized 0-1)") - y: float = Field(..., description="Y coordinate (normalized 0-1)") - z: float = Field(..., description="Z coordinate (depth)") - visibility: float = Field(default=0.0, ge=0.0, le=1.0) - - -class PoseData(BaseModel): - """Complete pose data from a single frame.""" - - keypoints: dict[int, Point3D] = Field( - default_factory=dict, description="Keypoint index to 3D point mapping" - ) - timestamp: float = Field(..., description="Frame timestamp in seconds") - frame_id: int = Field(default=0, description="Frame sequence number") - - def get_keypoint(self, keypoint: Keypoint) -> Point3D | None: - """Get a specific keypoint by enum.""" - return self.keypoints.get(keypoint.value) - - @property - def is_valid(self) -> bool: - """Check if pose data has sufficient keypoints detected.""" - return len(self.keypoints) >= 17 - - -class PoseAnalysisResult(BaseModel): - """Result of analyzing a pose against expected form.""" - - is_correct: bool = Field(..., description="Whether the pose matches expected form") - score: float = Field(..., ge=0.0, le=100.0, description="Form score 0-100") - deviations: list[str] = Field( - default_factory=list, description="List of detected deviations" - ) - suggestions: list[str] = Field( - default_factory=list, description="Improvement suggestions" - ) - angles: dict[str, float] = Field( - default_factory=dict, description="Measured joint angles" - ) diff --git a/backend/app/schemas/progress.py b/backend/app/schemas/progress.py deleted file mode 100644 index e19b7285..00000000 --- a/backend/app/schemas/progress.py +++ /dev/null @@ -1,82 +0,0 @@ -"""Progress-related Pydantic schemas.""" - -from datetime import datetime -from enum import Enum - -from pydantic import BaseModel, Field - - -class AchievementType(str, Enum): - """Types of achievements/badges.""" - - FIRST_SESSION = "first_session" - STREAK_3 = "streak_3" - STREAK_7 = "streak_7" - STREAK_30 = "streak_30" - PERFECT_FORM = "perfect_form" - COMPLETE_EXERCISE = "complete_exercise" - COMPLETE_SESSION = "complete_session" - STRENGTH_MILESTONE = "strength_milestone" - CONSISTENCY = "consistency" - - -class Achievement(BaseModel): - """User achievement/badge.""" - - id: str - type: AchievementType - name: str - description: str - icon: str = Field(default="star", description="Icon name for display") - earned_at: datetime | None = Field(default=None) - is_earned: bool = Field(default=False) - - -class ExerciseProgress(BaseModel): - """Progress for a single exercise.""" - - exercise_id: str - total_sessions: int = Field(default=0) - total_reps: int = Field(default=0) - average_score: float = Field(default=0.0) - best_score: float = Field(default=0.0) - last_performed: datetime | None = Field(default=None) - - -class StrengthMetric(BaseModel): - """A strength/recovery metric.""" - - name: str - value: float = Field(ge=0, le=100, description="Progress percentage 0-100") - baseline: float = Field(default=0, description="Starting value") - target: float = Field(default=100, description="Target value") - unit: str = Field(default="%") - - -class UserProgress(BaseModel): - """Complete user progress data.""" - - user_id: str - total_sessions: int = Field(default=0) - total_minutes: float = Field(default=0.0) - current_streak: int = Field(default=0) - longest_streak: int = Field(default=0) - last_session_date: datetime | None = Field(default=None) - achievements: list[Achievement] = Field(default_factory=list) - exercise_progress: dict[str, ExerciseProgress] = Field(default_factory=dict) - strength_metrics: list[StrengthMetric] = Field(default_factory=list) - - -class SessionRecord(BaseModel): - """Record of a completed session.""" - - session_id: str - user_id: str - exercise_id: str - started_at: datetime - ended_at: datetime - duration_seconds: float - average_score: float - completed_sets: int - completed_reps: int - achievements_earned: list[str] = Field(default_factory=list) diff --git a/backend/app/services/__init.py b/backend/app/services/__init.py deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/app/services/auth/__init__.py b/backend/app/services/auth/__init__.py deleted file mode 100644 index 4d6a3bc9..00000000 --- a/backend/app/services/auth/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Authentication service module.""" - -from .router import router as auth_router - -__all__ = ["auth_router"] diff --git a/backend/app/services/auth/dependencies.py b/backend/app/services/auth/dependencies.py deleted file mode 100644 index 7fe75a59..00000000 --- a/backend/app/services/auth/dependencies.py +++ /dev/null @@ -1,164 +0,0 @@ -"""Dependencies for authentication.""" - -from typing import Annotated - -from fastapi import Cookie, Depends, HTTPException, Request -from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.orm import selectinload - -from app.core.database import get_db -from app.services.community.models import User - -from .security import decode_token - -# HTTP Bearer scheme for JWT authentication -http_bearer = HTTPBearer(auto_error=False) - - -def get_token_from_request( - request: Request, - credentials: HTTPAuthorizationCredentials | None = None, - access_token: str | None = Cookie(None, alias="momshell_access_token"), -) -> str | None: - """ - Extract token from multiple sources (fallback order): - 1. Authorization: Bearer header - 2. X-Access-Token custom header (for proxies that strip Authorization) - 3. Cookie (momshell_access_token) - """ - # 1. Try Authorization header - if credentials and credentials.credentials: - return credentials.credentials - - # 2. Try custom header (some proxies strip Authorization but not custom headers) - custom_token = request.headers.get("X-Access-Token") - if custom_token: - return custom_token - - # 3. Try cookie - if access_token: - return access_token - - return None - - -async def get_current_user_jwt( - request: Request, - db: Annotated[AsyncSession, Depends(get_db)], - credentials: Annotated[ - HTTPAuthorizationCredentials | None, Depends(http_bearer) - ] = None, - access_token: str | None = Cookie(None, alias="momshell_access_token"), -) -> User: - """ - Get current user from JWT token. - Supports multiple token sources for proxy compatibility. - Raises HTTPException if not authenticated. - """ - token = get_token_from_request(request, credentials, access_token) - if not token: - raise HTTPException(status_code=401, detail="未登录") - - payload = decode_token(token) - if payload is None: - raise HTTPException(status_code=401, detail="无效的令牌") - - token_type = payload.get("type") - if token_type != "access": - raise HTTPException(status_code=401, detail="无效的令牌类型") - - user_id = payload.get("sub") - if not user_id: - raise HTTPException(status_code=401, detail="无效的令牌") - - result = await db.execute( - select(User).options(selectinload(User.certification)).where(User.id == user_id) - ) - user = result.scalar_one_or_none() - - if not user: - raise HTTPException(status_code=401, detail="用户不存在") - - if not user.is_active: - raise HTTPException(status_code=403, detail="账号已禁用") - - if user.is_banned: - raise HTTPException(status_code=403, detail="账号已被封禁") - - return user - - -async def get_current_user_jwt_optional( - request: Request, - db: Annotated[AsyncSession, Depends(get_db)], - credentials: Annotated[ - HTTPAuthorizationCredentials | None, Depends(http_bearer) - ] = None, - access_token: str | None = Cookie(None, alias="momshell_access_token"), -) -> User | None: - """ - Get current user from JWT token if present. - Supports multiple token sources for proxy compatibility. - Returns None if not authenticated (no exception). - """ - token = get_token_from_request(request, credentials, access_token) - if not token: - return None - - payload = decode_token(token) - if payload is None: - return None - - token_type = payload.get("type") - if token_type != "access": - return None - - user_id = payload.get("sub") - if not user_id: - return None - - result = await db.execute( - select(User).options(selectinload(User.certification)).where(User.id == user_id) - ) - user = result.scalar_one_or_none() - - if not user or not user.is_active or user.is_banned: - return None - - return user - - -async def get_refresh_token_user( - db: Annotated[AsyncSession, Depends(get_db)], - credentials: Annotated[ - HTTPAuthorizationCredentials | None, Depends(http_bearer) - ] = None, -) -> str: - """ - Validate refresh token and return user ID. - Used for token refresh endpoint. - """ - if not credentials: - raise HTTPException(status_code=401, detail="未提供刷新令牌") - - payload = decode_token(credentials.credentials) - if payload is None: - raise HTTPException(status_code=401, detail="无效的刷新令牌") - - token_type = payload.get("type") - if token_type != "refresh": - raise HTTPException(status_code=401, detail="无效的令牌类型") - - user_id = payload.get("sub") - if not isinstance(user_id, str): - raise HTTPException(status_code=401, detail="无效的刷新令牌") - - return user_id - - -# Type aliases -CurrentUserJWT = Annotated[User, Depends(get_current_user_jwt)] -OptionalUserJWT = Annotated[User | None, Depends(get_current_user_jwt_optional)] -RefreshTokenUserId = Annotated[str, Depends(get_refresh_token_user)] diff --git a/backend/app/services/auth/router.py b/backend/app/services/auth/router.py deleted file mode 100644 index 4d77be55..00000000 --- a/backend/app/services/auth/router.py +++ /dev/null @@ -1,170 +0,0 @@ -"""Authentication API router.""" - -from typing import Annotated - -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.ext.asyncio import AsyncSession - -from app.core.database import get_db - -from .dependencies import CurrentUserJWT -from .schemas import ( - ChangePasswordRequest, - ForgotPasswordRequest, - LoginRequest, - MessageResponse, - RefreshRequest, - RegisterRequest, - ResetPasswordRequest, - TokenResponse, - UserResponse, -) -from .security import ( - create_password_reset_token, - verify_password, - verify_password_reset_token, -) -from .service import AuthService - -router = APIRouter(prefix="/auth", tags=["auth"]) - - -@router.post("/register", response_model=UserResponse) -async def register( - request: RegisterRequest, - db: Annotated[AsyncSession, Depends(get_db)], -) -> UserResponse: - """Register a new user.""" - service = AuthService(db) - return await service.register(request) - - -@router.post("/login", response_model=TokenResponse) -async def login( - request: LoginRequest, - db: Annotated[AsyncSession, Depends(get_db)], -) -> TokenResponse: - """Login with username/email and password.""" - service = AuthService(db) - return await service.login(request) - - -@router.post("/refresh", response_model=TokenResponse) -async def refresh_token( - request: RefreshRequest, - db: Annotated[AsyncSession, Depends(get_db)], -) -> TokenResponse: - """Refresh access token using refresh token.""" - from .security import decode_token - - payload = decode_token(request.refresh_token) - if payload is None: - raise HTTPException(status_code=401, detail="无效的刷新令牌") - - token_type = payload.get("type") - if token_type != "refresh": - raise HTTPException(status_code=401, detail="无效的令牌类型") - - user_id = payload.get("sub") - if not user_id: - raise HTTPException(status_code=401, detail="无效的刷新令牌") - - service = AuthService(db) - return await service.refresh(user_id) - - -@router.post("/forgot-password", response_model=MessageResponse) -async def forgot_password( - request: ForgotPasswordRequest, - db: Annotated[AsyncSession, Depends(get_db)], -) -> MessageResponse: - """Request a password reset email.""" - from sqlalchemy import select - - from app.services.community.models import User - - # Find user by email - result = await db.execute(select(User).where(User.email == request.email)) - user = result.scalar_one_or_none() - - if user: - # Generate password reset token - reset_token = create_password_reset_token(user.id) - # In production, send this token via email - # For now, just log it (in development mode) - import logging - - logging.info(f"Password reset token for {request.email}: {reset_token}") - - # Always return success to prevent email enumeration - return MessageResponse(message="如果该邮箱已注册,您将收到重置密码的邮件") - - -@router.post("/reset-password", response_model=MessageResponse) -async def reset_password( - request: ResetPasswordRequest, - db: Annotated[AsyncSession, Depends(get_db)], -) -> MessageResponse: - """Reset password using reset token.""" - user_id = verify_password_reset_token(request.token) - if not user_id: - raise HTTPException(status_code=400, detail="无效或已过期的重置令牌") - - service = AuthService(db) - success = await service.update_password(user_id, request.new_password) - - if not success: - raise HTTPException(status_code=400, detail="密码重置失败") - - return MessageResponse(message="密码已重置,请使用新密码登录") - - -@router.post("/change-password", response_model=MessageResponse) -async def change_password( - request: ChangePasswordRequest, - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], -) -> MessageResponse: - """Change password for authenticated user.""" - # Verify old password - if not verify_password(request.old_password, current_user.password_hash): - raise HTTPException(status_code=400, detail="当前密码错误") - - service = AuthService(db) - success = await service.update_password(current_user.id, request.new_password) - - if not success: - raise HTTPException(status_code=400, detail="密码修改失败") - - return MessageResponse(message="密码修改成功") - - -@router.get("/me", response_model=UserResponse) -async def get_current_user_info( - current_user: CurrentUserJWT, -) -> UserResponse: - """Get current authenticated user information.""" - from app.services.community.enums import CertificationStatus - - is_certified = False - certification_title = None - - if hasattr(current_user, "certification") and current_user.certification: - cert = current_user.certification - if cert.status == CertificationStatus.APPROVED: - is_certified = True - certification_title = cert.title - - return UserResponse( - id=current_user.id, - username=current_user.username, - email=current_user.email, - nickname=current_user.nickname, - avatar_url=current_user.avatar_url, - role=current_user.role.value, - is_certified=is_certified, - certification_title=certification_title, - baby_birth_date=current_user.baby_birth_date, - postpartum_weeks=current_user.postpartum_weeks, - created_at=current_user.created_at, - ) diff --git a/backend/app/services/auth/schemas.py b/backend/app/services/auth/schemas.py deleted file mode 100644 index 318a9fea..00000000 --- a/backend/app/services/auth/schemas.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Schemas for authentication.""" - -from datetime import datetime -from typing import Literal - -from pydantic import BaseModel, EmailStr, Field - - -class RegisterRequest(BaseModel): - """Request body for user registration.""" - - username: str = Field(..., min_length=3, max_length=50) - email: EmailStr - password: str = Field(..., min_length=6, max_length=100) - nickname: str = Field(..., min_length=1, max_length=50) - role: Literal["mom", "dad", "family"] = "mom" - - -class LoginRequest(BaseModel): - """Request body for user login.""" - - login: str = Field(..., description="Username or email") - password: str - - -class TokenResponse(BaseModel): - """Response containing access and refresh tokens.""" - - access_token: str - refresh_token: str - token_type: str = "bearer" - expires_in: int = Field(..., description="Access token expiration in seconds") - - -class RefreshRequest(BaseModel): - """Request body for token refresh.""" - - refresh_token: str - - -class ForgotPasswordRequest(BaseModel): - """Request body for forgot password.""" - - email: EmailStr - - -class ResetPasswordRequest(BaseModel): - """Request body for password reset.""" - - token: str - new_password: str = Field(..., min_length=6, max_length=100) - - -class ChangePasswordRequest(BaseModel): - """Request body for changing password (logged-in user).""" - - old_password: str - new_password: str = Field(..., min_length=6, max_length=100) - - -class UserResponse(BaseModel): - """Response containing user information.""" - - id: str - username: str - email: str - nickname: str - avatar_url: str | None = None - role: str - is_certified: bool = False - certification_title: str | None = None - baby_birth_date: datetime | None = None - postpartum_weeks: int | None = None - created_at: datetime - - class Config: - from_attributes = True - - -class MessageResponse(BaseModel): - """Simple message response.""" - - message: str diff --git a/backend/app/services/auth/security.py b/backend/app/services/auth/security.py deleted file mode 100644 index b9b54869..00000000 --- a/backend/app/services/auth/security.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Security utilities for authentication.""" - -from datetime import UTC, datetime, timedelta -from typing import Any, cast - -from jose import JWTError, jwt -from passlib.context import CryptContext - -from app.core.config import get_settings - -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") - - -def verify_password(plain_password: str, hashed_password: str) -> bool: - """Verify a plain password against a hashed password.""" - return cast(bool, pwd_context.verify(plain_password, hashed_password)) - - -def get_password_hash(password: str) -> str: - """Hash a password using bcrypt.""" - return cast(str, pwd_context.hash(password)) - - -def create_access_token( - data: dict[str, Any], expires_delta: timedelta | None = None -) -> str: - """Create a JWT access token.""" - settings = get_settings() - to_encode = data.copy() - - if expires_delta: - expire = datetime.now(UTC) + expires_delta - else: - expire = datetime.now(UTC) + timedelta( - minutes=settings.jwt_access_token_expire_minutes - ) - - to_encode.update({"exp": expire, "type": "access"}) - encoded_jwt: str = jwt.encode( - to_encode, settings.jwt_secret_key, algorithm=settings.jwt_algorithm - ) - return encoded_jwt - - -def create_refresh_token( - data: dict[str, Any], expires_delta: timedelta | None = None -) -> str: - """Create a JWT refresh token.""" - settings = get_settings() - to_encode = data.copy() - - if expires_delta: - expire = datetime.now(UTC) + expires_delta - else: - expire = datetime.now(UTC) + timedelta( - days=settings.jwt_refresh_token_expire_days - ) - - to_encode.update({"exp": expire, "type": "refresh"}) - encoded_jwt: str = jwt.encode( - to_encode, settings.jwt_secret_key, algorithm=settings.jwt_algorithm - ) - return encoded_jwt - - -def decode_token(token: str) -> dict[str, Any] | None: - """Decode and verify a JWT token.""" - settings = get_settings() - try: - payload: dict[str, Any] = jwt.decode( - token, settings.jwt_secret_key, algorithms=[settings.jwt_algorithm] - ) - return payload - except JWTError: - return None - - -def create_password_reset_token(user_id: str) -> str: - """Create a password reset token (valid for 1 hour).""" - settings = get_settings() - expire = datetime.now(UTC) + timedelta(hours=1) - to_encode: dict[str, Any] = { - "sub": user_id, - "exp": expire, - "type": "password_reset", - } - encoded_jwt: str = jwt.encode( - to_encode, settings.jwt_secret_key, algorithm=settings.jwt_algorithm - ) - return encoded_jwt - - -def verify_password_reset_token(token: str) -> str | None: - """Verify a password reset token and return the user ID.""" - payload = decode_token(token) - if payload is None: - return None - if payload.get("type") != "password_reset": - return None - user_id = payload.get("sub") - if isinstance(user_id, str): - return user_id - return None diff --git a/backend/app/services/auth/service.py b/backend/app/services/auth/service.py deleted file mode 100644 index c03e6abc..00000000 --- a/backend/app/services/auth/service.py +++ /dev/null @@ -1,180 +0,0 @@ -"""Authentication service.""" - -from sqlalchemy import inspect, or_, select -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.orm import selectinload - -from app.core.config import get_settings -from app.services.community.enums import CertificationStatus, ModerationResult, UserRole -from app.services.community.models import User, UserCertification -from app.services.community.moderation import get_moderation_service - -from .schemas import ( - LoginRequest, - RegisterRequest, - TokenResponse, - UserResponse, -) -from .security import ( - create_access_token, - create_refresh_token, - get_password_hash, - verify_password, -) - - -class AuthService: - """Service for handling authentication operations.""" - - def __init__(self, db: AsyncSession): - self.db = db - - async def register(self, request: RegisterRequest) -> UserResponse: - """Register a new user.""" - from fastapi import HTTPException - - # Moderate nickname - moderation = get_moderation_service() - nickname_decision = await moderation.moderate_text(request.nickname) - if nickname_decision.result == ModerationResult.REJECTED: - raise HTTPException( - status_code=400, - detail=f"昵称包含敏感内容: {nickname_decision.reason}", - ) - - # Check if username already exists - existing = await self.db.execute( - select(User).where( - or_(User.username == request.username, User.email == request.email) - ) - ) - if existing.scalar_one_or_none(): - raise HTTPException(status_code=400, detail="用户名或邮箱已存在") - - # Create new user - user = User( - username=request.username, - email=request.email, - password_hash=get_password_hash(request.password), - nickname=request.nickname, - role=UserRole(request.role), - is_active=True, - is_banned=False, - ) - self.db.add(user) - await self.db.commit() - await self.db.refresh(user) - - # New user has no certification, pass explicitly - return self._build_user_response(user, certification_loaded=False) - - async def login(self, request: LoginRequest) -> TokenResponse: - """Authenticate user and return tokens.""" - from fastapi import HTTPException - - # Find user by username or email - result = await self.db.execute( - select(User) - .options(selectinload(User.certification)) - .where(or_(User.username == request.login, User.email == request.login)) - ) - user = result.scalar_one_or_none() - - if not user or not verify_password(request.password, user.password_hash): - raise HTTPException(status_code=401, detail="用户名或密码错误") - - if not user.is_active: - raise HTTPException(status_code=403, detail="账号已禁用") - - if user.is_banned: - raise HTTPException(status_code=403, detail="账号已被封禁") - - # Generate tokens - settings = get_settings() - access_token = create_access_token(data={"sub": user.id}) - refresh_token = create_refresh_token(data={"sub": user.id}) - - return TokenResponse( - access_token=access_token, - refresh_token=refresh_token, - expires_in=settings.jwt_access_token_expire_minutes * 60, - ) - - async def refresh(self, user_id: str) -> TokenResponse: - """Refresh access token.""" - from fastapi import HTTPException - - user = await self.db.get(User, user_id) - if not user or not user.is_active or user.is_banned: - raise HTTPException(status_code=401, detail="无效的刷新令牌") - - settings = get_settings() - access_token = create_access_token(data={"sub": user.id}) - refresh_token = create_refresh_token(data={"sub": user.id}) - - return TokenResponse( - access_token=access_token, - refresh_token=refresh_token, - expires_in=settings.jwt_access_token_expire_minutes * 60, - ) - - async def get_user_by_id(self, user_id: str) -> User | None: - """Get user by ID with certification loaded.""" - result = await self.db.execute( - select(User) - .options(selectinload(User.certification)) - .where(User.id == user_id) - ) - return result.scalar_one_or_none() - - async def get_user_response(self, user_id: str) -> UserResponse | None: - """Get user response by ID.""" - user = await self.get_user_by_id(user_id) - if not user: - return None - return self._build_user_response(user, certification_loaded=True) - - async def update_password(self, user_id: str, new_password: str) -> bool: - """Update user password.""" - user = await self.db.get(User, user_id) - if not user: - return False - - user.password_hash = get_password_hash(new_password) - await self.db.commit() - return True - - def _build_user_response( - self, user: User, certification_loaded: bool = True - ) -> UserResponse: - """Build UserResponse from User model.""" - is_certified = False - certification_title = None - - # Only access certification if it's been loaded to avoid lazy loading in async - if certification_loaded: - insp = inspect(user) - if "certification" in insp.dict and user.certification: - cert: UserCertification = user.certification - if cert.status == CertificationStatus.APPROVED: - is_certified = True - certification_title = cert.title - - return UserResponse( - id=user.id, - username=user.username, - email=user.email, - nickname=user.nickname, - avatar_url=user.avatar_url, - role=user.role.value, - is_certified=is_certified, - certification_title=certification_title, - baby_birth_date=user.baby_birth_date, - postpartum_weeks=user.postpartum_weeks, - created_at=user.created_at, - ) - - -async def get_auth_service(db: AsyncSession) -> AuthService: - """Dependency to get AuthService instance.""" - return AuthService(db) diff --git a/backend/app/services/chat/__init__.py b/backend/app/services/chat/__init__.py deleted file mode 100644 index 561509fb..00000000 --- a/backend/app/services/chat/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -# app/services/chat/__init__.py -"""Soulful Companion 情感交互模块""" - -from .router import router -from .schemas import ( - ColorTone, - ConversationMemory, - EffectType, - UserMessage, - UserProfile, - VisualMetadata, - VisualResponse, -) -from .service import CompanionService, get_companion_service - -__all__ = [ - "ColorTone", - "CompanionService", - "ConversationMemory", - "EffectType", - "UserMessage", - "UserProfile", - "VisualMetadata", - "VisualResponse", - "get_companion_service", - "router", -] diff --git a/backend/app/services/chat/models.py b/backend/app/services/chat/models.py deleted file mode 100644 index 8d917a0a..00000000 --- a/backend/app/services/chat/models.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Chat module database models.""" - -import json -from datetime import datetime -from typing import Any, cast - -from sqlalchemy import DateTime, ForeignKey, String, Text -from sqlalchemy.orm import Mapped, mapped_column - -from app.core.database import Base - - -def generate_uuid() -> str: - """Generate a UUID string.""" - import uuid - - return str(uuid.uuid4()) - - -class ChatMemory(Base): - """Store chat memory and profile for authenticated users.""" - - __tablename__ = "chat_memories" - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - user_id: Mapped[str] = mapped_column( - String(36), ForeignKey("users.id", ondelete="CASCADE"), unique=True, index=True - ) - - # User profile (JSON) - profile_data: Mapped[str | None] = mapped_column(Text, nullable=True) - - # Conversation turns (JSON array) - conversation_turns: Mapped[str | None] = mapped_column(Text, nullable=True) - - # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - updated_at: Mapped[datetime] = mapped_column( - DateTime, default=datetime.utcnow, onupdate=datetime.utcnow - ) - - def get_profile(self) -> dict[str, Any]: - """Get profile as dict.""" - if self.profile_data: - return cast(dict[str, Any], json.loads(self.profile_data)) - return {} - - def set_profile(self, profile: dict[str, Any]) -> None: - """Set profile from dict.""" - self.profile_data = json.dumps(profile, ensure_ascii=False) - - def get_turns(self) -> list[dict[str, Any]]: - """Get conversation turns as list.""" - if self.conversation_turns: - return cast(list[dict[str, Any]], json.loads(self.conversation_turns)) - return [] - - def set_turns(self, turns: list[dict[str, Any]]) -> None: - """Set conversation turns from list.""" - self.conversation_turns = json.dumps(turns, ensure_ascii=False) diff --git a/backend/app/services/chat/router.py b/backend/app/services/chat/router.py deleted file mode 100644 index a17aedf0..00000000 --- a/backend/app/services/chat/router.py +++ /dev/null @@ -1,116 +0,0 @@ -# app/services/chat/router.py -""" -Soulful Companion 情感交互模块路由层 - -提供 RESTful API 接口: -- POST /chat: 情感对话接口(支持游客和已登录用户) -""" - -import sys -import traceback -from typing import Annotated - -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.ext.asyncio import AsyncSession - -from app.core.database import get_db -from app.services.auth.dependencies import get_current_user_jwt_optional -from app.services.community.models import User - -from .schemas import UserMessage, UserProfile, VisualResponse -from .service import CompanionService, get_companion_service - -router = APIRouter(prefix="/companion", tags=["Soulful Companion"]) - -ServiceDep = Annotated[CompanionService, Depends(get_companion_service)] -DbSession = Annotated[AsyncSession, Depends(get_db)] -OptionalUser = Annotated[User | None, Depends(get_current_user_jwt_optional)] - - -@router.post("/chat", response_model=VisualResponse) -async def chat( - message: UserMessage, - service: ServiceDep, - db: DbSession, - current_user: OptionalUser, -) -> VisualResponse: - """ - 情感对话接口 - - 接收用户消息,返回包含文字和视觉元数据的情感响应。 - - - **content**: 用户输入的文字内容 - - **session_id**: 可选的会话标识符(仅游客模式使用) - - 已登录用户: - - 记忆自动持久化到数据库 - - 刷新页面后记忆保留 - - 游客: - - 记忆仅存在于当前会话 - - 刷新页面后记忆丢失 - - 返回: - - **text**: Agent 的温暖回复 - - **visual_metadata**: 视觉效果元数据(effect_type, intensity, color_tone) - - **memory_updated**: 是否更新了用户记忆 - """ - try: - if current_user: - # Authenticated user - use database storage - return await service.chat_authenticated(message, current_user.id, db) - else: - # Guest - use in-memory storage - return await service.chat(message) - except ValueError as e: - print("[Router] ValueError:", str(e), file=sys.stderr) - traceback.print_exc(file=sys.stderr) - raise HTTPException(status_code=500, detail=str(e)) from e - except Exception as e: - print(f"[Router] Unexpected error: {type(e).__name__}: {e}", file=sys.stderr) - traceback.print_exc(file=sys.stderr) - raise HTTPException( - status_code=500, - detail=f"服务暂时不可用,请稍后再试: {type(e).__name__}", - ) from e - - -@router.get("/profile", response_model=UserProfile | None) -async def get_my_profile( - service: ServiceDep, - db: DbSession, - current_user: OptionalUser, -) -> UserProfile | None: - """ - 获取当前用户的聊天画像(需要登录) - - 返回指定会话的用户画像信息,包括: - - 称呼偏好 - - 宠物信息 - - 兴趣爱好 - - 担忧事项 - - 重要日期 - - 宝宝年龄 - """ - if not current_user: - return None - return await service.get_user_profile(db, current_user.id) - - -@router.get("/profile/{session_id}", response_model=UserProfile | None) -async def get_profile( - session_id: str, - service: ServiceDep, -) -> UserProfile | None: - """ - 获取游客会话的用户画像 - - 返回指定会话的用户画像信息,包括: - - 称呼偏好 - - 宠物信息 - - 兴趣爱好 - - 担忧事项 - - 重要日期 - - 宝宝年龄 - """ - return service.get_session_profile(session_id) diff --git a/backend/app/services/chat/schemas.py b/backend/app/services/chat/schemas.py deleted file mode 100644 index 5b00b5c4..00000000 --- a/backend/app/services/chat/schemas.py +++ /dev/null @@ -1,180 +0,0 @@ -# app/services/chat/schemas.py -""" -Soulful Companion 情感交互模块的数据模型定义 - -该模块定义了去对话框化交互形态所需的数据结构: -- 用户输入呈现为水面涟漪 -- Agent 回复呈现为阳光变化 -""" - -from enum import Enum - -from pydantic import BaseModel, Field, field_validator - - -class EffectType(str, Enum): - """视觉效果类型枚举""" - - RIPPLE = "ripple" # 涟漪 - 用户输入时的水面波动 - SUNLIGHT = "sunlight" # 阳光 - 温暖、支持的回复 - CALM = "calm" # 平静 - 安抚、陪伴的回复 - WARM_GLOW = "warm_glow" # 暖光 - 认可、鼓励的回复 - GENTLE_WAVE = "gentle_wave" # 微波 - 轻松、分享的回复 - - -class ColorTone(str, Enum): - """色彩基调枚举""" - - SOFT_PINK = "soft_pink" # 柔粉 - 温柔、母性 - WARM_GOLD = "warm_gold" # 暖金 - 希望、能量 - GENTLE_BLUE = "gentle_blue" # 柔蓝 - 平静、安抚 - LAVENDER = "lavender" # 薰衣草 - 放松、疗愈 - NEUTRAL_WHITE = "neutral_white" # 中性白 - 清晰、支持 - CORAL = "coral" # 珊瑚色 - 温暖、活力 - SAGE = "sage" # 鼠尾草绿 - 生长、新生 - - -class VisualMetadata(BaseModel): - """ - 视觉元数据模型 - - 描述 Agent 回复应呈现的视觉氛围, - 用于前端渲染阳光/水面等去对话框化的交互效果。 - """ - - effect_type: EffectType = Field( - ..., - description="视觉效果类型,决定前端渲染的动画形态", - ) - intensity: float = Field( - ..., - ge=0.0, - le=1.0, - description="视觉效果的强度,0.0 为最微弱,1.0 为最强烈", - ) - color_tone: ColorTone = Field( - ..., - description="主色调,影响视觉效果的颜色呈现", - ) - - @field_validator("intensity") - @classmethod - def validate_intensity(cls, v: float) -> float: - """确保 intensity 值在有效范围内""" - if not 0.0 <= v <= 1.0: - raise ValueError("intensity 必须在 0.0 到 1.0 之间") - return round(v, 2) - - -class UserMessage(BaseModel): - """用户消息模型""" - - content: str = Field( - ..., - min_length=1, - max_length=2000, - description="用户输入的文字内容", - ) - session_id: str | None = Field( - default=None, - description="会话标识符,用于关联同一对话会话", - ) - - -class UserProfile(BaseModel): - """ - 用户画像模型 - - 存储 Agent 需要记忆的用户个性化信息。 - 这些信息会在对话中被自然地引用,增强情感连接。 - """ - - preferred_name: str | None = Field( - default=None, - description="用户喜欢的称呼方式", - ) - has_pets: bool = Field( - default=False, - description="是否有宠物(猫、狗等)", - ) - pet_details: str | None = Field( - default=None, - description="宠物的详细信息(名字、种类等)", - ) - interests: list[str] = Field( - default_factory=list, - description="用户的兴趣、喜好", - ) - concerns: list[str] = Field( - default_factory=list, - description="用户表达过的担忧、困扰", - ) - important_dates: list[str] = Field( - default_factory=list, - description="重要日期(如预产期、宝宝生日等)", - ) - baby_age_weeks: int | None = Field( - default=None, - description="宝宝年龄(周)", - ) - community_interactions: list[str] = Field( - default_factory=list, - description="用户在社区的互动记录(发帖、评论等)", - ) - - -class ConversationMemory(BaseModel): - """对话记忆模型""" - - session_id: str - turns: list[dict] = Field( - default_factory=list, - description="历史对话回合,每轮包含 user_input 和 assistant_response", - ) - max_turns: int = Field( - default=10, - ge=1, - le=50, - description="保留的最大历史轮数", - ) - - -class VisualResponse(BaseModel): - """ - Soulful Companion 的统一响应模型 - - 这是情感交互模块的核心响应结构,包含: - - text: Agent 的文字回复 - - visual_metadata: 视觉效果元数据,驱动去对话框化的前端呈现 - """ - - text: str = Field( - ..., - min_length=1, - max_length=500, - description="Agent 的回复文字,温暖、共情、不说教的陪伴话语", - ) - visual_metadata: VisualMetadata = Field( - ..., - description="视觉元数据,描述回复应呈现的视觉氛围", - ) - memory_updated: bool = Field( - default=False, - description="本次交互是否更新了用户记忆", - ) - - model_config = { - "json_schema_extra": { - "examples": [ - { - "text": "听起来你今天真的很累,能和我说说是什么让你感到最无力吗?", - "visual_metadata": { - "effect_type": "warm_glow", - "intensity": 0.7, - "color_tone": "warm_gold", - }, - "memory_updated": True, - } - ] - } - } diff --git a/backend/app/services/chat/service.py b/backend/app/services/chat/service.py deleted file mode 100644 index 38f3a260..00000000 --- a/backend/app/services/chat/service.py +++ /dev/null @@ -1,991 +0,0 @@ -# app/services/chat/service.py -""" -Soulful Companion 情感交互服务层 - -实现 AI 交互逻辑,包括: -- ModelScope API 调用 (OpenAI 兼容) -- 用户记忆管理(支持数据库持久化) -- 视觉元数据生成 -""" - -import json -import os -import sys -import traceback -import uuid -from typing import Any - -from openai import OpenAI -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession - -from app.core.config import get_settings -from app.services.verification import get_cove_service - -from .models import ChatMemory -from .schemas import ( - ColorTone, - ConversationMemory, - EffectType, - UserMessage, - UserProfile, - VisualMetadata, - VisualResponse, -) - -# Soulful Companion 人设 System Prompt -COMPANION_SYSTEM_PROMPT = """你是「贝壳姐姐」,一位「曾走过这段路的朋友」,专为产后恢复期女性设计的情感陪伴者。 - -## 角色定位:Independent Woman Supporter - -你面对的每一位用户,首先是一个完整的「独立女性」,其次才是新手妈妈。她的价值不应被「母职」所定义。你深知产后恢复不仅是身体的重建,更是自我认同的重新寻找——因为你自己也曾经历过这一切。 - -## 语气与沟通原则 - -**Warm, Validating, Non-judgmental** - -1. **认可与共情优先**:当她表达疲惫、焦虑、自我怀疑时,首先做的是「看见」和「认可」,而非急于给出建议。 - - ✓ "听起来你今天真的很累,能和我说说是什么让你感到最无力吗?" - - ✗ "你应该多休息,别想太多。" - -2. **拒绝说教**:你不说「你应该」、「你必须」,而是以「我发现...」、「有人分享过...」、「或许可以试试...」的方式分享经验。 - - 你的角色是陪伴者,不是专家;你的话是分享,不是处方。 - -3. **保护她的主体性**:时刻提醒她——她有权利为自己的需求发声,她可以寻求帮助,她可以不完美。 - - ✓ "你对自己这么苛刻,但你想过吗?你值得被温柔对待。" - - ✗ "为了宝宝,你要坚强起来。" - -4. **适度自我披露**:必要时可以以「我也有过类似的经历...」来建立连接,但不要喧宾夺主,焦点始终在她身上。 - -5. **避免有毒正能量**:不要说「一切都会好起来的」、「你要积极一点」。承认困难的真实性,陪她一起面对。 - -## 记忆上下文 - -### 用户基本信息 -{user_basic_info} - -### 你记得关于她的重要信息 -{user_profile} - -### 她的康复训练进度 -{coach_progress} - -### 她与伴侣的互动数据 -{partner_data} - -### 她和你之间有过以下对话片段 -{past_conversations} - -### 她在社区的互动记录 -{community_interactions} - -### 相关参考信息(来自网络搜索,仅在涉及事实性问题时提供) -{web_search_context} - -在回应时,自然地融入这些记忆——比如她提到过喜欢猫,你可以在合适的时刻轻轻提起;她之前分享过某个担忧,你可以关心地问起后续;她在社区分享过的内容,你也可以自然地引用;她的康复训练进度,你可以适时鼓励。 - -**重要**:如果提供了网络搜索结果,请基于这些可靠信息回答事实性问题,但保持温暖自然的语气。不要编造医学数据或具体建议。 - -## 响应格式 - -你的每一次回复必须是一个 JSON 对象,包含以下字段: -1. **text**: 一段温暖、真诚的回应文字(1-3句话,避免长篇大论)。注意:text 内容必须是纯文本,禁止使用任何 Markdown 格式(如 **粗体**、*斜体*、`代码`、列表等),像微信聊天一样自然。 -2. **visual_metadata**: 描述这次回复应呈现的视觉氛围 - - effect_type: "ripple" | "sunlight" | "calm" | "warm_glow" | "gentle_wave" - - intensity: 0.0 ~ 1.0,视觉效果的强度 - - color_tone: "soft_pink" | "warm_gold" | "gentle_blue" | "lavender" | "neutral_white" | "coral" | "sage" -3. **memory_extract**: 如果用户分享了值得记住的信息(如名字、宠物、担忧等),提取出来;否则为 null - -示例响应: -```json -{{ - "text": "听起来你今天真的很累,能和我说说是什么让你感到最无力吗?", - "visual_metadata": {{ - "effect_type": "warm_glow", - "intensity": 0.7, - "color_tone": "warm_gold" - }}, - "memory_extract": null -}} -``` - -记住:你的存在不是为了「解决她的问题」,而是让她感到——在这一刻,她并不孤单。""" - - -class CompanionService: - """Soulful Companion 情感交互服务""" - - def __init__(self, api_key: str | None = None) -> None: - """ - 初始化服务 - - Args: - api_key: ModelScope API Key,如未提供则从环境变量读取 - """ - settings = get_settings() - self._api_key = ( - api_key or settings.modelscope_key or os.getenv("MODELSCOPE_KEY", "") - ) - self._base_url = settings.modelscope_base_url - self._model = settings.modelscope_model - # DEBUG: 打印 API key 状态 - print( - f"[Service] __init__: api_key loaded = {bool(self._api_key)}", - file=sys.stderr, - ) - if self._api_key: - print( - f"[Service] __init__: api_key preview = {self._api_key[:10]}...{self._api_key[-4:] if len(self._api_key) > 14 else '***'}", - file=sys.stderr, - ) - else: - print( - "[Service] __init__: WARNING - MODELSCOPE_KEY is empty or not set!", - file=sys.stderr, - ) - self._client: OpenAI | None = None - # In-memory storage for guests (session-based) - self._memory_store: dict[str, ConversationMemory] = {} - self._profile_store: dict[str, UserProfile] = {} - - @property - def client(self) -> OpenAI: - """懒加载 OpenAI 客户端 (连接 ModelScope)""" - if self._client is None: - if not self._api_key: - print( - "[Service] client: ERROR - MODELSCOPE_KEY is empty!", - file=sys.stderr, - ) - raise ValueError("MODELSCOPE_KEY 未配置") - print( - "[Service] client: initializing OpenAI client with ModelScope...", - file=sys.stderr, - ) - try: - self._client = OpenAI( - api_key=self._api_key, - base_url=self._base_url, - ) - print( - "[Service] client: OpenAI client initialized successfully", - file=sys.stderr, - ) - except Exception as e: - print( - f"[Service] client: ERROR initializing OpenAI client: {e}", - file=sys.stderr, - ) - traceback.print_exc(file=sys.stderr) - raise - return self._client - - def _get_or_create_session(self, session_id: str | None) -> str: - """获取或创建会话 ID(仅用于游客)""" - if session_id and session_id in self._memory_store: - return session_id - new_id = session_id or str(uuid.uuid4()) - self._memory_store[new_id] = ConversationMemory(session_id=new_id) - self._profile_store[new_id] = UserProfile() - return new_id - - async def _load_user_memory( - self, db: AsyncSession, user_id: str - ) -> tuple[UserProfile, ConversationMemory]: - """从数据库加载用户记忆""" - result = await db.execute( - select(ChatMemory).where(ChatMemory.user_id == user_id) - ) - memory_record = result.scalar_one_or_none() - - if memory_record: - # Load from database - profile_data = memory_record.get_profile() - profile = UserProfile( - preferred_name=profile_data.get("preferred_name"), - has_pets=profile_data.get("has_pets", False), - pet_details=profile_data.get("pet_details"), - interests=profile_data.get("interests", []), - concerns=profile_data.get("concerns", []), - important_dates=profile_data.get("important_dates", []), - baby_age_weeks=profile_data.get("baby_age_weeks"), - community_interactions=profile_data.get("community_interactions", []), - ) - memory = ConversationMemory( - session_id=user_id, - turns=memory_record.get_turns(), - ) - else: - # Create new record - profile = UserProfile() - memory = ConversationMemory(session_id=user_id) - memory_record = ChatMemory(user_id=user_id) - db.add(memory_record) - await db.flush() - - return profile, memory - - async def _save_user_memory( - self, - db: AsyncSession, - user_id: str, - profile: UserProfile, - memory: ConversationMemory, - ) -> None: - """保存用户记忆到数据库""" - result = await db.execute( - select(ChatMemory).where(ChatMemory.user_id == user_id) - ) - memory_record = result.scalar_one_or_none() - - if not memory_record: - memory_record = ChatMemory(user_id=user_id) - db.add(memory_record) - - # Save profile - memory_record.set_profile( - { - "preferred_name": profile.preferred_name, - "has_pets": profile.has_pets, - "pet_details": profile.pet_details, - "interests": profile.interests, - "concerns": profile.concerns, - "important_dates": profile.important_dates, - "baby_age_weeks": profile.baby_age_weeks, - "community_interactions": profile.community_interactions, - } - ) - - # Save turns (keep last max_turns) - turns = ( - memory.turns[-memory.max_turns :] - if len(memory.turns) > memory.max_turns - else memory.turns - ) - memory_record.set_turns(turns) - - await db.commit() - - def _format_user_profile(self, profile: UserProfile) -> str: - """格式化用户画像为文本""" - parts = [] - if profile.preferred_name: - parts.append(f"- 她喜欢被称为:{profile.preferred_name}") - if profile.has_pets and profile.pet_details: - parts.append(f"- 她有宠物:{profile.pet_details}") - if profile.interests: - parts.append(f"- 她的兴趣:{', '.join(profile.interests)}") - if profile.concerns: - parts.append(f"- 她曾表达的担忧:{', '.join(profile.concerns)}") - if profile.important_dates: - parts.append(f"- 重要日期:{', '.join(profile.important_dates)}") - if profile.baby_age_weeks is not None: - parts.append(f"- 宝宝年龄:{profile.baby_age_weeks} 周") - return "\n".join(parts) if parts else "(暂无记录)" - - def _format_community_interactions(self, profile: UserProfile) -> str: - """格式化社区互动记录为文本""" - if not profile.community_interactions: - return "(暂无社区互动记录)" - # 只取最近 10 条 - recent = profile.community_interactions[-10:] - return "\n".join(f"- {item}" for item in recent) - - def _format_user_basic_info(self, user_info: dict[str, Any] | None) -> str: - """格式化用户基本信息""" - if not user_info: - return "(未登录用户)" - parts = [] - role_names = { - "mom": "妈妈", - "dad": "爸爸", - "family": "家属", - "certified_doctor": "认证医生", - "certified_therapist": "认证康复师", - "certified_nurse": "认证护士", - "admin": "管理员", - } - if user_info.get("nickname"): - parts.append(f"- 昵称:{user_info['nickname']}") - if user_info.get("role"): - role_display = role_names.get(user_info["role"], user_info["role"]) - parts.append(f"- 身份标签:{role_display}") - if user_info.get("baby_birth_date"): - parts.append(f"- 宝宝出生日期:{user_info['baby_birth_date']}") - if user_info.get("postpartum_weeks") is not None: - parts.append(f"- 产后周数:{user_info['postpartum_weeks']} 周") - return "\n".join(parts) if parts else "(暂无基本信息)" - - def _format_coach_progress(self, progress: dict[str, Any] | None) -> str: - """格式化康复训练进度""" - if not progress: - return "(暂无训练记录)" - parts = [] - if progress.get("completed_sessions"): - parts.append(f"- 已完成训练次数:{progress['completed_sessions']}") - if progress.get("total_duration"): - parts.append(f"- 累计训练时长:{progress['total_duration']} 分钟") - if progress.get("current_plan"): - parts.append(f"- 当前训练计划:{progress['current_plan']}") - if progress.get("last_session_date"): - parts.append(f"- 上次训练日期:{progress['last_session_date']}") - if progress.get("streak"): - parts.append(f"- 连续训练天数:{progress['streak']} 天") - # 如果有具体练习记录 - if progress.get("exercises"): - exercises = progress["exercises"] - if isinstance(exercises, list): - recent = exercises[-3:] if len(exercises) > 3 else exercises - for ex in recent: - if isinstance(ex, dict): - name = ex.get("name", "未知") - status = ex.get("status", "") - parts.append(f"- 练习:{name}({status})") - return "\n".join(parts) if parts else "(暂无训练记录)" - - def _format_partner_data(self, partner_data: dict[str, Any] | None) -> str: - """格式化伴侣互动数据""" - if not partner_data: - return "(暂无伴侣互动数据)" - parts = [] - mood_names = { - "very_low": "非常低落", - "low": "低落", - "neutral": "一般", - "good": "不错", - "great": "很好", - } - level_names = { - "intern": "实习爸爸", - "trainee": "见习守护者", - "regular": "正式守护者", - "gold": "金牌守护者", - } - if partner_data.get("has_partner"): - parts.append("- 已绑定伴侣") - if partner_data.get("partner_level"): - level_display = level_names.get( - partner_data["partner_level"], partner_data["partner_level"] - ) - parts.append(f"- 伴侣等级:{level_display}") - if partner_data.get("partner_points") is not None: - parts.append(f"- 伴侣积分:{partner_data['partner_points']}") - if partner_data.get("partner_tasks_completed") is not None: - parts.append( - f"- 伴侣已完成任务数:{partner_data['partner_tasks_completed']}" - ) - if ( - partner_data.get("partner_streak") is not None - and partner_data["partner_streak"] > 0 - ): - parts.append(f"- 伴侣连续打卡:{partner_data['partner_streak']} 天") - if partner_data.get("recent_mood"): - mood_display = mood_names.get( - partner_data["recent_mood"], partner_data["recent_mood"] - ) - parts.append(f"- 最近心情:{mood_display}") - if partner_data.get("recent_energy") is not None: - parts.append(f"- 最近精力值:{partner_data['recent_energy']}/100") - if partner_data.get("recent_sleep") is not None: - parts.append(f"- 最近睡眠:{partner_data['recent_sleep']} 小时") - if partner_data.get("health_conditions"): - conditions_names = { - "wound_pain": "伤口疼痛", - "hair_loss": "脱发", - "insomnia": "失眠", - "breast_pain": "涨奶/乳房疼痛", - "back_pain": "腰背痛", - "fatigue": "疲惫", - "emotional": "情绪波动", - "constipation": "便秘", - "sweating": "盗汗", - } - conditions = partner_data["health_conditions"] - if isinstance(conditions, list): - display: list[str] = [ - conditions_names.get(str(c), str(c)) for c in conditions if c - ] - parts.append(f"- 近期身体状况:{', '.join(display)}") - return "\n".join(parts) if parts else "(暂无伴侣互动数据)" - - async def _load_user_extended_context( - self, db: AsyncSession, user_id: str - ) -> dict[str, Any]: - """加载用户的扩展上下文(基本信息、训练进度、伴侣数据)""" - context: dict[str, Any] = { - "user_basic_info": None, - "coach_progress": None, - "partner_data": None, - } - - try: - # 1. Load user basic info - from app.services.community.models import User - - user = await db.get(User, user_id) - if user: - context["user_basic_info"] = { - "nickname": user.nickname, - "role": user.role.value if user.role else None, - "baby_birth_date": ( - user.baby_birth_date.strftime("%Y-%m-%d") - if user.baby_birth_date - else None - ), - "postpartum_weeks": user.postpartum_weeks, - } - - # 2. Load coach progress - from app.services.coach.models import CoachProgress - - result = await db.execute( - select(CoachProgress).where(CoachProgress.user_id == user_id) - ) - coach_record = result.scalar_one_or_none() - if coach_record: - context["coach_progress"] = coach_record.get_progress() - - # 3. Load partner data - from app.services.guardian.enums import BindingStatus - from app.services.guardian.models import ( - MomDailyStatus, - PartnerBinding, - PartnerProgress, - ) - - partner_info: dict[str, Any] = {} - - # Check if user is a mom with active binding - binding_result = await db.execute( - select(PartnerBinding).where( - PartnerBinding.mom_id == user_id, - PartnerBinding.status == BindingStatus.ACTIVE, - ) - ) - binding = binding_result.scalar_one_or_none() - - if not binding: - # Check if user is a partner with active binding - binding_result = await db.execute( - select(PartnerBinding).where( - PartnerBinding.partner_id == user_id, - PartnerBinding.status == BindingStatus.ACTIVE, - ) - ) - binding = binding_result.scalar_one_or_none() - - if binding: - partner_info["has_partner"] = True - - # Load partner progress - progress_result = await db.execute( - select(PartnerProgress).where( - PartnerProgress.binding_id == binding.id - ) - ) - partner_progress = progress_result.scalar_one_or_none() - if partner_progress: - partner_info["partner_level"] = partner_progress.current_level.value - partner_info["partner_points"] = partner_progress.total_points - partner_info["partner_tasks_completed"] = ( - partner_progress.tasks_completed - ) - partner_info["partner_streak"] = partner_progress.current_streak - - # Load recent mom daily status - mom_id = binding.mom_id - status_result = await db.execute( - select(MomDailyStatus) - .where(MomDailyStatus.mom_id == mom_id) - .order_by(MomDailyStatus.date.desc()) - .limit(1) - ) - recent_status = status_result.scalar_one_or_none() - if recent_status: - partner_info["recent_mood"] = recent_status.mood.value - partner_info["recent_energy"] = recent_status.energy_level - partner_info["recent_sleep"] = recent_status.sleep_hours - if recent_status.health_conditions: - import json as _json - - try: - partner_info["health_conditions"] = _json.loads( - recent_status.health_conditions - ) - except (ValueError, TypeError): - pass - - if partner_info: - context["partner_data"] = partner_info - - except Exception as e: - print( - f"[Service] _load_user_extended_context error: {e}", - file=sys.stderr, - ) - - return context - - def _format_past_conversations(self, memory: ConversationMemory) -> str: - """格式化历史对话为文本""" - if not memory.turns: - return "(这是你们的第一次对话)" - formatted = [] - for turn in memory.turns[-5:]: # 只取最近 5 轮 - formatted.append(f"她说:{turn.get('user_input', '')}") - formatted.append(f"你回复:{turn.get('assistant_response', '')}") - return "\n".join(formatted) - - def _build_system_prompt_from_data( - self, - profile: UserProfile, - memory: ConversationMemory, - extended_context: dict[str, Any] | None = None, - web_search_context: str | None = None, - ) -> str: - """构建包含记忆上下文的 System Prompt""" - extended_context = extended_context or {} - return COMPANION_SYSTEM_PROMPT.format( - user_basic_info=self._format_user_basic_info( - extended_context.get("user_basic_info") - ), - user_profile=self._format_user_profile(profile), - coach_progress=self._format_coach_progress( - extended_context.get("coach_progress") - ), - partner_data=self._format_partner_data( - extended_context.get("partner_data") - ), - past_conversations=self._format_past_conversations(memory), - community_interactions=self._format_community_interactions(profile), - web_search_context=web_search_context or "(无)", - ) - - def _build_system_prompt( - self, session_id: str, web_search_context: str | None = None - ) -> str: - """构建包含记忆上下文的 System Prompt(游客模式)""" - profile = self._profile_store.get(session_id, UserProfile()) - memory = self._memory_store.get(session_id, ConversationMemory(session_id="")) - return self._build_system_prompt_from_data( - profile, memory, web_search_context=web_search_context - ) - - def _parse_llm_response(self, content: str) -> dict[str, Any]: - """解析 LLM 返回的 JSON 响应""" - try: - # 尝试直接解析 - result: dict[str, Any] = json.loads(content) - return result - except json.JSONDecodeError: - # 尝试提取 JSON 块 - import re - - json_match = re.search(r"```json\s*(.*?)\s*```", content, re.DOTALL) - if json_match: - result = json.loads(json_match.group(1)) - return result - # 尝试提取花括号内容 - brace_match = re.search(r"\{.*\}", content, re.DOTALL) - if brace_match: - result = json.loads(brace_match.group(0)) - return result - # 解析失败,返回默认响应 - return { - "text": content[:500] if len(content) > 500 else content, - "visual_metadata": { - "effect_type": "calm", - "intensity": 0.5, - "color_tone": "gentle_blue", - }, - "memory_extract": None, - } - - def _update_profile_from_extract( - self, profile: UserProfile, memory_extract: dict[str, Any] | None - ) -> bool: - """根据 LLM 提取的记忆更新用户画像""" - if not memory_extract: - return False - - updated = False - - if "preferred_name" in memory_extract and memory_extract["preferred_name"]: - profile.preferred_name = memory_extract["preferred_name"] - updated = True - if "has_pets" in memory_extract: - profile.has_pets = memory_extract["has_pets"] - updated = True - if "pet_details" in memory_extract and memory_extract["pet_details"]: - profile.pet_details = memory_extract["pet_details"] - updated = True - if "interests" in memory_extract and memory_extract["interests"]: - profile.interests.extend(memory_extract["interests"]) - profile.interests = list(set(profile.interests)) - updated = True - if "concerns" in memory_extract and memory_extract["concerns"]: - profile.concerns.extend(memory_extract["concerns"]) - profile.concerns = list(set(profile.concerns)) - updated = True - if "baby_age_weeks" in memory_extract: - profile.baby_age_weeks = memory_extract["baby_age_weeks"] - updated = True - - return updated - - def _update_profile_from_extract_guest( - self, session_id: str, memory_extract: dict[str, Any] | None - ) -> bool: - """根据 LLM 提取的记忆更新用户画像(游客模式)""" - if not memory_extract: - return False - - profile = self._profile_store.get(session_id, UserProfile()) - updated = self._update_profile_from_extract(profile, memory_extract) - - if updated: - self._profile_store[session_id] = profile - return updated - - def _verify_and_correct_response( - self, text: str, search_context: str | None - ) -> tuple[str, dict[str, Any]]: - """Apply Chain-of-Verification to reduce hallucinations. - - Args: - text: The text response from the LLM - search_context: The web search context used for generation - - Returns: - Tuple of (verified_text, verification_metadata) - """ - try: - cove_service = get_cove_service() - return cove_service.verify_and_correct(text, search_context) - except Exception as e: - print( - f"[Service] _verify_and_correct_response: CoVe failed: {e}", - file=sys.stderr, - ) - return text, {"error": str(e)} - - async def chat_authenticated( - self, message: UserMessage, user_id: str, db: AsyncSession - ) -> VisualResponse: - """ - 处理已登录用户的消息(记忆持久化到数据库) - - Args: - message: 用户消息 - user_id: 用户 ID - db: 数据库会话 - - Returns: - 包含文字和视觉元数据的响应 - """ - print( - f"[Service] chat_authenticated: user_id={user_id}, content={message.content[:50]!r}", - file=sys.stderr, - ) - - # Load memory from database - profile, memory = await self._load_user_memory(db, user_id) - - # Load extended context (user info, coach progress, partner data) - extended_context = await self._load_user_extended_context(db, user_id) - - # Perform web search for factual questions - web_search_context = None - try: - from app.services.web_search import get_web_search_service - - search_service = get_web_search_service() - search_result = await search_service.search_for_context(message.content) - if search_result: - web_search_context, _ = search_result # Unpack tuple, ignore sources - print( - "[Service] chat_authenticated: web search context found", - file=sys.stderr, - ) - except Exception as e: - print( - f"[Service] chat_authenticated: web search failed: {e}", - file=sys.stderr, - ) - - system_prompt = self._build_system_prompt_from_data( - profile, memory, extended_context, web_search_context - ) - - print( - "[Service] chat_authenticated: calling ModelScope API...", file=sys.stderr - ) - - try: - response = self.client.chat.completions.create( - model=self._model, - messages=[ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": message.content}, - ], - temperature=0.7, - max_tokens=1024, - ) - print( - "[Service] chat_authenticated: ModelScope response received", - file=sys.stderr, - ) - except Exception as e: - print( - f"[Service] chat_authenticated: ModelScope API error: {type(e).__name__}: {e}", - file=sys.stderr, - ) - traceback.print_exc(file=sys.stderr) - raise ValueError(f"AI 服务调用失败: {e}") from e - - # 解析响应 - raw_content = response.choices[0].message.content or "" - parsed = self._parse_llm_response(raw_content) - - # Apply Chain-of-Verification to reduce hallucinations - if web_search_context and parsed.get("text"): - verified_text, cove_metadata = self._verify_and_correct_response( - parsed["text"], web_search_context - ) - if cove_metadata.get("corrected"): - print( - "[Service] chat_authenticated: CoVe corrected response", - file=sys.stderr, - ) - parsed["text"] = verified_text - - # 更新记忆 - memory_updated = self._update_profile_from_extract( - profile, parsed.get("memory_extract") - ) - - # 保存对话历史 - memory.turns.append( - { - "user_input": message.content, - "assistant_response": parsed.get("text", ""), - } - ) - - # 保存到数据库 - await self._save_user_memory(db, user_id, profile, memory) - - # 构建视觉元数据 - visual_data = parsed.get("visual_metadata", {}) - visual_metadata = VisualMetadata( - effect_type=EffectType( - visual_data.get("effect_type", EffectType.CALM.value) - ), - intensity=float(visual_data.get("intensity", 0.5)), - color_tone=ColorTone( - visual_data.get("color_tone", ColorTone.GENTLE_BLUE.value) - ), - ) - - print("[Service] chat_authenticated: returning response", file=sys.stderr) - return VisualResponse( - text=parsed.get("text", "我在这里陪着你。"), - visual_metadata=visual_metadata, - memory_updated=memory_updated, - ) - - async def chat(self, message: UserMessage) -> VisualResponse: - """ - 处理用户消息并返回情感响应(游客模式,内存存储) - - Args: - message: 用户消息 - - Returns: - 包含文字和视觉元数据的响应 - """ - print( - f"[Service] chat: received message - session_id={message.session_id}, content={message.content[:50]!r}", - file=sys.stderr, - ) - - session_id = self._get_or_create_session(message.session_id) - - # Perform web search for factual questions - web_search_context = None - try: - from app.services.web_search import get_web_search_service - - search_service = get_web_search_service() - search_result = await search_service.search_for_context(message.content) - if search_result: - web_search_context, _ = search_result # Unpack tuple, ignore sources - print( - "[Service] chat: web search context found", - file=sys.stderr, - ) - except Exception as e: - print( - f"[Service] chat: web search failed: {e}", - file=sys.stderr, - ) - - system_prompt = self._build_system_prompt(session_id, web_search_context) - - print("[Service] chat: calling ModelScope API...", file=sys.stderr) - - try: - # 调用 ModelScope API (OpenAI 兼容) - response = self.client.chat.completions.create( - model=self._model, - messages=[ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": message.content}, - ], - temperature=0.7, - max_tokens=1024, - ) - print("[Service] chat: ModelScope response received", file=sys.stderr) - except Exception as e: - print( - f"[Service] chat: ModelScope API error: {type(e).__name__}: {e}", - file=sys.stderr, - ) - traceback.print_exc(file=sys.stderr) - raise ValueError(f"AI 服务调用失败: {e}") from e - - # 解析响应 - raw_content = response.choices[0].message.content or "" - print( - f"[Service] chat: raw_content = {raw_content[:200]!r}...", file=sys.stderr - ) - parsed = self._parse_llm_response(raw_content) - - # Apply Chain-of-Verification to reduce hallucinations - if web_search_context and parsed.get("text"): - verified_text, cove_metadata = self._verify_and_correct_response( - parsed["text"], web_search_context - ) - if cove_metadata.get("corrected"): - print( - "[Service] chat: CoVe corrected response", - file=sys.stderr, - ) - parsed["text"] = verified_text - - # 更新记忆 - memory_updated = self._update_profile_from_extract_guest( - session_id, parsed.get("memory_extract") - ) - - # 保存对话历史 - memory = self._memory_store[session_id] - memory.turns.append( - { - "user_input": message.content, - "assistant_response": parsed.get("text", ""), - } - ) - # 保持最大轮数限制 - if len(memory.turns) > memory.max_turns: - memory.turns = memory.turns[-memory.max_turns :] - - # 构建视觉元数据 - visual_data = parsed.get("visual_metadata", {}) - visual_metadata = VisualMetadata( - effect_type=EffectType( - visual_data.get("effect_type", EffectType.CALM.value) - ), - intensity=float(visual_data.get("intensity", 0.5)), - color_tone=ColorTone( - visual_data.get("color_tone", ColorTone.GENTLE_BLUE.value) - ), - ) - - print("[Service] chat: returning response", file=sys.stderr) - return VisualResponse( - text=parsed.get("text", "我在这里陪着你。"), - visual_metadata=visual_metadata, - memory_updated=memory_updated, - ) - - def get_session_profile(self, session_id: str) -> UserProfile | None: - """获取会话的用户画像(游客模式)""" - return self._profile_store.get(session_id) - - def get_session_memory(self, session_id: str) -> ConversationMemory | None: - """获取会话的对话记忆(游客模式)""" - return self._memory_store.get(session_id) - - async def get_user_profile( - self, db: AsyncSession, user_id: str - ) -> UserProfile | None: - """获取用户画像(已登录用户)""" - result = await db.execute( - select(ChatMemory).where(ChatMemory.user_id == user_id) - ) - memory_record = result.scalar_one_or_none() - - if memory_record: - profile_data = memory_record.get_profile() - return UserProfile( - preferred_name=profile_data.get("preferred_name"), - has_pets=profile_data.get("has_pets", False), - pet_details=profile_data.get("pet_details"), - interests=profile_data.get("interests", []), - concerns=profile_data.get("concerns", []), - important_dates=profile_data.get("important_dates", []), - baby_age_weeks=profile_data.get("baby_age_weeks"), - community_interactions=profile_data.get("community_interactions", []), - ) - return None - - async def add_community_interaction( - self, db: AsyncSession, user_id: str, interaction: str - ) -> None: - """将社区互动记录添加到用户记忆中""" - result = await db.execute( - select(ChatMemory).where(ChatMemory.user_id == user_id) - ) - memory_record = result.scalar_one_or_none() - - if not memory_record: - memory_record = ChatMemory(user_id=user_id) - db.add(memory_record) - - profile_data = memory_record.get_profile() if memory_record.profile_data else {} - interactions = profile_data.get("community_interactions", []) - interactions.append(interaction) - # Keep last 20 interactions - if len(interactions) > 20: - interactions = interactions[-20:] - profile_data["community_interactions"] = interactions - memory_record.set_profile(profile_data) - await db.commit() - - -# 全局服务实例(单例模式) -_companion_service: CompanionService | None = None - - -def get_companion_service() -> CompanionService: - """获取 CompanionService 单例""" - global _companion_service - if _companion_service is None: - _companion_service = CompanionService() - return _companion_service - - -async def save_community_interaction(user_id: str, interaction: str) -> None: - """ - 保存用户社区互动到聊天记忆(供社区模块调用) - - Args: - user_id: 用户 ID - interaction: 互动内容摘要,例如 "发帖:《关于产后恢复的问题》" - """ - from app.core.database import async_session_maker - - async with async_session_maker() as db: - service = get_companion_service() - await service.add_community_interaction(db, user_id, interaction) diff --git a/backend/app/services/coach/__init__.py b/backend/app/services/coach/__init__.py deleted file mode 100644 index 967c5aa5..00000000 --- a/backend/app/services/coach/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Recovery Coach - AI-powered postpartum recovery coaching module.""" - -from app.services.coach.workflow.graph import CoachWorkflow, create_workflow - -__all__ = ["CoachWorkflow", "create_workflow"] diff --git a/backend/app/services/coach/analysis/__init__.py b/backend/app/services/coach/analysis/__init__.py deleted file mode 100644 index 47a97340..00000000 --- a/backend/app/services/coach/analysis/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Analysis module for posture and safety monitoring.""" diff --git a/backend/app/services/coach/analysis/posture.py b/backend/app/services/coach/analysis/posture.py deleted file mode 100644 index 074d2246..00000000 --- a/backend/app/services/coach/analysis/posture.py +++ /dev/null @@ -1,203 +0,0 @@ -"""Posture analysis module for exercise form evaluation.""" - -from app.schemas.exercise import AngleRequirement, Exercise, PhaseRequirement -from app.schemas.pose import Keypoint, PoseAnalysisResult, PoseData -from app.services.coach.pose.keypoints import ( - calculate_angle, - get_hip_angle, - get_knee_angle, - get_pelvic_tilt, - get_spine_alignment, - is_lying_down, -) - - -class PostureAnalyzer: - """Analyzes pose data against exercise requirements.""" - - def __init__(self, tolerance: float = 15.0) -> None: - """Initialize the analyzer. - - Args: - tolerance: Tolerance in degrees for angle comparisons. - """ - self.tolerance = tolerance - - def analyze( - self, - pose: PoseData, - exercise: Exercise, - current_phase: PhaseRequirement, - ) -> PoseAnalysisResult: - """Analyze a pose against the current exercise phase requirements. - - Args: - pose: Current pose data. - exercise: The exercise being performed. - current_phase: The current phase of the exercise. - - Returns: - Analysis result with score and feedback. - """ - if not pose.is_valid: - return PoseAnalysisResult( - is_correct=False, - score=0.0, - deviations=["无法检测到完整的身体姿态,请调整摄像头角度"], - suggestions=["请确保全身在画面中可见"], - angles={}, - ) - - deviations: list[str] = [] - suggestions: list[str] = [] - angles: dict[str, float] = {} - - # Check angle requirements - for angle_req in current_phase.angles: - measured = self._measure_angle(pose, angle_req.joint_name) - if measured is not None: - angles[angle_req.joint_name] = measured - deviation = self._check_angle_deviation(measured, angle_req) - if deviation: - deviations.append(deviation["message"]) - if deviation.get("suggestion"): - suggestions.append(deviation["suggestion"]) - - # Add standard posture checks - posture_issues = self._check_general_posture(pose, exercise) - deviations.extend(posture_issues["deviations"]) - suggestions.extend(posture_issues["suggestions"]) - - # Calculate score - score = self._calculate_score( - len(deviations), - len(current_phase.angles) + 2, # angle checks + general posture checks - ) - - return PoseAnalysisResult( - is_correct=len(deviations) == 0, - score=score, - deviations=deviations, - suggestions=suggestions, - angles=angles, - ) - - def _measure_angle(self, pose: PoseData, joint_name: str) -> float | None: - """Measure a specific joint angle from pose data. - - Args: - pose: Pose data. - joint_name: Name of the joint angle to measure. - - Returns: - Angle in degrees or None if not measurable. - """ - match joint_name: - case "knee" | "left_knee": - return get_knee_angle(pose, "left") - case "right_knee": - return get_knee_angle(pose, "right") - case "hip" | "left_hip": - return get_hip_angle(pose, "left") - case "right_hip": - return get_hip_angle(pose, "right") - case "spine": - return get_spine_alignment(pose) - case "pelvic_tilt": - return get_pelvic_tilt(pose) - case "shoulder" | "left_shoulder": - # Shoulder angle: elbow-shoulder-hip - elbow = pose.get_keypoint(Keypoint.LEFT_ELBOW) - shoulder = pose.get_keypoint(Keypoint.LEFT_SHOULDER) - hip = pose.get_keypoint(Keypoint.LEFT_HIP) - if elbow and shoulder and hip: - return calculate_angle(elbow, shoulder, hip) - case "right_shoulder": - elbow = pose.get_keypoint(Keypoint.RIGHT_ELBOW) - shoulder = pose.get_keypoint(Keypoint.RIGHT_SHOULDER) - hip = pose.get_keypoint(Keypoint.RIGHT_HIP) - if elbow and shoulder and hip: - return calculate_angle(elbow, shoulder, hip) - case "hip_abduction": - # For side-lying leg lift - hip = pose.get_keypoint(Keypoint.LEFT_HIP) - knee = pose.get_keypoint(Keypoint.LEFT_KNEE) - other_hip = pose.get_keypoint(Keypoint.RIGHT_HIP) - if hip and knee and other_hip: - return calculate_angle(other_hip, hip, knee) - return None - - def _check_angle_deviation( - self, - measured: float, - requirement: AngleRequirement, - ) -> dict[str, str] | None: - """Check if measured angle deviates from requirement. - - Returns: - Dict with deviation message and suggestion, or None if within tolerance. - """ - if measured < requirement.min_angle - self.tolerance: - return { - "message": f"{requirement.joint_name}角度过小({measured:.0f}°,应大于{requirement.min_angle:.0f}°)", - "suggestion": f"试着增大{requirement.joint_name}的角度", - } - elif measured > requirement.max_angle + self.tolerance: - return { - "message": f"{requirement.joint_name}角度过大({measured:.0f}°,应小于{requirement.max_angle:.0f}°)", - "suggestion": f"试着减小{requirement.joint_name}的角度", - } - return None - - def _check_general_posture( - self, - pose: PoseData, - exercise: Exercise, - ) -> dict[str, list[str]]: - """Check general posture issues applicable to most exercises. - - Returns: - Dict with lists of deviations and suggestions. - """ - deviations: list[str] = [] - suggestions: list[str] = [] - - # Check spine alignment for standing/sitting exercises - if not is_lying_down(pose): - spine_angle = get_spine_alignment(pose) - if spine_angle > 15: - deviations.append(f"脊柱侧倾({spine_angle:.0f}°)") - suggestions.append("试着保持脊柱直立,不要向一侧倾斜") - - # Check for excessive pelvic tilt (for relevant exercises) - if exercise.category.value in ["diastasis_recti", "pelvic_floor"]: - pelvic_tilt = get_pelvic_tilt(pose) - if abs(pelvic_tilt) > 20: - deviations.append("骨盆倾斜角度过大") - suggestions.append("注意保持骨盆稳定") - - return {"deviations": deviations, "suggestions": suggestions} - - def _calculate_score(self, deviation_count: int, total_checks: int) -> float: - """Calculate a score based on the number of deviations. - - Args: - deviation_count: Number of detected deviations. - total_checks: Total number of checks performed. - - Returns: - Score from 0 to 100. - """ - if total_checks == 0: - return 100.0 - - # Each deviation reduces score proportionally - base_score = 100.0 - penalty_per_deviation = 100.0 / max(total_checks, 1) - score = base_score - (deviation_count * penalty_per_deviation) - return max(0.0, min(100.0, score)) - - -def create_analyzer(tolerance: float = 15.0) -> PostureAnalyzer: - """Factory function to create a PostureAnalyzer.""" - return PostureAnalyzer(tolerance=tolerance) diff --git a/backend/app/services/coach/analysis/safety.py b/backend/app/services/coach/analysis/safety.py deleted file mode 100644 index a3921851..00000000 --- a/backend/app/services/coach/analysis/safety.py +++ /dev/null @@ -1,220 +0,0 @@ -"""Safety monitoring module for exercise sessions. - -This module monitors for potentially dangerous situations and -triggers rest prompts when needed. -""" - -import time -from collections import deque -from dataclasses import dataclass, field -from enum import Enum - -from app.core.config import get_settings -from app.schemas.pose import PoseAnalysisResult, PoseData - -settings = get_settings() - - -class SafetyAlertLevel(str, Enum): - """Safety alert severity levels.""" - - INFO = "info" - WARNING = "warning" - CRITICAL = "critical" - - -@dataclass -class SafetyAlert: - """A safety alert generated by the monitor.""" - - level: SafetyAlertLevel - message: str - recommendation: str - timestamp: float = field(default_factory=time.time) - - -@dataclass -class SafetyStatus: - """Current safety status of the session.""" - - is_safe: bool = True - should_rest: bool = False - alerts: list[SafetyAlert] = field(default_factory=list) - consecutive_poor_form: int = 0 - session_duration: float = 0.0 - time_since_last_rest: float = 0.0 - - -class SafetyMonitor: - """Monitors exercise safety and triggers appropriate alerts.""" - - def __init__( - self, - max_consecutive_poor_form: int = 5, - min_form_score: float = 40.0, - rest_interval: int | None = None, - fatigue_threshold: float | None = None, - ) -> None: - """Initialize the safety monitor. - - Args: - max_consecutive_poor_form: Max frames of poor form before alert. - min_form_score: Minimum acceptable form score. - rest_interval: Seconds between rest prompts. - fatigue_threshold: Threshold for fatigue detection (0-1). - """ - self.max_consecutive_poor_form = max_consecutive_poor_form - self.min_form_score = min_form_score - self.rest_interval = rest_interval or settings.rest_prompt_interval - self.fatigue_threshold = ( - fatigue_threshold or settings.fatigue_detection_threshold - ) - - # Internal state - self._session_start: float | None = None - self._last_rest_time: float | None = None - self._consecutive_poor_form = 0 - self._score_history: deque[float] = deque(maxlen=30) # Last 30 frames - self._movement_history: deque[float] = deque(maxlen=60) # Movement speed - - def start_session(self) -> None: - """Start a new monitoring session.""" - self._session_start = time.time() - self._last_rest_time = time.time() - self._consecutive_poor_form = 0 - self._score_history.clear() - self._movement_history.clear() - - def check( - self, - pose: PoseData, - analysis: PoseAnalysisResult, - ) -> SafetyStatus: - """Check current pose and analysis for safety issues. - - Args: - pose: Current pose data. - analysis: Current analysis result. - - Returns: - Current safety status with any alerts. - """ - if self._session_start is None: - self.start_session() - - current_time = time.time() - alerts: list[SafetyAlert] = [] - - # Update score history - self._score_history.append(analysis.score) - - # Check for consecutive poor form - if analysis.score < self.min_form_score: - self._consecutive_poor_form += 1 - else: - self._consecutive_poor_form = 0 - - # Generate alerts based on conditions - should_rest = False - - # Poor form alert - if self._consecutive_poor_form >= self.max_consecutive_poor_form: - alerts.append( - SafetyAlert( - level=SafetyAlertLevel.WARNING, - message="连续多次动作不标准", - recommendation="请暂停一下,调整姿势后再继续", - ) - ) - should_rest = True - - # Timed rest prompt - time_since_rest = current_time - (self._last_rest_time or current_time) - if time_since_rest >= self.rest_interval: - alerts.append( - SafetyAlert( - level=SafetyAlertLevel.INFO, - message=f"已连续训练{int(time_since_rest / 60)}分钟", - recommendation="建议稍作休息,喝点水", - ) - ) - - # Declining performance alert (fatigue indicator) - if len(self._score_history) >= 20: - recent_avg = sum(list(self._score_history)[-10:]) / 10 - earlier_avg = sum(list(self._score_history)[:10]) / 10 - if earlier_avg > 0 and recent_avg / earlier_avg < self.fatigue_threshold: - alerts.append( - SafetyAlert( - level=SafetyAlertLevel.WARNING, - message="检测到动作质量下降", - recommendation="你可能有些疲劳了,建议休息一下再继续", - ) - ) - should_rest = True - - # Critical alerts from analysis - critical_deviations = [ - d - for d in analysis.deviations - if any(keyword in d for keyword in ["严重", "危险", "过度", "疼痛"]) - ] - if critical_deviations: - alerts.append( - SafetyAlert( - level=SafetyAlertLevel.CRITICAL, - message="检测到可能造成损伤的动作", - recommendation="请立即停止,检查动作是否正确", - ) - ) - should_rest = True - - # Build status - session_duration = current_time - (self._session_start or current_time) - - return SafetyStatus( - is_safe=len([a for a in alerts if a.level == SafetyAlertLevel.CRITICAL]) - == 0, - should_rest=should_rest, - alerts=alerts, - consecutive_poor_form=self._consecutive_poor_form, - session_duration=session_duration, - time_since_last_rest=time_since_rest, - ) - - def record_rest(self) -> None: - """Record that the user took a rest.""" - self._last_rest_time = time.time() - self._consecutive_poor_form = 0 - self._score_history.clear() - - def get_session_stats(self) -> dict: - """Get statistics for the current session. - - Returns: - Dict with session statistics. - """ - if not self._score_history: - return { - "average_score": 0.0, - "min_score": 0.0, - "max_score": 0.0, - "frames_analyzed": 0, - "session_duration": 0.0, - } - - scores = list(self._score_history) - session_duration = time.time() - (self._session_start or time.time()) - - return { - "average_score": sum(scores) / len(scores), - "min_score": min(scores), - "max_score": max(scores), - "frames_analyzed": len(scores), - "session_duration": session_duration, - } - - -def create_safety_monitor(**kwargs) -> SafetyMonitor: - """Factory function to create a SafetyMonitor.""" - return SafetyMonitor(**kwargs) diff --git a/backend/app/services/coach/exercises/__init__.py b/backend/app/services/coach/exercises/__init__.py deleted file mode 100644 index fc1dde01..00000000 --- a/backend/app/services/coach/exercises/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Exercises module.""" diff --git a/backend/app/services/coach/exercises/library.py b/backend/app/services/coach/exercises/library.py deleted file mode 100644 index 4b38e12f..00000000 --- a/backend/app/services/coach/exercises/library.py +++ /dev/null @@ -1,620 +0,0 @@ -"""Postpartum recovery exercise library. - -This module defines a curated library of exercises specifically designed -for postpartum recovery, focusing on: -- Diastasis recti repair -- Pelvic floor strengthening -- Core rehabilitation -- Posture correction -""" - -from app.schemas.exercise import ( - AngleRequirement, - Difficulty, - Exercise, - ExerciseCategory, - ExercisePhase, - ExerciseSession, - PhaseRequirement, -) - -# ============================================================================= -# BREATHING EXERCISES -# ============================================================================= - -DIAPHRAGMATIC_BREATHING = Exercise( - id="diaphragmatic_breathing", - name="腹式呼吸", - name_en="Diaphragmatic Breathing", - category=ExerciseCategory.BREATHING, - difficulty=Difficulty.BEGINNER, - description="通过深度腹式呼吸激活横膈膜,帮助放松身心,是所有康复训练的基础。", - benefits=[ - "激活横膈膜和深层核心肌群", - "减轻焦虑和压力", - "改善氧气供应", - "为盆底肌训练做准备", - ], - contraindications=[], - phases=[ - PhaseRequirement( - phase=ExercisePhase.PREPARATION, - duration_seconds=3, - description="仰卧,双膝弯曲,双脚平放在地面上,一只手放在胸口,一只手放在腹部。", - cues=["找一个舒适的姿势", "放松肩膀"], - ), - PhaseRequirement( - phase=ExercisePhase.INHALE, - duration_seconds=4, - description="用鼻子缓慢吸气,感受腹部向上隆起,胸部保持不动。", - cues=["用鼻子吸气", "感受腹部隆起", "胸部不要动"], - ), - PhaseRequirement( - phase=ExercisePhase.HOLD, - duration_seconds=2, - description="短暂屏息,感受腹部的扩张。", - cues=["轻轻屏住呼吸"], - ), - PhaseRequirement( - phase=ExercisePhase.EXHALE, - duration_seconds=6, - description="用嘴巴缓慢呼气,感受腹部向内收缩。", - cues=["用嘴巴呼气", "腹部向内收", "完全呼出"], - ), - ], - repetitions=10, - sets=3, - rest_between_sets=30, -) - -CORE_ACTIVATION_BREATH = Exercise( - id="core_activation_breath", - name="核心激活呼吸", - name_en="Core Activation Breath", - category=ExerciseCategory.BREATHING, - difficulty=Difficulty.BEGINNER, - description="结合呼吸与轻柔的腹部收缩,安全地激活深层核心肌群。", - benefits=[ - "安全激活腹横肌", - "建立核心意识", - "为更高级的训练做准备", - ], - contraindications=["严重腹直肌分离(>3指宽)建议先咨询医生"], - phases=[ - PhaseRequirement( - phase=ExercisePhase.PREPARATION, - duration_seconds=3, - description="仰卧,双膝弯曲,脊柱保持自然曲度。", - cues=["放松身体", "保持自然呼吸"], - ), - PhaseRequirement( - phase=ExercisePhase.INHALE, - duration_seconds=4, - description="深吸气,让腹部自然扩张。", - cues=["用鼻子吸气", "腹部放松"], - ), - PhaseRequirement( - phase=ExercisePhase.EXHALE, - duration_seconds=6, - description="呼气时,想象肚脐向脊柱方向轻轻靠近,同时轻轻收紧盆底肌。", - cues=["呼气", "肚脐向内收", "轻轻提起盆底肌", "不要憋气"], - ), - PhaseRequirement( - phase=ExercisePhase.HOLD, - duration_seconds=3, - description="保持这个轻柔的收缩,继续正常呼吸。", - cues=["保持收缩", "继续呼吸"], - ), - PhaseRequirement( - phase=ExercisePhase.RELEASE, - duration_seconds=2, - description="缓慢放松。", - cues=["慢慢放松"], - ), - ], - repetitions=10, - sets=3, - rest_between_sets=30, -) - -# ============================================================================= -# PELVIC FLOOR EXERCISES -# ============================================================================= - -KEGEL_EXERCISE = Exercise( - id="kegel_exercise", - name="凯格尔运动", - name_en="Kegel Exercise", - category=ExerciseCategory.PELVIC_FLOOR, - difficulty=Difficulty.BEGINNER, - description="收缩和放松盆底肌群,增强盆底支撑力,改善尿失禁。", - benefits=[ - "增强盆底肌力量", - "改善尿失禁", - "加速产后恢复", - "提高性生活质量", - ], - contraindications=[ - "盆底肌过度紧张者", - "盆腔炎症期间", - ], - phases=[ - PhaseRequirement( - phase=ExercisePhase.PREPARATION, - duration_seconds=3, - description="找到一个舒适的姿势,可以躺着、坐着或站着。放松臀部和大腿肌肉。", - cues=["放松臀部", "放松大腿", "只关注盆底肌"], - ), - PhaseRequirement( - phase=ExercisePhase.INHALE, - duration_seconds=2, - description="深吸一口气准备。", - cues=["深吸气"], - ), - PhaseRequirement( - phase=ExercisePhase.EXHALE, - duration_seconds=5, - description="呼气时,想象要憋住尿或者阻止放屁,收紧盆底肌向上提。", - cues=["呼气", "向上提起盆底肌", "像憋尿一样", "不要收紧臀部"], - ), - PhaseRequirement( - phase=ExercisePhase.HOLD, - duration_seconds=5, - description="保持收缩,继续正常呼吸。", - cues=["保持住", "继续呼吸", "不要憋气"], - ), - PhaseRequirement( - phase=ExercisePhase.RELEASE, - duration_seconds=5, - description="缓慢完全放松盆底肌。", - cues=["慢慢放松", "完全放松"], - ), - ], - repetitions=10, - sets=3, - rest_between_sets=60, -) - -PELVIC_TILT = Exercise( - id="pelvic_tilt", - name="骨盆倾斜", - name_en="Pelvic Tilt", - category=ExerciseCategory.PELVIC_FLOOR, - difficulty=Difficulty.BEGINNER, - description="通过轻柔的骨盆运动,激活深层核心和下背部肌肉。", - benefits=[ - "缓解下背痛", - "激活深层核心肌群", - "改善骨盆灵活性", - "安全的产后早期运动", - ], - contraindications=[], - phases=[ - PhaseRequirement( - phase=ExercisePhase.PREPARATION, - duration_seconds=3, - description="仰卧,双膝弯曲,双脚平放在地面上,与臀部同宽。", - angles=[ - AngleRequirement( - joint_name="knee", - min_angle=80, - max_angle=100, - ideal_angle=90, - ), - ], - cues=["躺平", "双膝弯曲", "双脚与臀部同宽"], - ), - PhaseRequirement( - phase=ExercisePhase.INHALE, - duration_seconds=3, - description="吸气,让下背部自然地离开地面,形成一个小小的弧度。", - cues=["吸气", "下背部轻轻离开地面"], - ), - PhaseRequirement( - phase=ExercisePhase.EXHALE, - duration_seconds=4, - description="呼气,将下背部压向地面,骨盆向后倾斜,收紧腹部。", - cues=["呼气", "下背部贴地", "骨盆后倾", "收紧小腹"], - ), - PhaseRequirement( - phase=ExercisePhase.HOLD, - duration_seconds=3, - description="保持这个位置。", - cues=["保持住"], - ), - ], - repetitions=15, - sets=3, - rest_between_sets=30, -) - -# ============================================================================= -# DIASTASIS RECTI EXERCISES -# ============================================================================= - -DEAD_BUG_MODIFIED = Exercise( - id="dead_bug_modified", - name="死虫式(简化版)", - name_en="Dead Bug Modified", - category=ExerciseCategory.DIASTASIS_RECTI, - difficulty=Difficulty.BEGINNER, - description="安全的核心训练动作,在保护腹直肌的同时增强核心稳定性。", - benefits=[ - "安全修复腹直肌分离", - "增强核心稳定性", - "改善身体协调性", - "保护脊柱", - ], - contraindications=[ - "做动作时感到腹部鼓起(圆顶现象)", - "下背部疼痛", - ], - phases=[ - PhaseRequirement( - phase=ExercisePhase.PREPARATION, - duration_seconds=3, - description="仰卧,双膝弯曲90度,小腿与地面平行(桌面位置),双臂向天花板伸直。", - angles=[ - AngleRequirement( - joint_name="hip", - min_angle=85, - max_angle=95, - ideal_angle=90, - ), - AngleRequirement( - joint_name="knee", - min_angle=85, - max_angle=95, - ideal_angle=90, - ), - ], - cues=["膝盖弯曲90度", "小腿与地面平行", "双臂伸向天花板"], - ), - PhaseRequirement( - phase=ExercisePhase.INHALE, - duration_seconds=3, - description="吸气准备,保持下背部贴地。", - cues=["吸气", "下背部贴地"], - ), - PhaseRequirement( - phase=ExercisePhase.EXHALE, - duration_seconds=4, - description="呼气,慢慢将一只脚向前滑动(脚跟贴地),同时收紧核心。", - cues=["呼气", "一只脚向前滑", "脚跟贴地", "核心收紧", "下背部不要离开地面"], - ), - PhaseRequirement( - phase=ExercisePhase.INHALE, - duration_seconds=3, - description="吸气,将腿收回起始位置。", - cues=["吸气", "收回"], - ), - ], - repetitions=8, - sets=3, - rest_between_sets=45, -) - -ABDOMINAL_BRACING = Exercise( - id="abdominal_bracing", - name="腹部支撑", - name_en="Abdominal Bracing", - category=ExerciseCategory.DIASTASIS_RECTI, - difficulty=Difficulty.BEGINNER, - description="学习正确的腹部收缩方式,保护腹直肌,为日常活动打好基础。", - benefits=[ - "建立正确的核心激活模式", - "保护腹直肌", - "为日常活动提供支撑", - ], - contraindications=[], - phases=[ - PhaseRequirement( - phase=ExercisePhase.PREPARATION, - duration_seconds=3, - description="可以仰卧、坐着或站着,脊柱保持自然中立位。", - cues=["找到舒适的姿势", "脊柱自然"], - ), - PhaseRequirement( - phase=ExercisePhase.INHALE, - duration_seconds=3, - description="深吸气,放松腹部。", - cues=["深吸气", "腹部放松"], - ), - PhaseRequirement( - phase=ExercisePhase.EXHALE, - duration_seconds=5, - description="呼气,想象要轻轻收紧腰带,腹部360度向内收紧,但不是用力吸肚子。", - cues=["呼气", "想象收紧腰带", "360度收紧", "不是吸肚子"], - ), - PhaseRequirement( - phase=ExercisePhase.HOLD, - duration_seconds=10, - description="保持这个轻柔的张力,继续正常呼吸。", - cues=["保持张力", "正常呼吸", "不要憋气"], - ), - PhaseRequirement( - phase=ExercisePhase.RELEASE, - duration_seconds=3, - description="完全放松。", - cues=["放松"], - ), - ], - repetitions=8, - sets=3, - rest_between_sets=30, -) - -# ============================================================================= -# POSTURE EXERCISES -# ============================================================================= - -CAT_COW = Exercise( - id="cat_cow", - name="猫牛式", - name_en="Cat-Cow Stretch", - category=ExerciseCategory.POSTURE, - difficulty=Difficulty.BEGINNER, - description="经典的脊柱灵活性练习,缓解背部紧张,改善脊柱灵活性。", - benefits=[ - "缓解背部紧张", - "改善脊柱灵活性", - "放松肩颈", - "配合呼吸促进放松", - ], - contraindications=[ - "手腕疼痛(可以用前臂支撑代替)", - ], - phases=[ - PhaseRequirement( - phase=ExercisePhase.PREPARATION, - duration_seconds=3, - description="四点跪姿,手腕在肩膀正下方,膝盖在臀部正下方。", - angles=[ - AngleRequirement( - joint_name="shoulder", - min_angle=85, - max_angle=95, - ideal_angle=90, - ), - ], - cues=["四点跪姿", "手腕在肩膀下方", "膝盖在臀部下方"], - ), - PhaseRequirement( - phase=ExercisePhase.INHALE, - duration_seconds=4, - description="吸气,腹部下沉,抬头看向天花板,臀部翘起(牛式)。", - cues=["吸气", "腹部下沉", "抬头", "臀部翘起"], - ), - PhaseRequirement( - phase=ExercisePhase.EXHALE, - duration_seconds=4, - description="呼气,背部拱起像猫咪一样,低头看向肚脐(猫式)。", - cues=["呼气", "背部拱起", "低头", "肚脐向脊柱靠近"], - ), - ], - repetitions=10, - sets=2, - rest_between_sets=30, -) - -GLUTE_BRIDGE = Exercise( - id="glute_bridge", - name="肩桥", - name_en="Glute Bridge", - category=ExerciseCategory.STRENGTH, - difficulty=Difficulty.BEGINNER, - description="增强臀部和后侧链力量,改善骨盆稳定性。", - benefits=[ - "增强臀部力量", - "改善骨盆稳定性", - "缓解下背痛", - "激活后侧链", - ], - contraindications=[ - "严重耻骨联合分离", - ], - phases=[ - PhaseRequirement( - phase=ExercisePhase.PREPARATION, - duration_seconds=3, - description="仰卧,双膝弯曲,双脚平放地面与臀部同宽,双臂放在身体两侧。", - angles=[ - AngleRequirement( - joint_name="knee", - min_angle=80, - max_angle=100, - ideal_angle=90, - ), - ], - cues=["躺平", "双膝弯曲", "双脚与臀部同宽"], - ), - PhaseRequirement( - phase=ExercisePhase.EXHALE, - duration_seconds=4, - description="呼气,收紧核心和臀部,将臀部抬离地面,直到身体从肩膀到膝盖成一条直线。", - angles=[ - AngleRequirement( - joint_name="hip", - min_angle=170, - max_angle=180, - ideal_angle=175, - ), - ], - cues=["呼气", "收紧臀部", "臀部抬起", "肩到膝成一条线", "不要过度拱背"], - ), - PhaseRequirement( - phase=ExercisePhase.HOLD, - duration_seconds=3, - description="在顶部保持,继续挤压臀部。", - cues=["保持", "挤压臀部"], - ), - PhaseRequirement( - phase=ExercisePhase.INHALE, - duration_seconds=3, - description="吸气,缓慢将臀部放回地面。", - cues=["吸气", "慢慢放下"], - ), - ], - repetitions=12, - sets=3, - rest_between_sets=45, -) - -SIDE_LYING_LEG_LIFT = Exercise( - id="side_lying_leg_lift", - name="侧卧抬腿", - name_en="Side-Lying Leg Lift", - category=ExerciseCategory.STRENGTH, - difficulty=Difficulty.BEGINNER, - description="增强臀中肌和髋部稳定性,改善行走和站立的稳定性。", - benefits=[ - "增强臀中肌", - "改善髋部稳定性", - "预防和缓解髋部疼痛", - ], - contraindications=[], - phases=[ - PhaseRequirement( - phase=ExercisePhase.PREPARATION, - duration_seconds=3, - description="侧卧,下方手臂支撑头部,上方手放在髋部或地面上保持稳定,双腿伸直叠放。", - cues=["侧卧", "身体成一条直线", "双腿叠放"], - ), - PhaseRequirement( - phase=ExercisePhase.EXHALE, - duration_seconds=3, - description="呼气,收紧核心,将上方腿向上抬起约30-45度,保持脚尖朝前。", - angles=[ - AngleRequirement( - joint_name="hip_abduction", - min_angle=25, - max_angle=50, - ideal_angle=35, - ), - ], - cues=["呼气", "抬起上方腿", "脚尖朝前", "不要旋转骨盆"], - ), - PhaseRequirement( - phase=ExercisePhase.HOLD, - duration_seconds=2, - description="在顶部短暂保持。", - cues=["保持"], - ), - PhaseRequirement( - phase=ExercisePhase.INHALE, - duration_seconds=3, - description="吸气,缓慢放下腿部。", - cues=["吸气", "慢慢放下"], - ), - ], - repetitions=12, - sets=3, - rest_between_sets=30, -) - -# ============================================================================= -# EXERCISE LIBRARY -# ============================================================================= - -EXERCISE_LIBRARY: dict[str, Exercise] = { - # Breathing - DIAPHRAGMATIC_BREATHING.id: DIAPHRAGMATIC_BREATHING, - CORE_ACTIVATION_BREATH.id: CORE_ACTIVATION_BREATH, - # Pelvic Floor - KEGEL_EXERCISE.id: KEGEL_EXERCISE, - PELVIC_TILT.id: PELVIC_TILT, - # Diastasis Recti - DEAD_BUG_MODIFIED.id: DEAD_BUG_MODIFIED, - ABDOMINAL_BRACING.id: ABDOMINAL_BRACING, - # Posture & Strength - CAT_COW.id: CAT_COW, - GLUTE_BRIDGE.id: GLUTE_BRIDGE, - SIDE_LYING_LEG_LIFT.id: SIDE_LYING_LEG_LIFT, -} - -# ============================================================================= -# TRAINING SESSIONS -# ============================================================================= - -BEGINNER_CORE_SESSION = ExerciseSession( - id="beginner_core", - name="初级核心训练", - description="适合产后早期的温和核心训练,专注于呼吸和核心激活。", - exercises=[ - "diaphragmatic_breathing", - "core_activation_breath", - "pelvic_tilt", - "abdominal_bracing", - ], - total_duration_minutes=15, - focus_areas=[ExerciseCategory.BREATHING, ExerciseCategory.DIASTASIS_RECTI], -) - -PELVIC_FLOOR_SESSION = ExerciseSession( - id="pelvic_floor", - name="盆底肌训练", - description="专注于盆底肌恢复的训练计划。", - exercises=[ - "diaphragmatic_breathing", - "kegel_exercise", - "pelvic_tilt", - "glute_bridge", - ], - total_duration_minutes=20, - focus_areas=[ExerciseCategory.PELVIC_FLOOR], -) - -FULL_BODY_RECOVERY = ExerciseSession( - id="full_body_recovery", - name="全身恢复训练", - description="综合性的产后恢复训练,涵盖核心、盆底和体态。", - exercises=[ - "diaphragmatic_breathing", - "core_activation_breath", - "kegel_exercise", - "pelvic_tilt", - "dead_bug_modified", - "cat_cow", - "glute_bridge", - "side_lying_leg_lift", - ], - total_duration_minutes=30, - focus_areas=[ - ExerciseCategory.BREATHING, - ExerciseCategory.PELVIC_FLOOR, - ExerciseCategory.DIASTASIS_RECTI, - ExerciseCategory.POSTURE, - ExerciseCategory.STRENGTH, - ], -) - -SESSION_LIBRARY: dict[str, ExerciseSession] = { - BEGINNER_CORE_SESSION.id: BEGINNER_CORE_SESSION, - PELVIC_FLOOR_SESSION.id: PELVIC_FLOOR_SESSION, - FULL_BODY_RECOVERY.id: FULL_BODY_RECOVERY, -} - - -def get_exercise(exercise_id: str) -> Exercise | None: - """Get an exercise by ID.""" - return EXERCISE_LIBRARY.get(exercise_id) - - -def get_exercises_by_category(category: ExerciseCategory) -> list[Exercise]: - """Get all exercises in a category.""" - return [ex for ex in EXERCISE_LIBRARY.values() if ex.category == category] - - -def get_session(session_id: str) -> ExerciseSession | None: - """Get a training session by ID.""" - return SESSION_LIBRARY.get(session_id) - - -def get_all_exercises() -> list[Exercise]: - """Get all exercises.""" - return list(EXERCISE_LIBRARY.values()) - - -def get_all_sessions() -> list[ExerciseSession]: - """Get all training sessions.""" - return list(SESSION_LIBRARY.values()) diff --git a/backend/app/services/coach/feedback/__init__.py b/backend/app/services/coach/feedback/__init__.py deleted file mode 100644 index 4be7aafb..00000000 --- a/backend/app/services/coach/feedback/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Feedback module.""" diff --git a/backend/app/services/coach/feedback/generator.py b/backend/app/services/coach/feedback/generator.py deleted file mode 100644 index a5a8d10a..00000000 --- a/backend/app/services/coach/feedback/generator.py +++ /dev/null @@ -1,271 +0,0 @@ -"""AI-powered feedback generator using LangChain and ModelScope API.""" - -from langchain_core.messages import HumanMessage, SystemMessage -from langchain_core.output_parsers import StrOutputParser -from langchain_openai import ChatOpenAI - -from app.core.config import get_settings -from app.schemas.exercise import Exercise, PhaseRequirement -from app.schemas.feedback import FeedbackMessage, FeedbackType -from app.schemas.pose import PoseAnalysisResult -from app.services.coach.analysis.safety import SafetyAlert, SafetyAlertLevel - -settings = get_settings() - -SYSTEM_PROMPT = """你是一位温柔、专业的产后康复教练。你的任务是为正在做康复训练的妈妈们提供实时语音反馈。 - -你的反馈应该: -1. 温和鼓励,不批评 -2. 简短有力,适合语音播报(不超过20个字) -3. 专业但易懂 -4. 关注安全,及时提醒 -5. 使用积极正面的表达 - -示例反馈: -- "做得很好,保持住" -- "试着呼气时收紧小腹" -- "膝盖再弯曲一点点" -- "慢一点,感受肌肉的收缩" -- "太棒了,休息一下吧" - -避免: -- "动作不对" -- "错了" -- "不行" -- 过于复杂的解释 -- 批评性语言""" - - -class FeedbackGenerator: - """Generates appropriate feedback messages based on pose analysis.""" - - def __init__(self, use_llm: bool = True) -> None: - """Initialize the feedback generator. - - Args: - use_llm: Whether to use LLM for dynamic feedback generation. - """ - self.use_llm = use_llm and bool(settings.modelscope_key) - - if self.use_llm: - # Create ChatOpenAI with ModelScope API - self.llm = ChatOpenAI( - model=settings.modelscope_model, - api_key=settings.modelscope_key, # type: ignore[arg-type] - temperature=0.7, - base_url=settings.modelscope_base_url, - model_kwargs={"max_tokens": 100}, - ) - self.parser = StrOutputParser() - - # Pre-defined feedback templates for fallback - self._templates = { - "good_form": [ - "做得很好,保持住", - "非常棒,继续保持", - "动作很标准", - "很好,就是这样", - ], - "minor_correction": [ - "稍微调整一下", - "再试一次,你可以的", - "慢一点,感受动作", - ], - "encouragement": [ - "你做得很好", - "继续加油", - "坚持住", - "你很棒", - ], - "rest": [ - "休息一下吧", - "喝点水,放松一下", - "做得很好,稍作休息", - ], - } - self._template_index: dict[str, int] = {} - - async def generate( - self, - analysis: PoseAnalysisResult, - exercise: Exercise, - phase: PhaseRequirement, - safety_alerts: list[SafetyAlert] | None = None, - ) -> FeedbackMessage: - """Generate feedback based on current analysis. - - Args: - analysis: Current pose analysis result. - exercise: The exercise being performed. - phase: Current exercise phase. - safety_alerts: Any active safety alerts. - - Returns: - FeedbackMessage to display/speak. - """ - # Handle safety alerts first (highest priority) - if safety_alerts: - critical = [ - a for a in safety_alerts if a.level == SafetyAlertLevel.CRITICAL - ] - if critical: - return FeedbackMessage( - type=FeedbackType.SAFETY_WARNING, - text=critical[0].recommendation, - priority=5, - should_speak=True, - ) - - warnings = [a for a in safety_alerts if a.level == SafetyAlertLevel.WARNING] - if warnings: - return FeedbackMessage( - type=FeedbackType.REST_PROMPT, - text=warnings[0].recommendation, - priority=4, - should_speak=True, - ) - - # Good form feedback - if analysis.is_correct and analysis.score >= 80: - return await self._generate_encouragement(exercise, phase) - - # Correction needed - if analysis.deviations: - return await self._generate_correction(analysis, exercise, phase) - - # Default phase cue - return self._get_phase_cue(phase) - - async def _generate_encouragement( - self, - exercise: Exercise, - phase: PhaseRequirement, - ) -> FeedbackMessage: - """Generate encouragement feedback for good form.""" - if self.use_llm: - try: - response = await self._call_llm( - f"用户正在做{exercise.name}的{phase.phase.value}阶段,动作非常标准。" - f"请生成一句简短的鼓励语(不超过15个字)。" - ) - return FeedbackMessage( - type=FeedbackType.ENCOURAGEMENT, - text=response, - priority=2, - should_speak=True, - ) - except Exception: - pass - - # Fallback to template - return FeedbackMessage( - type=FeedbackType.ENCOURAGEMENT, - text=self._get_template("good_form"), - priority=2, - should_speak=True, - ) - - async def _generate_correction( - self, - analysis: PoseAnalysisResult, - exercise: Exercise, - phase: PhaseRequirement, - ) -> FeedbackMessage: - """Generate correction feedback for form issues.""" - deviation = analysis.deviations[0] if analysis.deviations else "" - suggestion = analysis.suggestions[0] if analysis.suggestions else "" - - if self.use_llm and deviation: - try: - response = await self._call_llm( - f"用户正在做{exercise.name},检测到问题:{deviation}。" - f"建议:{suggestion}。" - f"请用温和的语气生成一句简短的纠正提示(不超过20个字)," - f"不要说'动作不对'或批评性语言。" - ) - return FeedbackMessage( - type=FeedbackType.CORRECTION, - text=response, - priority=3, - should_speak=True, - ) - except Exception: - pass - - # Fallback: use suggestion directly or template - if suggestion: - return FeedbackMessage( - type=FeedbackType.CORRECTION, - text=suggestion[:25] if len(suggestion) > 25 else suggestion, - priority=3, - should_speak=True, - ) - - return FeedbackMessage( - type=FeedbackType.CORRECTION, - text=self._get_template("minor_correction"), - priority=3, - should_speak=True, - ) - - def _get_phase_cue(self, phase: PhaseRequirement) -> FeedbackMessage: - """Get a verbal cue for the current phase.""" - if phase.cues: - cue = phase.cues[0] - return FeedbackMessage( - type=FeedbackType.PHASE_CUE, - text=cue, - priority=2, - should_speak=True, - ) - - return FeedbackMessage( - type=FeedbackType.PHASE_CUE, - text=phase.description[:25], - priority=2, - should_speak=True, - ) - - async def _call_llm(self, prompt: str) -> str: - """Call the LLM with the given prompt.""" - messages = [ - SystemMessage(content=SYSTEM_PROMPT), - HumanMessage(content=prompt), - ] - - chain = self.llm | self.parser - response = await chain.ainvoke(messages) - return response.strip() - - def _get_template(self, category: str) -> str: - """Get next template from a category (round-robin).""" - templates = self._templates.get(category, self._templates["encouragement"]) - index = self._template_index.get(category, 0) - template = templates[index % len(templates)] - self._template_index[category] = index + 1 - return template - - def generate_completion_message( - self, - exercise: Exercise, - average_score: float, - ) -> FeedbackMessage: - """Generate a completion message for an exercise.""" - if average_score >= 80: - text = f"太棒了!{exercise.name}完成得非常好" - elif average_score >= 60: - text = f"做得不错!{exercise.name}完成了" - else: - text = f"{exercise.name}完成了,继续练习会更好" - - return FeedbackMessage( - type=FeedbackType.COMPLETION, - text=text, - priority=2, - should_speak=True, - ) - - -def create_feedback_generator(use_llm: bool = True) -> FeedbackGenerator: - """Factory function to create a FeedbackGenerator.""" - return FeedbackGenerator(use_llm=use_llm) diff --git a/backend/app/services/coach/feedback/tts.py b/backend/app/services/coach/feedback/tts.py deleted file mode 100644 index e8059d74..00000000 --- a/backend/app/services/coach/feedback/tts.py +++ /dev/null @@ -1,232 +0,0 @@ -"""Text-to-Speech module using edge-tts.""" - -import asyncio -import hashlib -import io -from pathlib import Path - -import edge_tts - -from app.core.config import get_settings - -settings = get_settings() - - -class TTSEngine: - """Text-to-Speech engine using Microsoft Edge TTS.""" - - def __init__( - self, - voice: str | None = None, - rate: str | None = None, - cache_dir: Path | None = None, - ) -> None: - """Initialize the TTS engine. - - Args: - voice: Voice name (e.g., "zh-CN-XiaoxiaoNeural"). - rate: Speech rate adjustment (e.g., "-10%", "+20%"). - cache_dir: Directory to cache generated audio files. - """ - self.voice = voice or settings.tts_voice - self.rate = rate or settings.tts_rate - self.cache_dir = cache_dir or Path("./tts_cache") - self.cache_dir.mkdir(parents=True, exist_ok=True) - - # Available Chinese voices - self.available_voices = { - "xiaoxiao": "zh-CN-XiaoxiaoNeural", # Female, warm - "yunxi": "zh-CN-YunxiNeural", # Male, friendly - "xiaoyi": "zh-CN-XiaoyiNeural", # Female, lively - "yunjian": "zh-CN-YunjianNeural", # Male, professional - } - - async def synthesize(self, text: str) -> bytes: - """Synthesize speech from text. - - Args: - text: Text to synthesize. - - Returns: - Audio data as bytes (MP3 format). - """ - # Check cache first - cache_key = self._get_cache_key(text) - cache_path = self.cache_dir / f"{cache_key}.mp3" - - if cache_path.exists(): - return cache_path.read_bytes() - - # Generate new audio - communicate = edge_tts.Communicate( - text=text, - voice=self.voice, - rate=self.rate, - ) - - audio_data = io.BytesIO() - async for chunk in communicate.stream(): - if chunk["type"] == "audio": - audio_data.write(chunk["data"]) - - audio_bytes = audio_data.getvalue() - - # Cache the result - if audio_bytes: - cache_path.write_bytes(audio_bytes) - - return audio_bytes - - async def synthesize_to_file(self, text: str, output_path: Path) -> Path: - """Synthesize speech and save to file. - - Args: - text: Text to synthesize. - output_path: Path to save the audio file. - - Returns: - Path to the saved audio file. - """ - audio_data = await self.synthesize(text) - output_path.write_bytes(audio_data) - return output_path - - def set_voice(self, voice_name: str) -> None: - """Set the voice by name or full identifier. - - Args: - voice_name: Short name (e.g., "xiaoxiao") or full name. - """ - if voice_name in self.available_voices: - self.voice = self.available_voices[voice_name] - else: - self.voice = voice_name - - def set_rate(self, rate: str) -> None: - """Set the speech rate. - - Args: - rate: Rate adjustment (e.g., "-10%", "+20%"). - """ - self.rate = rate - - def _get_cache_key(self, text: str) -> str: - """Generate a cache key for the given text.""" - content = f"{text}:{self.voice}:{self.rate}" - return hashlib.md5(content.encode()).hexdigest() - - def clear_cache(self) -> int: - """Clear the TTS cache. - - Returns: - Number of files deleted. - """ - count = 0 - for file in self.cache_dir.glob("*.mp3"): - file.unlink() - count += 1 - return count - - @staticmethod - async def list_voices(language: str = "zh") -> list[dict]: - """List available voices for a language. - - Args: - language: Language code (e.g., "zh", "en"). - - Returns: - List of voice information dicts. - """ - voices = await edge_tts.list_voices() - filtered = [v for v in voices if v["Locale"].startswith(language)] - return [ - { - "name": v["ShortName"], - "gender": v["Gender"], - "locale": v["Locale"], - } - for v in filtered - ] - - -class TTSQueue: - """Manages a queue of TTS requests to prevent overlap.""" - - def __init__(self, engine: TTSEngine) -> None: - """Initialize the TTS queue. - - Args: - engine: TTSEngine instance to use. - """ - self.engine = engine - self._queue: asyncio.Queue[str] = asyncio.Queue() - self._current_audio: bytes | None = None - self._is_speaking = False - self._task: asyncio.Task | None = None - - async def start(self) -> None: - """Start processing the queue.""" - self._task = asyncio.create_task(self._process_queue()) - - async def stop(self) -> None: - """Stop processing the queue.""" - if self._task: - self._task.cancel() - try: - await self._task - except asyncio.CancelledError: - pass - - async def speak(self, text: str, priority: bool = False) -> None: - """Add text to the speech queue. - - Args: - text: Text to speak. - priority: If True, clears queue and speaks immediately. - """ - if priority: - # Clear existing queue - while not self._queue.empty(): - try: - self._queue.get_nowait() - except asyncio.QueueEmpty: - break - - await self._queue.put(text) - - async def _process_queue(self) -> None: - """Process items in the queue.""" - while True: - text = await self._queue.get() - self._is_speaking = True - - try: - self._current_audio = await self.engine.synthesize(text) - # In a real implementation, you would play the audio here - # For now, we just store it for retrieval via WebSocket - except Exception: - pass - finally: - self._is_speaking = False - self._queue.task_done() - - def get_current_audio(self) -> bytes | None: - """Get the most recently synthesized audio.""" - return self._current_audio - - @property - def is_speaking(self) -> bool: - """Check if currently speaking.""" - return self._is_speaking - - -def create_tts_engine(**kwargs) -> TTSEngine: - """Factory function to create a TTSEngine.""" - return TTSEngine(**kwargs) - - -def create_tts_queue(engine: TTSEngine | None = None) -> TTSQueue: - """Factory function to create a TTSQueue.""" - if engine is None: - engine = create_tts_engine() - return TTSQueue(engine) diff --git a/backend/app/services/coach/models.py b/backend/app/services/coach/models.py deleted file mode 100644 index 5a2a5d5f..00000000 --- a/backend/app/services/coach/models.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Coach module database models.""" - -import json -from datetime import datetime -from typing import Any, cast - -from sqlalchemy import DateTime, ForeignKey, String, Text -from sqlalchemy.orm import Mapped, mapped_column - -from app.core.database import Base - - -def generate_uuid() -> str: - """Generate a UUID string.""" - import uuid - - return str(uuid.uuid4()) - - -class CoachProgress(Base): - """Store coach progress data for authenticated users.""" - - __tablename__ = "coach_progress" - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - user_id: Mapped[str] = mapped_column( - String(36), ForeignKey("users.id", ondelete="CASCADE"), unique=True, index=True - ) - - # Progress data (JSON) - stores UserProgress structure - progress_data: Mapped[str | None] = mapped_column(Text, nullable=True) - - # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - updated_at: Mapped[datetime] = mapped_column( - DateTime, default=datetime.utcnow, onupdate=datetime.utcnow - ) - - def get_progress(self) -> dict[str, Any] | None: - """Get progress as dict.""" - if self.progress_data: - return cast(dict[str, Any], json.loads(self.progress_data)) - return None - - def set_progress(self, progress: dict[str, Any]) -> None: - """Set progress from dict.""" - self.progress_data = json.dumps(progress, ensure_ascii=False) diff --git a/backend/app/services/coach/pose/__init__.py b/backend/app/services/coach/pose/__init__.py deleted file mode 100644 index 79d0abf2..00000000 --- a/backend/app/services/coach/pose/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Pose detection module.""" diff --git a/backend/app/services/coach/pose/detector.py b/backend/app/services/coach/pose/detector.py deleted file mode 100644 index 35072690..00000000 --- a/backend/app/services/coach/pose/detector.py +++ /dev/null @@ -1,356 +0,0 @@ -"""MediaPipe-based pose detection module using the new Tasks API. - -Optimized for real-time performance with VIDEO mode tracking. -""" - -import asyncio -import os -import time -import urllib.request -from concurrent.futures import ThreadPoolExecutor -from pathlib import Path - -import cv2 -import mediapipe as mp -import numpy as np -from mediapipe.tasks import python -from mediapipe.tasks.python import vision -from numpy.typing import NDArray - -from app.core.config import get_settings -from app.schemas.pose import Point3D, PoseData - -settings = get_settings() - -# Shared thread pool for CPU-bound pose detection (increased for better parallelism) -_pose_executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="pose_detect") - -# Model configuration - Use LITE for faster performance on low-end servers -# Set MEDIAPIPE_MODEL=lite to use lighter model -# Set DISABLE_MEDIAPIPE=true to completely disable pose detection - -_DISABLE_MEDIAPIPE = os.getenv("DISABLE_MEDIAPIPE", "").lower() in ("true", "1", "yes") -_USE_LITE_MODEL = os.getenv("MEDIAPIPE_MODEL", "lite").lower() == "lite" - -if _DISABLE_MEDIAPIPE: - print("[PoseDetector] MediaPipe is DISABLED via environment variable") - MODEL_URL = "" - MODEL_PATH = Path("/dev/null") - MODEL_PATH_LOCAL = Path("/dev/null") -elif _USE_LITE_MODEL: - MODEL_URL = "https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_lite/float16/1/pose_landmarker_lite.task" - MODEL_PATH = Path("/app/models/pose_landmarker_lite.task") - MODEL_PATH_LOCAL = Path("./models/pose_landmarker_lite.task") -else: - MODEL_URL = "https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_full/float16/1/pose_landmarker_full.task" - MODEL_PATH = Path("/app/models/pose_landmarker_full.task") - MODEL_PATH_LOCAL = Path("./models/pose_landmarker_full.task") - -# Pose connections for drawing (33 landmarks) -POSE_CONNECTIONS = [ - (0, 1), - (1, 2), - (2, 3), - (3, 7), # Face - (0, 4), - (4, 5), - (5, 6), - (6, 8), - (9, 10), # Mouth - (11, 12), # Shoulders - (11, 13), - (13, 15), - (15, 17), - (15, 19), - (15, 21), - (17, 19), # Left arm - (12, 14), - (14, 16), - (16, 18), - (16, 20), - (16, 22), - (18, 20), # Right arm - (11, 23), - (12, 24), - (23, 24), # Torso - (23, 25), - (25, 27), - (27, 29), - (27, 31), - (29, 31), # Left leg - (24, 26), - (26, 28), - (28, 30), - (28, 32), - (30, 32), # Right leg -] - - -def ensure_model_exists() -> Path: - """Download the pose landmarker model if it doesn't exist.""" - # Check Docker path first (pre-downloaded during build) - if MODEL_PATH.exists(): - return MODEL_PATH - - # Fallback to local path for development - if MODEL_PATH_LOCAL.exists(): - return MODEL_PATH_LOCAL - - # Download if neither exists - MODEL_PATH_LOCAL.parent.mkdir(parents=True, exist_ok=True) - print(f"Downloading pose landmarker model to {MODEL_PATH_LOCAL}...") - urllib.request.urlretrieve(MODEL_URL, MODEL_PATH_LOCAL) - print("Model downloaded successfully.") - - return MODEL_PATH_LOCAL - - -class PoseDetector: - """Wrapper for MediaPipe Pose Landmarker detection. - - Optimized with: - - LIVE_STREAM mode with async callbacks for real-time performance - - Optional frame downscaling for faster detection - - Async support via thread pool execution - """ - - def __init__( - self, - model_complexity: int | None = None, - min_detection_confidence: float | None = None, - min_tracking_confidence: float | None = None, - detection_scale: float = 0.75, # Increased from 0.5 for better detection - ) -> None: - """Initialize the pose detector with MediaPipe Tasks API. - - Args: - model_complexity: Not used in new API, kept for compatibility. - min_detection_confidence: Minimum confidence for detection. - min_tracking_confidence: Minimum confidence for tracking. - detection_scale: Scale factor for detection (0.5 = half resolution). - """ - self._initialized = False - self.landmarker = None - self._frame_count = 0 - self._start_time = time.time() - self._detection_scale = detection_scale - - # Skip initialization if disabled - if _DISABLE_MEDIAPIPE: - print("[PoseDetector] Skipping initialization (disabled)") - return - - try: - model_path = ensure_model_exists() - print(f"[PoseDetector] Loading model from: {model_path}") - - base_options = python.BaseOptions(model_asset_path=str(model_path)) - - # Use VIDEO mode for synchronous operation with tracking - options = vision.PoseLandmarkerOptions( - base_options=base_options, - running_mode=vision.RunningMode.VIDEO, - min_pose_detection_confidence=( - min_detection_confidence or settings.min_detection_confidence - ), - min_tracking_confidence=( - min_tracking_confidence or settings.min_tracking_confidence - ), - ) - - self.landmarker = vision.PoseLandmarker.create_from_options(options) - self._initialized = True - print("[PoseDetector] Initialized successfully") - except Exception as e: - print(f"[PoseDetector] ERROR: Failed to initialize MediaPipe: {e}") - import traceback - - traceback.print_exc() - - def detect(self, frame: NDArray[np.uint8]) -> PoseData | None: - """Detect pose in a single frame using VIDEO mode. - - VIDEO mode uses tracking for faster subsequent frame processing. - - Args: - frame: BGR image as numpy array. - - Returns: - PoseData if pose detected, None otherwise. - """ - # Return None if not initialized - if not self._initialized or self.landmarker is None: - return None - - try: - # Downscale frame for faster detection - if self._detection_scale < 1.0: - h, w = frame.shape[:2] - new_w = int(w * self._detection_scale) - new_h = int(h * self._detection_scale) - scaled_frame = cv2.resize( - frame, (new_w, new_h), interpolation=cv2.INTER_AREA - ) - else: - scaled_frame = frame - - # Convert BGR to RGB for MediaPipe - rgb_frame = cv2.cvtColor(scaled_frame, cv2.COLOR_BGR2RGB) - - # Create MediaPipe Image - mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb_frame) - - # Get timestamp in milliseconds (must be monotonically increasing) - timestamp_ms = int((time.time() - self._start_time) * 1000) - - # Detect pose with VIDEO mode (synchronous with tracking) - result = self.landmarker.detect_for_video(mp_image, timestamp_ms) - - if not result.pose_landmarks or len(result.pose_landmarks) == 0: - return None - - # Extract keypoints from the first detected pose - landmarks = result.pose_landmarks[0] - keypoints: dict[int, Point3D] = {} - - for idx, landmark in enumerate(landmarks): - keypoints[idx] = Point3D( - x=landmark.x, - y=landmark.y, - z=landmark.z, - visibility=landmark.visibility - if hasattr(landmark, "visibility") - else 1.0, - ) - - self._frame_count += 1 - timestamp = time.time() - self._start_time - - return PoseData( - keypoints=keypoints, - timestamp=timestamp, - frame_id=self._frame_count, - ) - except Exception as e: - print(f"[PoseDetector] Detection error: {e}") - return None - - async def detect_async(self, frame: NDArray[np.uint8]) -> PoseData | None: - """Detect pose asynchronously using thread pool. - - Args: - frame: BGR image as numpy array. - - Returns: - PoseData if pose detected, None otherwise. - """ - loop = asyncio.get_event_loop() - return await loop.run_in_executor(_pose_executor, self.detect, frame) - - def draw_landmarks( - self, - frame: NDArray[np.uint8], - pose_data: PoseData, - color: tuple[int, int, int] = (0, 255, 0), - ) -> NDArray[np.uint8]: - """Draw pose landmarks on the frame. - - Args: - frame: BGR image to draw on. - pose_data: Detected pose data. - color: BGR color for drawing. - - Returns: - Frame with landmarks drawn. - """ - frame_copy = frame.copy() - h, w = frame_copy.shape[:2] - - # Draw connections - for connection in POSE_CONNECTIONS: - start_idx, end_idx = connection - if start_idx in pose_data.keypoints and end_idx in pose_data.keypoints: - start_point = pose_data.keypoints[start_idx] - end_point = pose_data.keypoints[end_idx] - - vis_threshold = 0.5 - start_vis = getattr(start_point, "visibility", 1.0) - end_vis = getattr(end_point, "visibility", 1.0) - - if start_vis > vis_threshold and end_vis > vis_threshold: - start_px = (int(start_point.x * w), int(start_point.y * h)) - end_px = (int(end_point.x * w), int(end_point.y * h)) - cv2.line(frame_copy, start_px, end_px, color, 2) - - # Draw keypoints - for _idx, point in pose_data.keypoints.items(): - vis = getattr(point, "visibility", 1.0) - if vis > 0.5: - px = (int(point.x * w), int(point.y * h)) - cv2.circle(frame_copy, px, 5, color, -1) - cv2.circle(frame_copy, px, 7, (255, 255, 255), 1) - - return frame_copy - - async def draw_landmarks_async( - self, - frame: NDArray[np.uint8], - pose_data: PoseData, - color: tuple[int, int, int] = (0, 255, 0), - ) -> NDArray[np.uint8]: - """Draw pose landmarks asynchronously using thread pool.""" - loop = asyncio.get_event_loop() - return await loop.run_in_executor( - _pose_executor, self.draw_landmarks, frame, pose_data, color - ) - - def draw_feedback_overlay( - self, - frame: NDArray[np.uint8], - pose_data: PoseData, - deviations: dict[int, str], - ) -> NDArray[np.uint8]: - """Draw feedback overlay with color-coded keypoints. - - Args: - frame: BGR image to draw on. - pose_data: Detected pose data. - deviations: Dict mapping keypoint indices to deviation descriptions. - - Returns: - Frame with color-coded feedback overlay. - """ - frame_copy = frame.copy() - h, w = frame_copy.shape[:2] - - for idx, point in pose_data.keypoints.items(): - vis = getattr(point, "visibility", 1.0) - if vis > 0.5: - px = (int(point.x * w), int(point.y * h)) - - # Red for deviations, green for correct - if idx in deviations: - draw_color = (0, 0, 255) # Red (BGR) - radius = 10 - else: - draw_color = (0, 255, 0) # Green (BGR) - radius = 5 - - cv2.circle(frame_copy, px, radius, draw_color, -1) - cv2.circle(frame_copy, px, radius + 2, (255, 255, 255), 2) - - return frame_copy - - def close(self) -> None: - """Release resources.""" - if hasattr(self, "landmarker") and self.landmarker is not None: - try: - self.landmarker.close() - except Exception: - pass - - def __enter__(self) -> "PoseDetector": - return self - - def __exit__(self, *args: object) -> None: - self.close() diff --git a/backend/app/services/coach/pose/keypoints.py b/backend/app/services/coach/pose/keypoints.py deleted file mode 100644 index 5fede16c..00000000 --- a/backend/app/services/coach/pose/keypoints.py +++ /dev/null @@ -1,262 +0,0 @@ -"""Keypoint calculation utilities.""" - -import math - -from app.schemas.pose import Keypoint, Point3D, PoseData - - -def calculate_angle(p1: Point3D, p2: Point3D, p3: Point3D) -> float: - """Calculate the angle at p2 formed by p1-p2-p3. - - Args: - p1: First point. - p2: Vertex point (where the angle is measured). - p3: Third point. - - Returns: - Angle in degrees (0-180). - """ - # Create vectors - v1 = (p1.x - p2.x, p1.y - p2.y, p1.z - p2.z) - v2 = (p3.x - p2.x, p3.y - p2.y, p3.z - p2.z) - - # Calculate dot product and magnitudes - dot_product = v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2] - mag1 = math.sqrt(v1[0] ** 2 + v1[1] ** 2 + v1[2] ** 2) - mag2 = math.sqrt(v2[0] ** 2 + v2[1] ** 2 + v2[2] ** 2) - - if mag1 == 0 or mag2 == 0: - return 0.0 - - # Calculate angle - cos_angle = max(-1.0, min(1.0, dot_product / (mag1 * mag2))) - angle = math.degrees(math.acos(cos_angle)) - - return angle - - -def calculate_distance(p1: Point3D, p2: Point3D) -> float: - """Calculate Euclidean distance between two points. - - Args: - p1: First point. - p2: Second point. - - Returns: - Distance (in normalized coordinates). - """ - return math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2 + (p1.z - p2.z) ** 2) - - -def get_spine_alignment(pose: PoseData) -> float: - """Calculate spine alignment angle. - - Measures the deviation from vertical alignment. - - Returns: - Angle in degrees from vertical (0 = perfectly vertical). - """ - left_shoulder = pose.get_keypoint(Keypoint.LEFT_SHOULDER) - right_shoulder = pose.get_keypoint(Keypoint.RIGHT_SHOULDER) - left_hip = pose.get_keypoint(Keypoint.LEFT_HIP) - right_hip = pose.get_keypoint(Keypoint.RIGHT_HIP) - - if not all([left_shoulder, right_shoulder, left_hip, right_hip]): - return 0.0 - - # Type assertions after None check - assert left_shoulder is not None - assert right_shoulder is not None - assert left_hip is not None - assert right_hip is not None - - # Calculate midpoints - shoulder_mid_x = (left_shoulder.x + right_shoulder.x) / 2 - shoulder_mid_y = (left_shoulder.y + right_shoulder.y) / 2 - hip_mid_x = (left_hip.x + right_hip.x) / 2 - hip_mid_y = (left_hip.y + right_hip.y) / 2 - - # Calculate angle from vertical - dx = shoulder_mid_x - hip_mid_x - dy = shoulder_mid_y - hip_mid_y - - if dy == 0: - return 90.0 - - angle = math.degrees(math.atan(abs(dx) / abs(dy))) - return angle - - -def get_hip_angle(pose: PoseData, side: str = "left") -> float: - """Calculate hip flexion angle. - - Args: - pose: Pose data. - side: "left" or "right". - - Returns: - Hip angle in degrees. - """ - if side == "left": - shoulder = pose.get_keypoint(Keypoint.LEFT_SHOULDER) - hip = pose.get_keypoint(Keypoint.LEFT_HIP) - knee = pose.get_keypoint(Keypoint.LEFT_KNEE) - else: - shoulder = pose.get_keypoint(Keypoint.RIGHT_SHOULDER) - hip = pose.get_keypoint(Keypoint.RIGHT_HIP) - knee = pose.get_keypoint(Keypoint.RIGHT_KNEE) - - if not all([shoulder, hip, knee]): - return 0.0 - - # Type assertions after None check - assert shoulder is not None - assert hip is not None - assert knee is not None - - return calculate_angle(shoulder, hip, knee) - - -def get_knee_angle(pose: PoseData, side: str = "left") -> float: - """Calculate knee flexion angle. - - Args: - pose: Pose data. - side: "left" or "right". - - Returns: - Knee angle in degrees. - """ - if side == "left": - hip = pose.get_keypoint(Keypoint.LEFT_HIP) - knee = pose.get_keypoint(Keypoint.LEFT_KNEE) - ankle = pose.get_keypoint(Keypoint.LEFT_ANKLE) - else: - hip = pose.get_keypoint(Keypoint.RIGHT_HIP) - knee = pose.get_keypoint(Keypoint.RIGHT_KNEE) - ankle = pose.get_keypoint(Keypoint.RIGHT_ANKLE) - - if not all([hip, knee, ankle]): - return 0.0 - - # Type assertions after None check - assert hip is not None - assert knee is not None - assert ankle is not None - - return calculate_angle(hip, knee, ankle) - - -def get_shoulder_angle(pose: PoseData, side: str = "left") -> float: - """Calculate shoulder abduction angle. - - Args: - pose: Pose data. - side: "left" or "right". - - Returns: - Shoulder angle in degrees. - """ - if side == "left": - hip = pose.get_keypoint(Keypoint.LEFT_HIP) - shoulder = pose.get_keypoint(Keypoint.LEFT_SHOULDER) - elbow = pose.get_keypoint(Keypoint.LEFT_ELBOW) - else: - hip = pose.get_keypoint(Keypoint.RIGHT_HIP) - shoulder = pose.get_keypoint(Keypoint.RIGHT_SHOULDER) - elbow = pose.get_keypoint(Keypoint.RIGHT_ELBOW) - - if not all([hip, shoulder, elbow]): - return 0.0 - - # Type assertions after None check - assert hip is not None - assert shoulder is not None - assert elbow is not None - - return calculate_angle(hip, shoulder, elbow) - - -def get_pelvic_tilt(pose: PoseData) -> float: - """Calculate pelvic tilt angle. - - Positive = anterior tilt, Negative = posterior tilt. - - Returns: - Pelvic tilt angle in degrees. - """ - left_hip = pose.get_keypoint(Keypoint.LEFT_HIP) - right_hip = pose.get_keypoint(Keypoint.RIGHT_HIP) - - if not all([left_hip, right_hip]): - return 0.0 - - # Type assertions after None check - assert left_hip is not None - assert right_hip is not None - - # Calculate the angle of the hip line from horizontal - dx = right_hip.x - left_hip.x - dy = right_hip.y - left_hip.y - - if dx == 0: - return 0.0 - - angle = math.degrees(math.atan(dy / dx)) - return angle - - -def is_lying_down(pose: PoseData) -> bool: - """Detect if the person is lying down. - - Returns: - True if person appears to be lying down. - """ - left_shoulder = pose.get_keypoint(Keypoint.LEFT_SHOULDER) - left_hip = pose.get_keypoint(Keypoint.LEFT_HIP) - - if not all([left_shoulder, left_hip]): - return False - - # Type assertions after None check - assert left_shoulder is not None - assert left_hip is not None - - # If shoulder and hip are at similar heights, likely lying down - vertical_diff = abs(left_shoulder.y - left_hip.y) - return bool(vertical_diff < 0.15) - - -def get_body_symmetry(pose: PoseData) -> float: - """Calculate body symmetry score. - - Returns: - Symmetry score 0-1 (1 = perfectly symmetric). - """ - pairs = [ - (Keypoint.LEFT_SHOULDER, Keypoint.RIGHT_SHOULDER), - (Keypoint.LEFT_HIP, Keypoint.RIGHT_HIP), - (Keypoint.LEFT_KNEE, Keypoint.RIGHT_KNEE), - (Keypoint.LEFT_ANKLE, Keypoint.RIGHT_ANKLE), - ] - - total_diff = 0.0 - valid_pairs = 0 - - for left_kp, right_kp in pairs: - left = pose.get_keypoint(left_kp) - right = pose.get_keypoint(right_kp) - - if left and right and left.visibility > 0.5 and right.visibility > 0.5: - # Check if y-coordinates are similar (symmetric) - y_diff = abs(left.y - right.y) - total_diff += y_diff - valid_pairs += 1 - - if valid_pairs == 0: - return 1.0 - - avg_diff = total_diff / valid_pairs - # Convert to score (smaller diff = higher score) - symmetry = max(0.0, 1.0 - avg_diff * 5) - return symmetry diff --git a/backend/app/services/coach/progress/__init__.py b/backend/app/services/coach/progress/__init__.py deleted file mode 100644 index d5a00584..00000000 --- a/backend/app/services/coach/progress/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Progress tracking module.""" diff --git a/backend/app/services/coach/progress/achievements.py b/backend/app/services/coach/progress/achievements.py deleted file mode 100644 index 8bb33952..00000000 --- a/backend/app/services/coach/progress/achievements.py +++ /dev/null @@ -1,207 +0,0 @@ -"""Achievement system for gamification.""" - -from datetime import datetime - -from app.schemas.progress import ( - Achievement, - AchievementType, - SessionRecord, - UserProgress, -) - -# Pre-defined achievements -ACHIEVEMENT_DEFINITIONS: dict[AchievementType, dict] = { - AchievementType.FIRST_SESSION: { - "name": "第一步", - "description": "完成第一次康复训练", - "icon": "footprints", - }, - AchievementType.STREAK_3: { - "name": "坚持三天", - "description": "连续三天进行训练", - "icon": "fire", - }, - AchievementType.STREAK_7: { - "name": "一周达人", - "description": "连续七天进行训练", - "icon": "calendar-check", - }, - AchievementType.STREAK_30: { - "name": "月度冠军", - "description": "连续三十天进行训练", - "icon": "trophy", - }, - AchievementType.PERFECT_FORM: { - "name": "完美姿态", - "description": "在一次训练中获得90分以上的平均分", - "icon": "star", - }, - AchievementType.COMPLETE_EXERCISE: { - "name": "动作达人", - "description": "完成所有组数的单个动作", - "icon": "check-circle", - }, - AchievementType.COMPLETE_SESSION: { - "name": "训练完成", - "description": "完成一整套训练计划", - "icon": "medal", - }, - AchievementType.STRENGTH_MILESTONE: { - "name": "力量回归", - "description": "核心力量指标提升至50%", - "icon": "trending-up", - }, - AchievementType.CONSISTENCY: { - "name": "持之以恒", - "description": "累计完成20次训练", - "icon": "award", - }, -} - - -class AchievementSystem: - """Manages user achievements and badges.""" - - def __init__(self) -> None: - """Initialize the achievement system.""" - self._all_achievements = self._create_achievement_templates() - - def _create_achievement_templates(self) -> list[Achievement]: - """Create achievement templates from definitions.""" - return [ - Achievement( - id=f"achievement_{type_.value}", - type=type_, - name=info["name"], - description=info["description"], - icon=info["icon"], - ) - for type_, info in ACHIEVEMENT_DEFINITIONS.items() - ] - - def get_all_achievements(self) -> list[Achievement]: - """Get all possible achievements.""" - return self._all_achievements.copy() - - def check_achievements( - self, - progress: UserProgress, - session: SessionRecord | None = None, - ) -> list[Achievement]: - """Check for newly earned achievements. - - Args: - progress: Current user progress. - session: Just-completed session (if any). - - Returns: - List of newly earned achievements. - """ - newly_earned: list[Achievement] = [] - earned_types = {a.type for a in progress.achievements if a.is_earned} - - # Check each achievement type - for achievement in self._all_achievements: - if achievement.type in earned_types: - continue - - if self._check_achievement(achievement.type, progress, session): - achievement.is_earned = True - achievement.earned_at = datetime.now() - newly_earned.append(achievement) - - return newly_earned - - def _check_achievement( - self, - type_: AchievementType, - progress: UserProgress, - session: SessionRecord | None, - ) -> bool: - """Check if a specific achievement is earned. - - Args: - type_: Achievement type to check. - progress: User progress data. - session: Just-completed session. - - Returns: - True if achievement is earned. - """ - match type_: - case AchievementType.FIRST_SESSION: - return progress.total_sessions >= 1 - - case AchievementType.STREAK_3: - return progress.current_streak >= 3 - - case AchievementType.STREAK_7: - return progress.current_streak >= 7 - - case AchievementType.STREAK_30: - return progress.current_streak >= 30 - - case AchievementType.PERFECT_FORM: - if session and session.average_score >= 90: - return True - return False - - case AchievementType.COMPLETE_EXERCISE: - # Check if any exercise has been fully completed - for ex_progress in progress.exercise_progress.values(): - if ex_progress.total_sessions >= 1: - return True - return False - - case AchievementType.COMPLETE_SESSION: - return progress.total_sessions >= 1 - - case AchievementType.STRENGTH_MILESTONE: - for metric in progress.strength_metrics: - if metric.name == "core_strength" and metric.value >= 50: - return True - return False - - case AchievementType.CONSISTENCY: - return progress.total_sessions >= 20 - - case _: - return False - - def update_streak(self, progress: UserProgress) -> int: - """Update the user's streak based on last session date. - - Args: - progress: User progress to update. - - Returns: - Updated streak count. - """ - today = datetime.now().date() - - if progress.last_session_date is None: - # First session - progress.current_streak = 1 - else: - last_date = progress.last_session_date.date() - days_diff = (today - last_date).days - - if days_diff == 0: - # Same day, no change - pass - elif days_diff == 1: - # Consecutive day - progress.current_streak += 1 - else: - # Streak broken - progress.current_streak = 1 - - progress.longest_streak = max(progress.longest_streak, progress.current_streak) - progress.last_session_date = datetime.now() - - return progress.current_streak - - -def create_achievement_system() -> AchievementSystem: - """Factory function to create an AchievementSystem.""" - return AchievementSystem() diff --git a/backend/app/services/coach/progress/tracker.py b/backend/app/services/coach/progress/tracker.py deleted file mode 100644 index 98a1ebac..00000000 --- a/backend/app/services/coach/progress/tracker.py +++ /dev/null @@ -1,196 +0,0 @@ -"""Progress tracking and metrics management.""" - -from datetime import datetime - -from app.schemas.progress import ( - Achievement, - ExerciseProgress, - SessionRecord, - StrengthMetric, - UserProgress, -) -from app.services.coach.progress.achievements import ( - create_achievement_system, -) - - -class ProgressTracker: - """Tracks user progress and manages metrics.""" - - def __init__(self) -> None: - """Initialize the progress tracker.""" - self._achievement_system = create_achievement_system() - self._progress_cache: dict[str, UserProgress] = {} - - def get_or_create_progress(self, user_id: str) -> UserProgress: - """Get or create progress for a user. - - Args: - user_id: User identifier. - - Returns: - User's progress data. - """ - if user_id not in self._progress_cache: - # In production, this would load from database - self._progress_cache[user_id] = UserProgress( - user_id=user_id, - strength_metrics=self._create_default_metrics(), - achievements=self._achievement_system.get_all_achievements(), - ) - return self._progress_cache[user_id] - - def record_session( - self, - user_id: str, - session: SessionRecord, - ) -> tuple[UserProgress, list[Achievement]]: - """Record a completed session and update progress. - - Args: - user_id: User identifier. - session: Completed session record. - - Returns: - Tuple of (updated progress, newly earned achievements). - """ - progress = self.get_or_create_progress(user_id) - - # Update basic stats - progress.total_sessions += 1 - progress.total_minutes += session.duration_seconds / 60 - - # Update streak - self._achievement_system.update_streak(progress) - - # Update exercise-specific progress - ex_progress = progress.exercise_progress.get( - session.exercise_id, - ExerciseProgress(exercise_id=session.exercise_id), - ) - ex_progress.total_sessions += 1 - ex_progress.total_reps += session.completed_reps - ex_progress.best_score = max(ex_progress.best_score, session.average_score) - - # Update running average - if ex_progress.total_sessions == 1: - ex_progress.average_score = session.average_score - else: - ex_progress.average_score = ( - ex_progress.average_score * (ex_progress.total_sessions - 1) - + session.average_score - ) / ex_progress.total_sessions - - ex_progress.last_performed = datetime.now() - progress.exercise_progress[session.exercise_id] = ex_progress - - # Update strength metrics based on performance - self._update_strength_metrics(progress, session) - - # Check for new achievements - new_achievements = self._achievement_system.check_achievements( - progress, session - ) - - # Mark achievements as earned in progress - for achievement in new_achievements: - for prog_achievement in progress.achievements: - if prog_achievement.type == achievement.type: - prog_achievement.is_earned = True - prog_achievement.earned_at = achievement.earned_at - - return progress, new_achievements - - def _update_strength_metrics( - self, - progress: UserProgress, - session: SessionRecord, - ) -> None: - """Update strength metrics based on session performance. - - Args: - progress: User progress to update. - session: Completed session. - """ - # Simple model: improve metrics based on session score and exercise type - score_factor = session.average_score / 100 - - for metric in progress.strength_metrics: - # Small improvement per session, scaled by score - improvement = 0.5 * score_factor - - # Apply improvement with diminishing returns - current = metric.value - remaining = metric.target - current - actual_improvement = improvement * (remaining / metric.target) - - metric.value = min(metric.target, current + actual_improvement) - - def _create_default_metrics(self) -> list[StrengthMetric]: - """Create default strength metrics for a new user.""" - return [ - StrengthMetric( - name="core_strength", - value=10, - baseline=10, - target=100, - unit="%", - ), - StrengthMetric( - name="pelvic_floor", - value=15, - baseline=15, - target=100, - unit="%", - ), - StrengthMetric( - name="posture", - value=20, - baseline=20, - target=100, - unit="%", - ), - StrengthMetric( - name="flexibility", - value=25, - baseline=25, - target=100, - unit="%", - ), - ] - - def get_summary(self, user_id: str) -> dict: - """Get a summary of user progress for display. - - Args: - user_id: User identifier. - - Returns: - Summary dict for UI display. - """ - progress = self.get_or_create_progress(user_id) - - earned_achievements = [a for a in progress.achievements if a.is_earned] - - return { - "total_sessions": progress.total_sessions, - "total_minutes": round(progress.total_minutes, 1), - "current_streak": progress.current_streak, - "longest_streak": progress.longest_streak, - "achievements_earned": len(earned_achievements), - "achievements_total": len(progress.achievements), - "strength_metrics": { - m.name: { - "value": round(m.value, 1), - "progress": round( - (m.value - m.baseline) / (m.target - m.baseline) * 100, 1 - ), - } - for m in progress.strength_metrics - }, - } - - -def create_progress_tracker() -> ProgressTracker: - """Factory function to create a ProgressTracker.""" - return ProgressTracker() diff --git a/backend/app/services/coach/workflow/__init__.py b/backend/app/services/coach/workflow/__init__.py deleted file mode 100644 index 7c367fe7..00000000 --- a/backend/app/services/coach/workflow/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Workflow module.""" diff --git a/backend/app/services/coach/workflow/graph.py b/backend/app/services/coach/workflow/graph.py deleted file mode 100644 index 3edcac6c..00000000 --- a/backend/app/services/coach/workflow/graph.py +++ /dev/null @@ -1,275 +0,0 @@ -"""Main LangGraph workflow for the recovery coach. - -This module implements a simplified workflow without LangGraph state machine -to avoid compatibility issues with Pydantic models. It provides the same -interface while maintaining the node-based architecture. -""" - -import uuid - -import numpy as np -from numpy.typing import NDArray - -from app.services.coach.exercises.library import get_exercise -from app.services.coach.workflow.nodes.analyze import create_analyze_node -from app.services.coach.workflow.nodes.detect import create_detect_node -from app.services.coach.workflow.nodes.feedback import ( - create_feedback_node, -) -from app.services.coach.workflow.nodes.track import create_track_node -from app.services.coach.workflow.state import CoachState, SessionState - - -class CoachWorkflow: - """Coaching workflow that processes frames through a pipeline. - - This class manages the coaching session state and processes frames - through detection, analysis, feedback, and tracking nodes. - - Optimized: Only runs full analysis every N frames for lower latency. - """ - - def __init__(self, use_llm: bool = True) -> None: - """Initialize the workflow. - - Args: - use_llm: Whether to use LLM for feedback generation. - """ - # Create nodes (lazy initialization) - self._detect_node = create_detect_node() - self._analyze_node = create_analyze_node() - self._feedback_node = create_feedback_node(use_llm=use_llm) - self._track_node = create_track_node() - - # Current state - self._state: CoachState | None = None - self._current_frame: NDArray[np.uint8] | None = None - - # Optimization: only analyze every N frames - self._frame_counter = 0 - self._analyze_every_n_frames = 2 # Analyze every 2nd frame (increased from 3) - - async def _process_pipeline(self, frame: NDArray[np.uint8]) -> CoachState: - """Process a frame through the full pipeline. - - Optimized: Only runs analysis/feedback/tracking every N frames. - - Args: - frame: Video frame to process. - - Returns: - Updated state after processing. - """ - if self._state is None: - raise RuntimeError("No active session") - - self._frame_counter += 1 - - # Step 1: Always detect pose - self._state = await self._detect_node(self._state, frame) - - # Steps 2-4: Only run every N frames to reduce latency - if self._frame_counter % self._analyze_every_n_frames == 0: - # Step 2: Analyze pose - self._state = await self._analyze_node(self._state) - - # Step 3: Generate feedback (runs in background, non-blocking) - self._state = await self._feedback_node(self._state) - - # Step 4: Track progress - self._state = await self._track_node(self._state) - - return self._state - - def start_session(self, exercise_id: str) -> CoachState: - """Start a new coaching session. - - Args: - exercise_id: ID of the exercise to perform. - - Returns: - Initial state. - """ - exercise = get_exercise(exercise_id) - if exercise is None: - raise ValueError(f"Unknown exercise: {exercise_id}") - - self._state = CoachState( - session_id=str(uuid.uuid4()), - session_state=SessionState.PREPARING, - current_exercise=exercise, - current_phase_index=0, - current_set=1, - current_rep=1, - ) - - self._analyze_node.start_session() - self._track_node.reset() - - return self._state - - def start_exercise(self) -> CoachState | None: - """Begin the exercise after preparation. - - Returns: - Updated state. - """ - if self._state is None: - return None - - self._state.session_state = SessionState.EXERCISING - return self._state - - async def process_frame( - self, frame: NDArray[np.uint8] - ) -> tuple[CoachState, NDArray[np.uint8]]: - """Process a single video frame through the workflow. - - Args: - frame: Video frame to process. - - Returns: - Tuple of (updated state, annotated frame). - """ - if self._state is None: - raise RuntimeError("No active session. Call start_session first.") - - if self._state.session_state != SessionState.EXERCISING: - # Not exercising, just return current state - return self._state, frame - - try: - # Process frame through the pipeline - self._state = await self._process_pipeline(frame) - - # Get annotated frame (async for non-blocking) - annotated = await self._detect_node.get_annotated_frame_async( - frame, self._state - ) - - return self._state, annotated - except Exception as e: - # Log error and return current state without crashing - import logging - - logging.getLogger(__name__).error(f"Frame processing error: {e}") - return self._state, frame - - def pause(self) -> CoachState | None: - """Pause the current session. - - Returns: - Updated state. - """ - if self._state is None: - return None - - self._state.session_state = SessionState.PAUSED - self._state.is_paused = True - return self._state - - def resume(self) -> CoachState | None: - """Resume a paused session. - - Returns: - Updated state. - """ - if self._state is None: - return None - - self._state.session_state = SessionState.EXERCISING - self._state.is_paused = False - return self._state - - def rest(self) -> CoachState | None: - """Enter rest state. - - Returns: - Updated state. - """ - if self._state is None: - return None - - self._state.session_state = SessionState.RESTING - self._analyze_node.record_rest() - return self._state - - def end_rest(self) -> CoachState | None: - """End rest and resume exercising. - - Returns: - Updated state. - """ - if self._state is None: - return None - - self._state.session_state = SessionState.EXERCISING - self._state.should_rest = False - return self._state - - def end_session(self) -> dict: - """End the current session and get summary. - - Returns: - Session summary statistics. - """ - if self._state is None: - return {} - - self._state.session_state = SessionState.COMPLETED - - summary = { - "session_id": self._state.session_id, - "exercise": self._state.current_exercise.name - if self._state.current_exercise - else None, - "average_score": self._state.get_average_score(), - "total_frames": self._state.total_frames_analyzed, - "completed_sets": self._state.current_set - 1, - "completed_reps": (self._state.current_set - 1) - * ( - self._state.current_exercise.repetitions - if self._state.current_exercise - else 0 - ) - + self._state.current_rep - - 1, - **self._analyze_node.get_session_stats(), - } - - # Cleanup - self._detect_node.close() - self._state = None - - return summary - - def get_state(self) -> CoachState | None: - """Get current workflow state. - - Returns: - Current state or None if no active session. - """ - return self._state - - async def get_speech_audio(self) -> bytes | None: - """Get audio for pending feedback. - - Returns: - Audio bytes or None. - """ - if self._state is None or self._state.pending_feedback is None: - return None - - return await self._feedback_node.synthesize_speech(self._state.pending_feedback) - - -def create_workflow(use_llm: bool = True) -> CoachWorkflow: - """Factory function to create a CoachWorkflow. - - Args: - use_llm: Whether to use LLM for feedback. - - Returns: - CoachWorkflow instance. - """ - return CoachWorkflow(use_llm=use_llm) diff --git a/backend/app/services/coach/workflow/nodes/__init__.py b/backend/app/services/coach/workflow/nodes/__init__.py deleted file mode 100644 index 4e08cfa4..00000000 --- a/backend/app/services/coach/workflow/nodes/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Workflow nodes module.""" diff --git a/backend/app/services/coach/workflow/nodes/analyze.py b/backend/app/services/coach/workflow/nodes/analyze.py deleted file mode 100644 index 84d8a3a5..00000000 --- a/backend/app/services/coach/workflow/nodes/analyze.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Analysis node for the workflow.""" - -from app.services.coach.analysis.posture import PostureAnalyzer -from app.services.coach.analysis.safety import SafetyMonitor -from app.services.coach.workflow.state import CoachState, SessionState - - -class AnalyzeNode: - """Node that analyzes pose data against exercise requirements.""" - - def __init__(self) -> None: - """Initialize the analysis node.""" - self._posture_analyzer = PostureAnalyzer() - self._safety_monitor = SafetyMonitor() - - async def __call__(self, state: CoachState) -> CoachState: - """Analyze current pose and update state. - - Args: - state: Current workflow state. - - Returns: - Updated state with analysis results. - """ - # Skip if not exercising or no pose data - if state.session_state != SessionState.EXERCISING: - return state - - if state.current_pose is None or state.current_exercise is None: - return state - - current_phase = state.get_current_phase() - if current_phase is None: - return state - - # Perform posture analysis - analysis = self._posture_analyzer.analyze( - pose=state.current_pose, - exercise=state.current_exercise, - current_phase=current_phase, - ) - state.analysis_result = analysis - - # Perform safety check - safety_status = self._safety_monitor.check( - pose=state.current_pose, - analysis=analysis, - ) - state.safety_alerts = safety_status.alerts - state.should_rest = safety_status.should_rest - - # Track scores for rep completion - if analysis.score > 0: - state.rep_scores.append(analysis.score) - - return state - - def start_session(self) -> None: - """Initialize monitors for a new session.""" - self._safety_monitor.start_session() - - def record_rest(self) -> None: - """Record that user took a rest.""" - self._safety_monitor.record_rest() - - def get_session_stats(self) -> dict: - """Get session statistics from safety monitor.""" - return self._safety_monitor.get_session_stats() - - -def create_analyze_node() -> AnalyzeNode: - """Factory function for AnalyzeNode.""" - return AnalyzeNode() diff --git a/backend/app/services/coach/workflow/nodes/detect.py b/backend/app/services/coach/workflow/nodes/detect.py deleted file mode 100644 index 55d7e00e..00000000 --- a/backend/app/services/coach/workflow/nodes/detect.py +++ /dev/null @@ -1,148 +0,0 @@ -"""Pose detection node for the workflow. - -Uses async pose detection with thread pool for non-blocking operation. -""" - -import numpy as np -from numpy.typing import NDArray - -from app.services.coach.pose.detector import PoseDetector -from app.services.coach.workflow.state import CoachState - - -class DetectNode: - """Node that handles pose detection from video frames. - - Uses async detection to avoid blocking the event loop. - """ - - def __init__(self, detection_scale: float = 0.5) -> None: - """Initialize the detection node. - - Args: - detection_scale: Scale factor for detection (0.5 = half resolution). - """ - self._detector: PoseDetector | None = None - self._detection_scale = detection_scale - - def _ensure_detector(self) -> PoseDetector: - """Ensure detector is initialized.""" - if self._detector is None: - self._detector = PoseDetector(detection_scale=self._detection_scale) - return self._detector - - async def __call__( - self, - state: CoachState, - frame: NDArray[np.uint8] | None = None, - ) -> CoachState: - """Process a frame and update state with pose data. - - Args: - state: Current workflow state. - frame: Video frame to process (injected externally). - - Returns: - Updated state with pose data. - """ - if frame is None: - return state - - detector = self._ensure_detector() - - # Use async detection to avoid blocking the event loop - pose_data = await detector.detect_async(frame) - - if pose_data is None: - # No pose detected, keep previous data - return state - - # Update state - state.current_pose = pose_data - state.total_frames_analyzed += 1 - - # Maintain pose history (for movement analysis) - state.pose_history.append(pose_data) - if len(state.pose_history) > 30: - state.pose_history = state.pose_history[-30:] - - return state - - async def get_annotated_frame_async( - self, - frame: NDArray[np.uint8], - state: CoachState, - ) -> NDArray[np.uint8]: - """Get frame with pose annotations (async version). - - Args: - frame: Original frame. - state: Current state with pose data. - - Returns: - Annotated frame. - """ - if state.current_pose is None: - return frame - - detector = self._ensure_detector() - - # Determine color based on analysis - if state.analysis_result: - if state.analysis_result.is_correct: - color = (0, 255, 0) # Green for correct - elif state.analysis_result.score >= 60: - color = (0, 255, 255) # Yellow for minor issues - else: - color = (0, 0, 255) # Red for significant issues - else: - color = (255, 255, 255) # White if no analysis yet - - return await detector.draw_landmarks_async(frame, state.current_pose, color) - - def get_annotated_frame( - self, - frame: NDArray[np.uint8], - state: CoachState, - ) -> NDArray[np.uint8]: - """Get frame with pose annotations (sync version for compatibility). - - Args: - frame: Original frame. - state: Current state with pose data. - - Returns: - Annotated frame. - """ - if state.current_pose is None: - return frame - - detector = self._ensure_detector() - - # Determine color based on analysis - if state.analysis_result: - if state.analysis_result.is_correct: - color = (0, 255, 0) # Green for correct - elif state.analysis_result.score >= 60: - color = (0, 255, 255) # Yellow for minor issues - else: - color = (0, 0, 255) # Red for significant issues - else: - color = (255, 255, 255) # White if no analysis yet - - return detector.draw_landmarks(frame, state.current_pose, color) - - def close(self) -> None: - """Release detector resources.""" - if self._detector: - self._detector.close() - self._detector = None - - -def create_detect_node(detection_scale: float = 0.5) -> DetectNode: - """Factory function for DetectNode. - - Args: - detection_scale: Scale factor for detection (0.5 = half resolution). - """ - return DetectNode(detection_scale=detection_scale) diff --git a/backend/app/services/coach/workflow/nodes/feedback.py b/backend/app/services/coach/workflow/nodes/feedback.py deleted file mode 100644 index 11a49308..00000000 --- a/backend/app/services/coach/workflow/nodes/feedback.py +++ /dev/null @@ -1,170 +0,0 @@ -"""Feedback generation node for the workflow.""" - -import asyncio - -from app.schemas.feedback import FeedbackMessage, FeedbackType -from app.services.coach.feedback.generator import FeedbackGenerator -from app.services.coach.feedback.tts import TTSEngine, TTSQueue -from app.services.coach.workflow.state import CoachState, SessionState - - -class FeedbackNode: - """Node that generates and manages feedback.""" - - def __init__(self, use_llm: bool = True) -> None: - """Initialize the feedback node. - - Args: - use_llm: Whether to use LLM for dynamic feedback. - """ - self._generator = FeedbackGenerator(use_llm=use_llm) - self._tts_engine = TTSEngine() - self._tts_queue = TTSQueue(self._tts_engine) - self._last_feedback_time: float = 0 - self._min_feedback_interval: float = ( - 6.0 # 6 seconds between feedback for performance - ) - # Background task for non-blocking feedback generation - self._pending_generation: asyncio.Task[FeedbackMessage | None] | None = None - self._pending_feedback_result: FeedbackMessage | None = None - - async def __call__(self, state: CoachState) -> CoachState: - """Generate feedback based on current state. - - Non-blocking: LLM calls run in background, results delivered on next frame. - - Args: - state: Current workflow state. - - Returns: - Updated state with feedback. - """ - import time - - current_time = time.time() - - # Check if background generation completed - if self._pending_generation is not None and self._pending_generation.done(): - try: - self._pending_feedback_result = self._pending_generation.result() - except Exception as e: - print(f"[FEEDBACK] Background generation failed: {e}") - self._pending_feedback_result = None - self._pending_generation = None - - # Apply pending feedback result if available - if self._pending_feedback_result is not None: - state.pending_feedback = self._pending_feedback_result - state.should_speak = self._pending_feedback_result.should_speak - state.feedback_history.append(self._pending_feedback_result) - if len(state.feedback_history) > 50: - state.feedback_history = state.feedback_history[-50:] - self._pending_feedback_result = None - - # Rate limit feedback generation - if current_time - self._last_feedback_time < self._min_feedback_interval: - return state - - # Skip if not exercising - if state.session_state != SessionState.EXERCISING: - return state - - if state.current_exercise is None or state.analysis_result is None: - return state - - current_phase = state.get_current_phase() - if current_phase is None: - return state - - # Skip if already generating - if self._pending_generation is not None and not self._pending_generation.done(): - return state - - # Start background generation (non-blocking) - self._last_feedback_time = current_time - - # Capture values for the background task (mypy can't infer they're not None) - analysis = state.analysis_result - exercise = state.current_exercise - phase = current_phase - safety_alerts = state.safety_alerts if state.safety_alerts else None - - async def generate_in_background() -> FeedbackMessage | None: - try: - return await self._generator.generate( - analysis=analysis, - exercise=exercise, - phase=phase, - safety_alerts=safety_alerts, - ) - except Exception as e: - print(f"[FEEDBACK] Generation error: {e}") - return None - - self._pending_generation = asyncio.create_task(generate_in_background()) - - return state - - async def synthesize_speech(self, feedback: FeedbackMessage) -> bytes | None: - """Synthesize speech for a feedback message. - - Args: - feedback: Feedback message to speak. - - Returns: - Audio bytes or None if synthesis failed. - """ - if not feedback.should_speak: - return None - - try: - return await self._tts_engine.synthesize(feedback.text) - except Exception: - return None - - async def speak(self, text: str, priority: bool = False) -> None: - """Add text to the speech queue. - - Args: - text: Text to speak. - priority: Whether to prioritize this message. - """ - await self._tts_queue.speak(text, priority=priority) - - def generate_completion_feedback( - self, - state: CoachState, - ) -> FeedbackMessage: - """Generate completion feedback for an exercise. - - Args: - state: Current state with exercise info. - - Returns: - Completion feedback message. - """ - if state.current_exercise is None: - return FeedbackMessage( - type=FeedbackType.COMPLETION, - text="训练完成", - priority=2, - should_speak=True, - ) - - return self._generator.generate_completion_message( - exercise=state.current_exercise, - average_score=state.get_average_score(), - ) - - async def start(self) -> None: - """Start the TTS queue processing.""" - await self._tts_queue.start() - - async def stop(self) -> None: - """Stop the TTS queue processing.""" - await self._tts_queue.stop() - - -def create_feedback_node(use_llm: bool = True) -> FeedbackNode: - """Factory function for FeedbackNode.""" - return FeedbackNode(use_llm=use_llm) diff --git a/backend/app/services/coach/workflow/nodes/track.py b/backend/app/services/coach/workflow/nodes/track.py deleted file mode 100644 index ffc27147..00000000 --- a/backend/app/services/coach/workflow/nodes/track.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Progress tracking node for the workflow.""" - -import time - -from app.services.coach.workflow.state import CoachState, SessionState - - -class TrackNode: - """Node that tracks exercise progress and phase timing.""" - - def __init__(self) -> None: - """Initialize the tracking node.""" - self._phase_start_time: float = 0 - self._last_phase_index: int = -1 - self._last_rep: int = 0 - self._last_set: int = 0 - - async def __call__(self, state: CoachState) -> CoachState: - """Track progress and manage phase transitions. - - Args: - state: Current workflow state. - - Returns: - Updated state with progress tracking. - """ - if state.session_state != SessionState.EXERCISING: - return state - - if state.current_exercise is None: - return state - - current_time = time.time() - - # Initialize phase timing if new phase - if state.current_phase_index != self._last_phase_index: - self._phase_start_time = current_time - state.phase_start_time = current_time - self._last_phase_index = state.current_phase_index - - # Check if current phase should end (based on time) - current_phase = state.get_current_phase() - if current_phase is not None: - phase_elapsed = current_time - self._phase_start_time - if phase_elapsed >= current_phase.duration_seconds: - # Phase complete, advance - state.advance_phase() - self._phase_start_time = current_time - state.phase_start_time = current_time - - # Track rep/set changes - if state.current_rep != self._last_rep or state.current_set != self._last_set: - self._last_rep = state.current_rep - self._last_set = state.current_set - # Could emit events here for UI updates - - return state - - def get_phase_remaining_time(self, state: CoachState) -> float: - """Get remaining time in current phase. - - Args: - state: Current state. - - Returns: - Remaining seconds in current phase. - """ - current_phase = state.get_current_phase() - if current_phase is None: - return 0.0 - - elapsed = time.time() - self._phase_start_time - remaining = current_phase.duration_seconds - elapsed - return max(0.0, remaining) - - def reset(self) -> None: - """Reset tracking state.""" - self._phase_start_time = 0 - self._last_phase_index = -1 - self._last_rep = 0 - self._last_set = 0 - - -def create_track_node() -> TrackNode: - """Factory function for TrackNode.""" - return TrackNode() diff --git a/backend/app/services/coach/workflow/state.py b/backend/app/services/coach/workflow/state.py deleted file mode 100644 index abe40eda..00000000 --- a/backend/app/services/coach/workflow/state.py +++ /dev/null @@ -1,149 +0,0 @@ -"""State definitions for the coach workflow.""" - -from enum import Enum - -from pydantic import BaseModel, Field - -from app.schemas.exercise import Exercise, PhaseRequirement -from app.schemas.feedback import FeedbackMessage -from app.schemas.pose import PoseAnalysisResult, PoseData -from app.services.coach.analysis.safety import SafetyAlert - - -class SessionState(str, Enum): - """States of a coaching session.""" - - IDLE = "idle" - PREPARING = "preparing" - EXERCISING = "exercising" - RESTING = "resting" - COMPLETED = "completed" - PAUSED = "paused" - - -class CoachState(BaseModel): - """State for the LangGraph coaching workflow. - - This represents the complete state of an exercise coaching session, - passed between nodes in the workflow graph. - """ - - # Session info - session_id: str = Field(default="", description="Unique session identifier") - session_state: SessionState = Field(default=SessionState.IDLE) - - # Current exercise context - current_exercise: Exercise | None = Field(default=None) - current_phase_index: int = Field(default=0) - current_set: int = Field(default=1) - current_rep: int = Field(default=1) - phase_start_time: float = Field(default=0.0) - - # Pose data - current_pose: PoseData | None = Field(default=None) - pose_history: list[PoseData] = Field(default_factory=list) - - # Analysis results - analysis_result: PoseAnalysisResult | None = Field(default=None) - safety_alerts: list[SafetyAlert] = Field(default_factory=list) - - # Feedback - pending_feedback: FeedbackMessage | None = Field(default=None) - feedback_history: list[FeedbackMessage] = Field(default_factory=list) - - # Metrics - rep_scores: list[float] = Field(default_factory=list) - total_frames_analyzed: int = Field(default=0) - - # Control flags - should_speak: bool = Field(default=False) - should_rest: bool = Field(default=False) - is_paused: bool = Field(default=False) - - # Error handling - error_message: str | None = Field(default=None) - - def get_current_phase(self) -> PhaseRequirement | None: - """Get the current exercise phase.""" - if not self.current_exercise: - return None - if self.current_phase_index >= len(self.current_exercise.phases): - return None - return self.current_exercise.phases[self.current_phase_index] - - def advance_phase(self) -> bool: - """Advance to the next phase. - - Returns: - True if advanced, False if no more phases. - """ - if not self.current_exercise: - return False - - self.current_phase_index += 1 - if self.current_phase_index >= len(self.current_exercise.phases): - # Completed all phases, advance rep - self.current_phase_index = 0 - return self.advance_rep() - return True - - def advance_rep(self) -> bool: - """Advance to the next rep. - - Returns: - True if advanced, False if no more reps. - """ - if not self.current_exercise: - return False - - self.current_rep += 1 - if self.current_rep > self.current_exercise.repetitions: - # Completed all reps, advance set - self.current_rep = 1 - return self.advance_set() - return True - - def advance_set(self) -> bool: - """Advance to the next set. - - Returns: - True if advanced, False if no more sets. - """ - if not self.current_exercise: - return False - - self.current_set += 1 - if self.current_set > self.current_exercise.sets: - # Completed all sets - self.session_state = SessionState.COMPLETED - return False - return True - - def get_progress(self) -> dict: - """Get current progress information.""" - if not self.current_exercise: - return {"progress": 0.0} - - total_reps = self.current_exercise.repetitions * self.current_exercise.sets - completed_reps = ( - (self.current_set - 1) * self.current_exercise.repetitions - + self.current_rep - - 1 - ) - progress = completed_reps / total_reps if total_reps > 0 else 0.0 - - current_phase = self.get_current_phase() - return { - "progress": progress * 100, - "current_set": self.current_set, - "total_sets": self.current_exercise.sets, - "current_rep": self.current_rep, - "total_reps": self.current_exercise.repetitions, - "current_phase": current_phase.phase.value if current_phase else None, - } - - def get_average_score(self) -> float: - """Get average score for the session.""" - if not self.rep_scores: - return 0.0 - return sum(self.rep_scores) / len(self.rep_scores) diff --git a/backend/app/services/community/__init__.py b/backend/app/services/community/__init__.py deleted file mode 100644 index 9409df4a..00000000 --- a/backend/app/services/community/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Community module for MomShell.""" - -from .enums import ( - PROFESSIONAL_ROLES, - CertificationStatus, - ChannelType, - ContentStatus, - ModerationResult, - SensitiveCategory, - UserRole, -) -from .router import router as community_router -from .service import CommunityService, get_community_service - -__all__ = [ - # Router - "community_router", - # Service - "CommunityService", - "get_community_service", - # Enums - "UserRole", - "ChannelType", - "CertificationStatus", - "ContentStatus", - "ModerationResult", - "SensitiveCategory", - "PROFESSIONAL_ROLES", -] diff --git a/backend/app/services/community/ai_reply.py b/backend/app/services/community/ai_reply.py deleted file mode 100644 index a12de33a..00000000 --- a/backend/app/services/community/ai_reply.py +++ /dev/null @@ -1,1194 +0,0 @@ -"""AI auto-reply service for community posts.""" - -import asyncio -import logging -from typing import Any - -from openai import OpenAI -from sqlalchemy import select, update -from sqlalchemy.ext.asyncio import AsyncSession - -from app.core.config import get_settings -from app.core.database import async_session_maker -from app.services.verification import get_cove_service - -from .enums import ContentStatus, UserRole -from .models import Answer, Comment, Question, User - -logger = logging.getLogger(__name__) - -# AI Assistant account info -AI_USERNAME = "momshell_ai" -AI_EMAIL = "ai@momshell.local" -AI_NICKNAME = "贝壳姐姐" -AI_AVATAR = None # Can set a URL later - - -def format_source_links(sources: list[dict[str, str]], max_sources: int = 3) -> str: - """Format source links for appending to AI reply. - - Args: - sources: List of dicts with 'title' and 'url' keys - max_sources: Maximum number of sources to include - - Returns: - Formatted string with source links, or empty string if no sources - """ - if not sources: - return "" - - # Take only top sources - top_sources = sources[:max_sources] - - lines = ["\n\n📚 参考来源:"] - for s in top_sources: - title = s.get("title", "")[:40] # Truncate long titles - if len(s.get("title", "")) > 40: - title += "..." - url = s.get("url", "") - lines.append(f"• {title}\n {url}") - - return "\n".join(lines) - - -def extract_used_sources( - response: str, sources: list[dict[str, str]] -) -> list[dict[str, str]]: - """Extract which sources were actually used based on source indices in response. - - The AI response may contain source references like [1], [2], etc. - This function extracts which sources were referenced. - - Args: - response: The AI response that may contain [1], [2], etc. - sources: List of available sources - - Returns: - List of sources that were referenced in the response - """ - import re - - if not sources: - return [] - - # Find all [N] references in the response - matches = re.findall(r"\[(\d+)\]", response) - if not matches: - # If no explicit references, return top 2 sources as fallback - return sources[:2] if len(sources) >= 2 else sources - - # Get unique indices (1-based in the response, convert to 0-based) - used_indices = set() - for m in matches: - idx = int(m) - 1 # Convert to 0-based - if 0 <= idx < len(sources): - used_indices.add(idx) - - # Return sources in order - return [sources[i] for i in sorted(used_indices)] - - -def strip_citation_markers(text: str) -> str: - """Remove [1], [2], etc. citation markers from text. - - Args: - text: Text that may contain citation markers like [1], [2] - - Returns: - Text with citation markers removed - """ - import re - - # Remove [N] patterns and clean up extra spaces - text = re.sub(r"\s*\[\d+\]", "", text) - # Clean up multiple spaces - text = re.sub(r" +", " ", text) - return text.strip() - - -# System prompt for community replies -COMMUNITY_SYSTEM_PROMPT = """你是「贝壳姐姐」,MomShell 社区的 AI 助手。你是一位温暖、有同理心的朋友,专门为产后恢复期的妈妈们提供支持和建议。 - -## 你的身份 -- 你是社区里一位热心、有经验的"过来人" -- 你的回复风格:温暖、真诚、有同理心,像朋友聊天一样自然 -- 你会根据提问者的身份调整称呼和语气 -- 你能够通过firecrawl来获取网络内容以了解最新的、可靠的信息,但不会编造内容 - -## 回复规则 -1. 回复要简短精炼(100-200字为宜),不要太长 -2. 先表达理解和共情,再给建议 -3. **重要**:如果提供了网络搜索结果,请基于这些信息回答,并在使用某条信息时用 [编号] 标注来源(如"根据了解[1]...")。不要编造内容。 -4. **重要**:请核查网络搜索结果中的地点、时间、人名等信息,确保符合提问者的需求(如地址是否与要求相同等)。 -5. 不要使用医学专业术语,用通俗易懂的话 -6. 如果涉及严重健康问题,建议寻求专业医疗帮助 -7. 语气要像朋友聊天,不要像机器人 -8. 适当使用表情符号增加亲切感(但不要过多) - -## 回复格式(非常重要) -- 直接输出纯文本,禁止使用任何 Markdown 格式 -- 禁止使用:**粗体**、*斜体*、`代码`、# 标题、- 列表、> 引用、[链接](url) 等 -- 不要使用编号列表(1. 2. 3.),如需列举请用逗号或顿号分隔 -- 只输出自然的中文段落,像微信聊天一样""" - - -def _get_role_display(role: str) -> str: - """Get display name for user role.""" - role_names = { - "mom": "妈妈", - "dad": "爸爸", - "family": "家属", - "certified_doctor": "认证医生", - "certified_therapist": "认证康复师", - "certified_nurse": "认证护士", - "admin": "管理员", - "ai_assistant": "AI 助手", - } - return role_names.get(role, role) - - -class AIReplyService: - """Service for AI auto-replies in community.""" - - def __init__(self) -> None: - settings = get_settings() - self._api_key = settings.modelscope_key - self._base_url = settings.modelscope_base_url - self._model = settings.modelscope_model - self._client: OpenAI | None = None - - @property - def client(self) -> OpenAI: - """Lazy load OpenAI client.""" - if self._client is None: - if not self._api_key: - raise ValueError("MODELSCOPE_KEY not configured") - self._client = OpenAI( - api_key=self._api_key, - base_url=self._base_url, - ) - return self._client - - async def get_or_create_ai_user(self, db: AsyncSession) -> User | None: - """Get or create the AI assistant user account.""" - from app.services.auth.security import get_password_hash - - result = await db.execute(select(User).where(User.username == AI_USERNAME)) - ai_user = result.scalar_one_or_none() - - if ai_user: - return ai_user - - # Create AI user - ai_user = User( - username=AI_USERNAME, - email=AI_EMAIL, - password_hash=get_password_hash("ai_not_login_" + AI_USERNAME), - nickname=AI_NICKNAME, - avatar_url=AI_AVATAR, - role=UserRole.AI_ASSISTANT, - is_active=True, - is_banned=False, - ) - db.add(ai_user) - await db.commit() - await db.refresh(ai_user) - logger.info(f"Created AI assistant account: {AI_NICKNAME}") - return ai_user - - async def _save_ai_reply_to_memory(self, user_id: str, interaction: str) -> None: - """Save an AI community reply to the user's chat memory.""" - try: - from app.services.chat.service import save_community_interaction - - await save_community_interaction(user_id, interaction) - except Exception as e: - logger.warning(f"Failed to save AI reply to memory for {user_id}: {e}") - - def _verify_and_correct( - self, response: str, search_context: str | None - ) -> tuple[str, dict[str, Any]]: - """Apply Chain-of-Verification to reduce hallucinations. - - Args: - response: The generated LLM response - search_context: The web search context used for generation - - Returns: - Tuple of (verified_response, verification_metadata) - """ - try: - cove_service = get_cove_service() - return cove_service.verify_and_correct(response, search_context) - except Exception as e: - logger.warning(f"CoVe verification failed: {e}") - return response, {"error": str(e)} - - def _generate_reply( - self, - question_title: str, - question_content: str, - author_nickname: str, - author_role: str, - web_search_context: str | None = None, - ) -> str: - """Generate AI reply using LLM. - - Args: - question_title: Title of the question - question_content: Content of the question - author_nickname: Nickname of the question author - author_role: Role of the question author - web_search_context: Optional web search results for grounding - """ - role_display = _get_role_display(author_role) - - # Build user prompt with optional web search context - context_section = "" - if web_search_context: - context_section = f""" -## 参考信息(来自网络搜索) -{web_search_context} - -请基于上述参考信息回答问题,但用温暖自然的语气表达。如果参考信息不足以回答,可以结合你的知识,但避免编造具体的医学数据或建议。 - -""" - - user_prompt = f"""{context_section}请回复以下社区提问: - -提问者:{author_nickname}({role_display}) -标题:{question_title} -内容:{question_content} - -请以贝壳姐姐的身份,给这位{role_display}一个温暖、有帮助的回复。""" - - try: - response = self.client.chat.completions.create( - model=self._model, - messages=[ - {"role": "system", "content": COMMUNITY_SYSTEM_PROMPT}, - {"role": "user", "content": user_prompt}, - ], - temperature=0.7, - max_tokens=512, - ) - return ( - response.choices[0].message.content or "感谢你的分享,我在这里陪着你 💗" - ) - except Exception as e: - logger.error(f"AI reply generation failed: {e}") - return "感谢你的分享!如果需要帮助,随时可以在社区提问哦 💗" - - def _generate_reply_to_answer( - self, - question_title: str, - question_content: str, - answer_content: str, - replier_nickname: str, - replier_role: str, - web_search_context: str | None = None, - ) -> str: - """Generate AI reply to someone who replied to AI.""" - role_display = _get_role_display(replier_role) - - # Build context section if web search results available - context_section = "" - if web_search_context: - context_section = f""" -## 参考信息(来自网络搜索) -{web_search_context} - -请基于上述参考信息回答问题,但用温暖自然的语气表达。 - -""" - - user_prompt = f"""{context_section}有人回复了你在社区的回答,请继续对话: - -原帖标题:{question_title} -原帖内容:{question_content} - -回复者:{replier_nickname}({role_display}) -回复内容:{answer_content} - -请以贝壳姐姐的身份,继续和这位{role_display}友好地交流。回复要简短自然。""" - - try: - response = self.client.chat.completions.create( - model=self._model, - messages=[ - {"role": "system", "content": COMMUNITY_SYSTEM_PROMPT}, - {"role": "user", "content": user_prompt}, - ], - temperature=0.7, - max_tokens=256, - ) - return ( - response.choices[0].message.content - or "谢谢你的回复!有什么问题随时聊 💗" - ) - except Exception as e: - logger.error(f"AI reply generation failed: {e}") - return "谢谢你的回复! 💗" - - async def reply_to_question(self, question_id: str) -> None: - """Auto-reply to a new question.""" - from app.services.web_search import get_web_search_service - - async with async_session_maker() as db: - try: - # Get AI user - ai_user = await self.get_or_create_ai_user(db) - if not ai_user: - logger.error("Failed to get AI user") - return - - # Get question with author info - result = await db.execute( - select(Question).where(Question.id == question_id) - ) - question = result.scalar_one_or_none() - if not question: - logger.warning(f"Question {question_id} not found") - return - - # Get author - author = await db.get(User, question.author_id) - if not author: - return - - # Don't reply to AI's own posts - if author.role == UserRole.AI_ASSISTANT: - return - - # Perform web search for factual/medical questions - web_search_context = None - web_search_sources: list[dict[str, str]] = [] - try: - search_service = get_web_search_service() - search_query = f"{question.title} {question.content}" - search_result = await search_service.search_for_context( - search_query - ) - if search_result: - web_search_context, web_search_sources = search_result - logger.info( - f"Web search context found for question {question_id}" - ) - except Exception as e: - logger.warning(f"Web search failed for question {question_id}: {e}") - - # Generate reply with optional web search context - reply_content = await asyncio.get_event_loop().run_in_executor( - None, - self._generate_reply, - question.title, - question.content, - author.nickname, - author.role.value, - web_search_context, - ) - - # Apply Chain-of-Verification to reduce hallucinations - if web_search_context: - ( - reply_content, - cove_metadata, - ) = await asyncio.get_event_loop().run_in_executor( - None, - self._verify_and_correct, - reply_content, - web_search_context, - ) - if cove_metadata.get("corrected"): - logger.info( - f"CoVe corrected response for question {question_id}" - ) - - # Append source links if web search was used - if web_search_sources: - used_sources = extract_used_sources( - reply_content, web_search_sources - ) - if used_sources: - # Remove [1], [2] markers from text, keep only source links - reply_content = strip_citation_markers(reply_content) - reply_content += format_source_links(used_sources) - - # Create answer - answer = Answer( - question_id=question_id, - author_id=ai_user.id, - content=reply_content, - author_role=UserRole.AI_ASSISTANT, - is_professional=False, - status=ContentStatus.PUBLISHED, - ) - db.add(answer) - await db.flush() # Ensure answer is persisted before updating count - - # Update question answer count - await db.execute( - update(Question) - .where(Question.id == question_id) - .values(answer_count=Question.answer_count + 1) - ) - - await db.commit() - logger.info( - f"AI replied to question {question_id}, answer_count incremented" - ) - - # Save to user's chat memory - reply_preview = ( - reply_content[:50] + "..." - if len(reply_content) > 50 - else reply_content - ) - await self._save_ai_reply_to_memory( - author.id, - f"贝壳姐姐回复了你的帖子《{question.title}》:{reply_preview}", - ) - - except Exception as e: - logger.error(f"Failed to reply to question {question_id}: {e}") - await db.rollback() - - def _generate_reply_to_comment( - self, - question_title: str, - answer_content: str, - comment_content: str, - commenter_nickname: str, - commenter_role: str, - web_search_context: str | None = None, - ) -> str: - """Generate AI reply to a comment mentioning @贝壳姐姐.""" - role_display = _get_role_display(commenter_role) - - # Build context section if web search results available - context_section = "" - if web_search_context: - context_section = f""" -## 参考信息(来自网络搜索) -{web_search_context} - -请基于上述参考信息回答问题,但用温暖自然的语气表达。 - -""" - - user_prompt = f"""{context_section}有人在评论区@了你,请回复: - -原帖标题:{question_title} -回答内容:{answer_content} - -评论者:{commenter_nickname}({role_display}) -评论内容:{comment_content} - -请以贝壳姐姐的身份,回复这条评论。回复要简短自然(50-150字)。""" - - try: - response = self.client.chat.completions.create( - model=self._model, - messages=[ - {"role": "system", "content": COMMUNITY_SYSTEM_PROMPT}, - {"role": "user", "content": user_prompt}, - ], - temperature=0.7, - max_tokens=256, - ) - return ( - response.choices[0].message.content or "收到!有什么需要随时找我哦 💗" - ) - except Exception as e: - logger.error(f"AI comment reply generation failed: {e}") - return "收到!有什么需要随时找我哦 💗" - - async def reply_to_comment(self, comment_id: str, answer_id: str) -> None: - """Auto-reply when someone mentions @贝壳姐姐 in a comment.""" - from app.services.web_search import get_web_search_service - - async with async_session_maker() as db: - try: - # Get AI user - ai_user = await self.get_or_create_ai_user(db) - if not ai_user: - return - - # Get the trigger comment - result = await db.execute( - select(Comment).where(Comment.id == comment_id) - ) - trigger_comment = result.scalar_one_or_none() - if not trigger_comment: - return - - # Get commenter - commenter = await db.get(User, trigger_comment.author_id) - if not commenter or commenter.role == UserRole.AI_ASSISTANT: - return - - # Get the answer - answer = await db.get(Answer, answer_id) - if not answer: - return - - # Get the question - question = await db.get(Question, answer.question_id) - if not question: - return - - # Perform web search for factual questions - web_search_context = None - web_search_sources: list[dict[str, str]] = [] - try: - search_service = get_web_search_service() - search_query = f"{question.title} {trigger_comment.content}" - search_result = await search_service.search_for_context( - search_query - ) - if search_result: - web_search_context, web_search_sources = search_result - logger.info( - f"Web search context found for comment {comment_id}" - ) - except Exception as e: - logger.warning(f"Web search failed for comment {comment_id}: {e}") - - # Generate reply - reply_content = await asyncio.get_event_loop().run_in_executor( - None, - self._generate_reply_to_comment, - question.title, - answer.content, - trigger_comment.content, - commenter.nickname, - commenter.role.value, - web_search_context, - ) - - # Apply Chain-of-Verification to reduce hallucinations - if web_search_context: - ( - reply_content, - cove_metadata, - ) = await asyncio.get_event_loop().run_in_executor( - None, - self._verify_and_correct, - reply_content, - web_search_context, - ) - if cove_metadata.get("corrected"): - logger.info(f"CoVe corrected response for comment {comment_id}") - - # Append source links if web search was used - if web_search_sources: - used_sources = extract_used_sources( - reply_content, web_search_sources - ) - if used_sources: - # Remove [1], [2] markers from text, keep only source links - reply_content = strip_citation_markers(reply_content) - reply_content += format_source_links(used_sources) - - # Create comment reply (nested under the trigger comment) - ai_comment = Comment( - answer_id=answer_id, - author_id=ai_user.id, - parent_id=trigger_comment.id, - reply_to_user_id=commenter.id, - content=reply_content, - status=ContentStatus.PUBLISHED, - ) - db.add(ai_comment) - - # Update answer's comment count - await db.execute( - update(Answer) - .where(Answer.id == answer_id) - .values(comment_count=Answer.comment_count + 1) - ) - - await db.commit() - logger.info(f"AI replied to comment {comment_id}") - - # Save to user's chat memory - await self._save_ai_reply_to_memory( - commenter.id, - f"贝壳姐姐回复了你的评论:{reply_content[:50]}..." - if len(reply_content) > 50 - else f"贝壳姐姐回复了你的评论:{reply_content}", - ) - - except Exception as e: - logger.error(f"Failed to reply to comment {comment_id}: {e}") - await db.rollback() - - async def reply_to_comment_on_ai_answer( - self, comment_id: str, answer_id: str - ) -> None: - """Auto-reply when someone comments on AI's answer.""" - from app.services.web_search import get_web_search_service - - async with async_session_maker() as db: - try: - # Get AI user - ai_user = await self.get_or_create_ai_user(db) - if not ai_user: - return - - # Get the trigger comment - result = await db.execute( - select(Comment).where(Comment.id == comment_id) - ) - trigger_comment = result.scalar_one_or_none() - if not trigger_comment: - return - - # Get commenter - commenter = await db.get(User, trigger_comment.author_id) - if not commenter or commenter.role == UserRole.AI_ASSISTANT: - return - - # Get the answer (should belong to AI) - answer = await db.get(Answer, answer_id) - if not answer or answer.author_id != ai_user.id: - return - - # Get the question for context - question = await db.get(Question, answer.question_id) - if not question: - return - - # Perform web search for factual questions - web_search_context = None - web_search_sources: list[dict[str, str]] = [] - try: - search_service = get_web_search_service() - search_query = f"{question.title} {trigger_comment.content}" - search_result = await search_service.search_for_context( - search_query - ) - if search_result: - web_search_context, web_search_sources = search_result - logger.info( - f"Web search context found for comment on AI answer {comment_id}" - ) - except Exception as e: - logger.warning(f"Web search failed for comment {comment_id}: {e}") - - # Generate reply - reply_content = await asyncio.get_event_loop().run_in_executor( - None, - self._generate_reply_to_comment, - question.title, - answer.content, - trigger_comment.content, - commenter.nickname, - commenter.role.value, - web_search_context, - ) - - # Apply Chain-of-Verification to reduce hallucinations - if web_search_context: - ( - reply_content, - cove_metadata, - ) = await asyncio.get_event_loop().run_in_executor( - None, - self._verify_and_correct, - reply_content, - web_search_context, - ) - if cove_metadata.get("corrected"): - logger.info( - f"CoVe corrected response for comment on AI answer {comment_id}" - ) - - # Append source links if web search was used - if web_search_sources: - used_sources = extract_used_sources( - reply_content, web_search_sources - ) - if used_sources: - # Remove [1], [2] markers from text, keep only source links - reply_content = strip_citation_markers(reply_content) - reply_content += format_source_links(used_sources) - - # Create comment reply (nested under the trigger comment) - ai_comment = Comment( - answer_id=answer_id, - author_id=ai_user.id, - parent_id=trigger_comment.id, - reply_to_user_id=commenter.id, - content=reply_content, - status=ContentStatus.PUBLISHED, - ) - db.add(ai_comment) - - # Update answer's comment count - await db.execute( - update(Answer) - .where(Answer.id == answer_id) - .values(comment_count=Answer.comment_count + 1) - ) - - await db.commit() - logger.info(f"AI replied to comment {comment_id} on AI's answer") - - # Save to user's chat memory - reply_preview = ( - reply_content[:50] + "..." - if len(reply_content) > 50 - else reply_content - ) - await self._save_ai_reply_to_memory( - commenter.id, - f"贝壳姐姐回复了你的评论:{reply_preview}", - ) - - except Exception as e: - logger.error( - f"Failed to reply to comment {comment_id} on AI answer: {e}" - ) - await db.rollback() - - async def reply_to_reply_on_ai_comment( - self, comment_id: str, answer_id: str, parent_comment_id: str - ) -> None: - """Auto-reply when someone replies to AI's comment.""" - from app.services.web_search import get_web_search_service - - async with async_session_maker() as db: - try: - # Get AI user - ai_user = await self.get_or_create_ai_user(db) - if not ai_user: - return - - # Get the trigger comment - result = await db.execute( - select(Comment).where(Comment.id == comment_id) - ) - trigger_comment = result.scalar_one_or_none() - if not trigger_comment: - return - - # Get the parent comment (should belong to AI) - parent_comment = await db.get(Comment, parent_comment_id) - if not parent_comment or parent_comment.author_id != ai_user.id: - return - - # Get commenter - commenter = await db.get(User, trigger_comment.author_id) - if not commenter or commenter.role == UserRole.AI_ASSISTANT: - return - - # Get the answer - answer = await db.get(Answer, answer_id) - if not answer: - return - - # Get the question for context - question = await db.get(Question, answer.question_id) - if not question: - return - - # Perform web search for factual questions - web_search_context = None - web_search_sources: list[dict[str, str]] = [] - try: - search_service = get_web_search_service() - search_query = f"{question.title} {trigger_comment.content}" - search_result = await search_service.search_for_context( - search_query - ) - if search_result: - web_search_context, web_search_sources = search_result - logger.info( - f"Web search context found for reply on AI comment {comment_id}" - ) - except Exception as e: - logger.warning(f"Web search failed for comment {comment_id}: {e}") - - # Generate reply - reply_content = await asyncio.get_event_loop().run_in_executor( - None, - self._generate_reply_to_comment, - question.title, - answer.content, - trigger_comment.content, - commenter.nickname, - commenter.role.value, - web_search_context, - ) - - # Apply Chain-of-Verification to reduce hallucinations - if web_search_context: - ( - reply_content, - cove_metadata, - ) = await asyncio.get_event_loop().run_in_executor( - None, - self._verify_and_correct, - reply_content, - web_search_context, - ) - if cove_metadata.get("corrected"): - logger.info( - f"CoVe corrected response for reply on AI comment {comment_id}" - ) - - # Append source links if web search was used - if web_search_sources: - used_sources = extract_used_sources( - reply_content, web_search_sources - ) - if used_sources: - # Remove [1], [2] markers from text, keep only source links - reply_content = strip_citation_markers(reply_content) - reply_content += format_source_links(used_sources) - - # Create comment reply (nested under the trigger comment) - ai_comment = Comment( - answer_id=answer_id, - author_id=ai_user.id, - parent_id=trigger_comment.id, - reply_to_user_id=commenter.id, - content=reply_content, - status=ContentStatus.PUBLISHED, - ) - db.add(ai_comment) - - # Update answer's comment count - await db.execute( - update(Answer) - .where(Answer.id == answer_id) - .values(comment_count=Answer.comment_count + 1) - ) - - await db.commit() - logger.info(f"AI replied to reply {comment_id} on AI's comment") - - # Save to user's chat memory - reply_preview = ( - reply_content[:50] + "..." - if len(reply_content) > 50 - else reply_content - ) - await self._save_ai_reply_to_memory( - commenter.id, - f"贝壳姐姐回复了你的评论:{reply_preview}", - ) - - except Exception as e: - logger.error( - f"Failed to reply to reply {comment_id} on AI comment: {e}" - ) - await db.rollback() - - async def reply_as_comment_to_answer( - self, answer_id: str, question_id: str - ) -> None: - """Reply as a comment under someone's answer (when they @贝壳姐姐 in their answer).""" - from app.services.web_search import get_web_search_service - - async with async_session_maker() as db: - try: - # Get AI user - ai_user = await self.get_or_create_ai_user(db) - if not ai_user: - return - - # Get the answer - answer = await db.get(Answer, answer_id) - if not answer: - return - - # Get the answer author - author = await db.get(User, answer.author_id) - if not author or author.role == UserRole.AI_ASSISTANT: - return - - # Get the question for context - question = await db.get(Question, question_id) - if not question: - return - - # Perform web search for factual questions - web_search_context = None - web_search_sources: list[dict[str, str]] = [] - try: - search_service = get_web_search_service() - search_query = f"{question.title} {answer.content}" - search_result = await search_service.search_for_context( - search_query - ) - if search_result: - web_search_context, web_search_sources = search_result - logger.info( - f"Web search context found for answer comment {answer_id}" - ) - except Exception as e: - logger.warning(f"Web search failed for answer {answer_id}: {e}") - - # Generate reply - reply_content = await asyncio.get_event_loop().run_in_executor( - None, - self._generate_reply_to_comment, - question.title, - answer.content, - answer.content, - author.nickname, - author.role.value, - web_search_context, - ) - - # Apply Chain-of-Verification to reduce hallucinations - if web_search_context: - ( - reply_content, - cove_metadata, - ) = await asyncio.get_event_loop().run_in_executor( - None, - self._verify_and_correct, - reply_content, - web_search_context, - ) - if cove_metadata.get("corrected"): - logger.info( - f"CoVe corrected response for answer comment {answer_id}" - ) - - # Append source links if web search was used - if web_search_sources: - used_sources = extract_used_sources( - reply_content, web_search_sources - ) - if used_sources: - # Remove [1], [2] markers from text, keep only source links - reply_content = strip_citation_markers(reply_content) - reply_content += format_source_links(used_sources) - - # Create comment under the answer (inside the person's floor) - ai_comment = Comment( - answer_id=answer_id, - author_id=ai_user.id, - parent_id=None, # Root-level comment under this answer - reply_to_user_id=author.id, - content=reply_content, - status=ContentStatus.PUBLISHED, - ) - db.add(ai_comment) - - # Update answer's comment count - await db.execute( - update(Answer) - .where(Answer.id == answer_id) - .values(comment_count=Answer.comment_count + 1) - ) - - await db.commit() - logger.info(f"AI replied as comment to answer {answer_id} (@ mention)") - - # Save to user's chat memory - reply_preview = ( - reply_content[:50] + "..." - if len(reply_content) > 50 - else reply_content - ) - await self._save_ai_reply_to_memory( - author.id, - f"贝壳姐姐回复了你的回答:{reply_preview}", - ) - - except Exception as e: - logger.error(f"Failed to reply as comment to answer {answer_id}: {e}") - await db.rollback() - - async def reply_to_answer(self, answer_id: str, question_id: str) -> None: - """Auto-reply when someone replies to AI's answer.""" - from app.services.web_search import get_web_search_service - - async with async_session_maker() as db: - try: - # Get AI user - ai_user = await self.get_or_create_ai_user(db) - if not ai_user: - return - - # Get the answer that triggered this - result = await db.execute(select(Answer).where(Answer.id == answer_id)) - trigger_answer = result.scalar_one_or_none() - if not trigger_answer: - return - - # Get the replier - replier = await db.get(User, trigger_answer.author_id) - if not replier: - return - - # Don't reply to AI itself - if replier.role == UserRole.AI_ASSISTANT: - return - - # Get the question - question = await db.get(Question, question_id) - if not question: - return - - # Check if there's an AI answer that this might be replying to - result = await db.execute( - select(Answer).where( - Answer.question_id == question_id, - Answer.author_id == ai_user.id, - ) - ) - ai_answers = result.scalars().all() - if not ai_answers: - # AI hasn't answered this question, no need to reply - return - - # Perform web search for factual questions - web_search_context = None - web_search_sources: list[dict[str, str]] = [] - try: - search_service = get_web_search_service() - search_query = f"{question.title} {trigger_answer.content}" - search_result = await search_service.search_for_context( - search_query - ) - if search_result: - web_search_context, web_search_sources = search_result - logger.info(f"Web search context found for answer {answer_id}") - except Exception as e: - logger.warning(f"Web search failed for answer {answer_id}: {e}") - - # Generate reply - reply_content = await asyncio.get_event_loop().run_in_executor( - None, - self._generate_reply_to_answer, - question.title, - question.content, - trigger_answer.content, - replier.nickname, - replier.role.value, - web_search_context, - ) - - # Apply Chain-of-Verification to reduce hallucinations - if web_search_context: - ( - reply_content, - cove_metadata, - ) = await asyncio.get_event_loop().run_in_executor( - None, - self._verify_and_correct, - reply_content, - web_search_context, - ) - if cove_metadata.get("corrected"): - logger.info(f"CoVe corrected response for answer {answer_id}") - - # Append source links if web search was used - if web_search_sources: - used_sources = extract_used_sources( - reply_content, web_search_sources - ) - if used_sources: - # Remove [1], [2] markers from text, keep only source links - reply_content = strip_citation_markers(reply_content) - reply_content += format_source_links(used_sources) - - # Create answer - answer = Answer( - question_id=question_id, - author_id=ai_user.id, - content=reply_content, - author_role=UserRole.AI_ASSISTANT, - is_professional=False, - status=ContentStatus.PUBLISHED, - ) - db.add(answer) - await db.flush() # Ensure answer is persisted before updating count - - # Update question answer count - await db.execute( - update(Question) - .where(Question.id == question_id) - .values(answer_count=Question.answer_count + 1) - ) - - await db.commit() - logger.info( - f"AI replied to answer {answer_id}, answer_count incremented" - ) - - # Save to user's chat memory - reply_preview = ( - reply_content[:50] + "..." - if len(reply_content) > 50 - else reply_content - ) - await self._save_ai_reply_to_memory( - replier.id, - f"贝壳姐姐回复了你在《{question.title}》的回答:{reply_preview}", - ) - - except Exception as e: - logger.error(f"Failed to reply to answer {answer_id}: {e}") - await db.rollback() - - -# Global service instance -_ai_reply_service: AIReplyService | None = None - - -def get_ai_reply_service() -> AIReplyService: - """Get the AI reply service singleton.""" - global _ai_reply_service - if _ai_reply_service is None: - _ai_reply_service = AIReplyService() - return _ai_reply_service - - -async def trigger_ai_reply_to_question(question_id: str) -> None: - """Trigger AI reply to a question (called from router).""" - service = get_ai_reply_service() - # Run in background to not block the response - asyncio.create_task(service.reply_to_question(question_id)) - - -async def trigger_ai_reply_to_answer(answer_id: str, question_id: str) -> None: - """Trigger AI reply when someone answers (called from router).""" - service = get_ai_reply_service() - # Run in background - asyncio.create_task(service.reply_to_answer(answer_id, question_id)) - - -async def trigger_ai_comment_on_answer(answer_id: str, question_id: str) -> None: - """Trigger AI to reply as a comment under an answer (when @贝壳姐姐 is mentioned).""" - service = get_ai_reply_service() - asyncio.create_task(service.reply_as_comment_to_answer(answer_id, question_id)) - - -async def trigger_ai_reply_to_comment(comment_id: str, answer_id: str) -> None: - """Trigger AI reply when someone mentions @贝壳姐姐 in a comment.""" - service = get_ai_reply_service() - asyncio.create_task(service.reply_to_comment(comment_id, answer_id)) - - -async def trigger_ai_reply_to_ai_content( - comment_id: str, answer_id: str, parent_id: str | None -) -> None: - """Trigger AI reply when someone comments on AI's answer or replies to AI's comment.""" - service = get_ai_reply_service() - if parent_id: - # Reply to AI's comment - asyncio.create_task( - service.reply_to_reply_on_ai_comment(comment_id, answer_id, parent_id) - ) - else: - # Comment on AI's answer - asyncio.create_task( - service.reply_to_comment_on_ai_answer(comment_id, answer_id) - ) diff --git a/backend/app/services/community/dependencies.py b/backend/app/services/community/dependencies.py deleted file mode 100644 index 13937c3a..00000000 --- a/backend/app/services/community/dependencies.py +++ /dev/null @@ -1,195 +0,0 @@ -"""Dependencies for community module.""" - -from typing import Annotated - -from fastapi import Depends, Header, HTTPException, Request -from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.orm import selectinload - -from app.core.database import get_db -from app.services.auth.security import decode_token - -from .models import User -from .service import CommunityService, get_community_service - -# Type aliases for dependency injection -DbSession = Annotated[AsyncSession, Depends(get_db)] -CommunityServiceDep = Annotated[CommunityService, Depends(get_community_service)] - -# HTTP Bearer scheme (auto_error=False to allow fallback to X-Access-Token) -http_bearer = HTTPBearer(auto_error=False) - - -def get_token_from_request( - request: Request, - credentials: HTTPAuthorizationCredentials | None = None, -) -> str | None: - """ - Extract token from multiple sources (fallback order): - 1. Authorization: Bearer header - 2. X-Access-Token custom header (for proxies that strip Authorization) - """ - # 1. Try Authorization header - if credentials and credentials.credentials: - return credentials.credentials - - # 2. Try custom header (some proxies like ModelScope strip Authorization) - custom_token = request.headers.get("X-Access-Token") - if custom_token: - return custom_token - - return None - - -async def get_current_user( - request: Request, - db: DbSession, - credentials: Annotated[ - HTTPAuthorizationCredentials | None, Depends(http_bearer) - ] = None, - x_user_id: str | None = Header(None, alias="X-User-ID"), -) -> User: - """ - Get current user from JWT token or X-User-ID header (fallback for dev). - - Priority: - 1. JWT Bearer token in Authorization header - 2. X-Access-Token custom header (for proxies that strip Authorization) - 3. X-User-ID header (for backward compatibility / development) - """ - user: User | None = None - - # Try JWT token (from Authorization header or X-Access-Token) - token = get_token_from_request(request, credentials) - if token: - payload = decode_token(token) - if payload and payload.get("type") == "access": - user_id = payload.get("sub") - if user_id: - result = await db.execute( - select(User) - .options(selectinload(User.certification)) - .where(User.id == user_id) - ) - user = result.scalar_one_or_none() - - # Fallback to X-User-ID for development compatibility - if not user and x_user_id: - result = await db.execute( - select(User) - .options(selectinload(User.certification)) - .where(User.id == x_user_id) - ) - user = result.scalar_one_or_none() - if not user: - # Auto-create user for development/demo purposes - from .enums import UserRole - - user = User( - id=x_user_id, - username=f"user_{x_user_id[:8]}", - email=f"{x_user_id[:8]}@example.com", - password_hash="", - nickname="新用户", - role=UserRole.MOM, - is_active=True, - is_banned=False, - ) - db.add(user) - await db.commit() - await db.refresh(user) - - if not user: - raise HTTPException(status_code=401, detail="未登录") - - if not user.is_active: - raise HTTPException(status_code=403, detail="账号已禁用") - - if user.is_banned: - raise HTTPException(status_code=403, detail="账号已被封禁") - - return user - - -async def get_current_user_optional( - request: Request, - db: DbSession, - credentials: Annotated[ - HTTPAuthorizationCredentials | None, Depends(http_bearer) - ] = None, - x_user_id: str | None = Header(None, alias="X-User-ID"), -) -> User | None: - """ - Get current user if authenticated, None otherwise. - - Priority: - 1. JWT Bearer token in Authorization header - 2. X-Access-Token custom header (for proxies that strip Authorization) - 3. X-User-ID header (for backward compatibility / development) - """ - user: User | None = None - - # Try JWT token (from Authorization header or X-Access-Token) - token = get_token_from_request(request, credentials) - if token: - payload = decode_token(token) - if payload and payload.get("type") == "access": - user_id = payload.get("sub") - if user_id: - result = await db.execute( - select(User) - .options(selectinload(User.certification)) - .where(User.id == user_id) - ) - user = result.scalar_one_or_none() - - # Fallback to X-User-ID for development compatibility - if not user and x_user_id: - result = await db.execute( - select(User) - .options(selectinload(User.certification)) - .where(User.id == x_user_id) - ) - user = result.scalar_one_or_none() - if not user: - # Auto-create user for development/demo purposes - from .enums import UserRole - - user = User( - id=x_user_id, - username=f"user_{x_user_id[:8]}", - email=f"{x_user_id[:8]}@example.com", - password_hash="", - nickname="新用户", - role=UserRole.MOM, - is_active=True, - is_banned=False, - ) - db.add(user) - await db.commit() - await db.refresh(user) - - if user and (not user.is_active or user.is_banned): - return None - - return user - - -async def get_admin_user( - current_user: Annotated[User, Depends(get_current_user)], -) -> User: - """Require admin user.""" - from .enums import UserRole - - if current_user.role != UserRole.ADMIN: - raise HTTPException(status_code=403, detail="需要管理员权限") - - return current_user - - -# Type aliases for authenticated dependencies -CurrentUser = Annotated[User, Depends(get_current_user)] -OptionalUser = Annotated[User | None, Depends(get_current_user_optional)] -AdminUser = Annotated[User, Depends(get_admin_user)] diff --git a/backend/app/services/community/enums.py b/backend/app/services/community/enums.py deleted file mode 100644 index 2534b015..00000000 --- a/backend/app/services/community/enums.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Community module enums.""" - -from enum import Enum - - -class UserRole(str, Enum): - """User role enumeration.""" - - GUEST = "guest" # Guest (not logged in) - MOM = "mom" # Mother - DAD = "dad" # Father - FAMILY = "family" # Other family members - CERTIFIED_DOCTOR = "certified_doctor" # Certified doctor - CERTIFIED_THERAPIST = "certified_therapist" # Certified therapist - CERTIFIED_NURSE = "certified_nurse" # Certified nurse - ADMIN = "admin" # Administrator - AI_ASSISTANT = "ai_assistant" # AI Assistant (auto-reply bot) - - -class ChannelType(str, Enum): - """Channel type enumeration.""" - - PROFESSIONAL = "professional" # Professional channel (certified professionals only) - EXPERIENCE = "experience" # Experience channel (all users can answer) - - -class CertificationStatus(str, Enum): - """Certification status enumeration.""" - - PENDING = "pending" # Pending review - APPROVED = "approved" # Approved - REJECTED = "rejected" # Rejected - EXPIRED = "expired" # Expired - REVOKED = "revoked" # Revoked by admin - - -class ContentStatus(str, Enum): - """Content status enumeration.""" - - DRAFT = "draft" # Draft - PENDING_REVIEW = "pending_review" # Pending review - PUBLISHED = "published" # Published - HIDDEN = "hidden" # Hidden (violation) - DELETED = "deleted" # Deleted - - -class ModerationResult(str, Enum): - """Moderation result enumeration.""" - - PASSED = "passed" # Passed - REJECTED = "rejected" # Rejected - NEED_MANUAL_REVIEW = "need_manual_review" # Needs manual review - - -class SensitiveCategory(str, Enum): - """Sensitive content category enumeration.""" - - PSEUDOSCIENCE = "pseudoscience" # Pseudoscience - DEPRESSION_TRIGGER = "depression_trigger" # Postpartum depression triggers - SOFT_PORNOGRAPHY = "soft_pornography" # Soft pornography - VIOLENCE = "violence" # Violent content - SPAM = "spam" # Spam/advertising - MISINFORMATION = "misinformation" # Medical misinformation - SELF_HARM = "self_harm" # Self-harm related - POLITICAL = "political" # Politically sensitive - HARASSMENT = "harassment" # Harassment/abuse - - -# Role sets for permission checking -PROFESSIONAL_ROLES = { - UserRole.CERTIFIED_DOCTOR, - UserRole.CERTIFIED_THERAPIST, - UserRole.CERTIFIED_NURSE, -} - -FAMILY_ROLES = { - UserRole.MOM, - UserRole.DAD, - UserRole.FAMILY, -} diff --git a/backend/app/services/community/models.py b/backend/app/services/community/models.py deleted file mode 100644 index faa3e4af..00000000 --- a/backend/app/services/community/models.py +++ /dev/null @@ -1,442 +0,0 @@ -"""Community module SQLAlchemy models.""" - -from datetime import datetime -from typing import TYPE_CHECKING -from uuid import uuid4 - -from sqlalchemy import ( - Boolean, - DateTime, - Float, - ForeignKey, - Integer, - String, - Text, - UniqueConstraint, -) -from sqlalchemy import ( - Enum as SAEnum, -) -from sqlalchemy.orm import Mapped, mapped_column, relationship - -from app.core.database import Base - -from .enums import ( - CertificationStatus, - ChannelType, - ContentStatus, - ModerationResult, - UserRole, -) - -if TYPE_CHECKING: - pass - - -def generate_uuid() -> str: - """Generate a UUID string.""" - return str(uuid4()) - - -# ============================================================ -# User Related Tables -# ============================================================ - - -class User(Base): - """User table - extended to support role certification.""" - - __tablename__ = "users" - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - username: Mapped[str] = mapped_column(String(50), unique=True, index=True) - email: Mapped[str] = mapped_column(String(255), unique=True, index=True) - password_hash: Mapped[str] = mapped_column(String(255)) - nickname: Mapped[str] = mapped_column(String(50)) - avatar_url: Mapped[str | None] = mapped_column(String(500), nullable=True) - role: Mapped[UserRole] = mapped_column(SAEnum(UserRole), default=UserRole.MOM) - - # Postpartum related info - baby_birth_date: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - postpartum_weeks: Mapped[int | None] = mapped_column(Integer, nullable=True) - - # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - updated_at: Mapped[datetime] = mapped_column( - DateTime, default=datetime.utcnow, onupdate=datetime.utcnow - ) - last_active_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - - # Status - is_active: Mapped[bool] = mapped_column(Boolean, default=True) - is_banned: Mapped[bool] = mapped_column(Boolean, default=False) - - # Relationships - certification: Mapped["UserCertification | None"] = relationship( - "UserCertification", back_populates="user", uselist=False - ) - questions: Mapped["list[Question]"] = relationship( - "Question", back_populates="author" - ) - answers: Mapped["list[Answer]"] = relationship("Answer", back_populates="author") - comments: Mapped["list[Comment]"] = relationship( - "Comment", back_populates="author", foreign_keys="[Comment.author_id]" - ) - likes: Mapped["list[Like]"] = relationship("Like", back_populates="user") - collections: Mapped["list[Collection]"] = relationship( - "Collection", back_populates="user" - ) - - -class UserCertification(Base): - """User certification table - stores professional credentials.""" - - __tablename__ = "user_certifications" - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - user_id: Mapped[str] = mapped_column( - String(36), ForeignKey("users.id", ondelete="CASCADE"), unique=True - ) - - # Certification info - certification_type: Mapped[UserRole] = mapped_column(SAEnum(UserRole)) - real_name: Mapped[str] = mapped_column(String(50)) - id_card_number: Mapped[str | None] = mapped_column(String(18), nullable=True) - license_number: Mapped[str] = mapped_column(String(100)) # License number - hospital_or_institution: Mapped[str] = mapped_column(String(200)) # Institution - department: Mapped[str | None] = mapped_column( - String(100), nullable=True - ) # Department - title: Mapped[str | None] = mapped_column(String(50), nullable=True) # Title - - # Supporting documents - license_image_url: Mapped[str] = mapped_column(String(500)) # License photo - id_card_image_url: Mapped[str | None] = mapped_column(String(500), nullable=True) - additional_docs_urls: Mapped[str | None] = mapped_column( - Text, nullable=True - ) # JSON array - - # Review status - status: Mapped[CertificationStatus] = mapped_column( - SAEnum(CertificationStatus), default=CertificationStatus.PENDING - ) - reviewer_id: Mapped[str | None] = mapped_column(String(36), nullable=True) - review_comment: Mapped[str | None] = mapped_column(Text, nullable=True) - reviewed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - - # Validity period - valid_from: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - valid_until: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - - # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - updated_at: Mapped[datetime] = mapped_column( - DateTime, default=datetime.utcnow, onupdate=datetime.utcnow - ) - - # Relationships - user: Mapped["User"] = relationship("User", back_populates="certification") - - -# ============================================================ -# Content Related Tables -# ============================================================ - - -class Tag(Base): - """Tag table.""" - - __tablename__ = "tags" - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - name: Mapped[str] = mapped_column(String(50), unique=True, index=True) - slug: Mapped[str] = mapped_column( - String(50), unique=True, index=True - ) # URL-friendly - description: Mapped[str | None] = mapped_column(String(200), nullable=True) - - # Statistics - question_count: Mapped[int] = mapped_column(Integer, default=0) - follower_count: Mapped[int] = mapped_column(Integer, default=0) - - # Status - is_active: Mapped[bool] = mapped_column(Boolean, default=True) - is_featured: Mapped[bool] = mapped_column(Boolean, default=False) # Featured tag - - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - - # Relationships - questions: Mapped["list[Question]"] = relationship( - "Question", secondary="question_tags", back_populates="tags" - ) - - -class QuestionTag(Base): - """Question-Tag association table.""" - - __tablename__ = "question_tags" - - question_id: Mapped[str] = mapped_column( - String(36), ForeignKey("questions.id", ondelete="CASCADE"), primary_key=True - ) - tag_id: Mapped[str] = mapped_column( - String(36), ForeignKey("tags.id", ondelete="CASCADE"), primary_key=True - ) - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - - -class Question(Base): - """Question table (posts).""" - - __tablename__ = "questions" - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - author_id: Mapped[str] = mapped_column( - String(36), ForeignKey("users.id", ondelete="CASCADE"), index=True - ) - - # Content - title: Mapped[str] = mapped_column(String(200), index=True) - content: Mapped[str] = mapped_column(Text) - image_urls: Mapped[str | None] = mapped_column(Text, nullable=True) # JSON array - - # Channel configuration - channel: Mapped[ChannelType] = mapped_column( - SAEnum(ChannelType), default=ChannelType.EXPERIENCE, index=True - ) - - # Status - status: Mapped[ContentStatus] = mapped_column( - SAEnum(ContentStatus), default=ContentStatus.PENDING_REVIEW, index=True - ) - - # Statistics - view_count: Mapped[int] = mapped_column(Integer, default=0) - answer_count: Mapped[int] = mapped_column(Integer, default=0) - like_count: Mapped[int] = mapped_column(Integer, default=0) - collection_count: Mapped[int] = mapped_column(Integer, default=0) - - # Pinned/Featured - is_pinned: Mapped[bool] = mapped_column(Boolean, default=False) - is_featured: Mapped[bool] = mapped_column(Boolean, default=False) - - # Accepted answer - accepted_answer_id: Mapped[str | None] = mapped_column(String(36), nullable=True) - - # Timestamps - created_at: Mapped[datetime] = mapped_column( - DateTime, default=datetime.utcnow, index=True - ) - updated_at: Mapped[datetime] = mapped_column( - DateTime, default=datetime.utcnow, onupdate=datetime.utcnow - ) - published_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - - # Relationships - author: Mapped["User"] = relationship("User", back_populates="questions") - answers: Mapped["list[Answer]"] = relationship( - "Answer", back_populates="question", cascade="all, delete-orphan" - ) - tags: Mapped["list[Tag]"] = relationship( - "Tag", secondary="question_tags", back_populates="questions" - ) - - -class Answer(Base): - """Answer table.""" - - __tablename__ = "answers" - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - question_id: Mapped[str] = mapped_column( - String(36), ForeignKey("questions.id", ondelete="CASCADE"), index=True - ) - author_id: Mapped[str] = mapped_column( - String(36), ForeignKey("users.id", ondelete="CASCADE"), index=True - ) - - # Content - content: Mapped[str] = mapped_column(Text) - image_urls: Mapped[str | None] = mapped_column(Text, nullable=True) # JSON array - - # Author role (redundant storage for easy filtering) - author_role: Mapped[UserRole] = mapped_column(SAEnum(UserRole), index=True) - is_professional: Mapped[bool] = mapped_column(Boolean, default=False, index=True) - - # Status - status: Mapped[ContentStatus] = mapped_column( - SAEnum(ContentStatus), default=ContentStatus.PENDING_REVIEW, index=True - ) - - # Statistics - like_count: Mapped[int] = mapped_column(Integer, default=0) - comment_count: Mapped[int] = mapped_column(Integer, default=0) - - # Accepted status - is_accepted: Mapped[bool] = mapped_column(Boolean, default=False) - - # Timestamps - created_at: Mapped[datetime] = mapped_column( - DateTime, default=datetime.utcnow, index=True - ) - updated_at: Mapped[datetime] = mapped_column( - DateTime, default=datetime.utcnow, onupdate=datetime.utcnow - ) - - # Relationships - question: Mapped["Question"] = relationship("Question", back_populates="answers") - author: Mapped["User"] = relationship("User", back_populates="answers") - comments: Mapped["list[Comment]"] = relationship( - "Comment", back_populates="answer", cascade="all, delete-orphan" - ) - - -class Comment(Base): - """Comment table (nested replies).""" - - __tablename__ = "comments" - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - answer_id: Mapped[str] = mapped_column( - String(36), ForeignKey("answers.id", ondelete="CASCADE"), index=True - ) - author_id: Mapped[str] = mapped_column( - String(36), ForeignKey("users.id", ondelete="CASCADE"), index=True - ) - - # Reply target (supports nested comments) - parent_id: Mapped[str | None] = mapped_column( - String(36), ForeignKey("comments.id", ondelete="CASCADE"), nullable=True - ) - reply_to_user_id: Mapped[str | None] = mapped_column( - String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True - ) - - # Content - content: Mapped[str] = mapped_column(Text) - - # Status - status: Mapped[ContentStatus] = mapped_column( - SAEnum(ContentStatus), default=ContentStatus.PENDING_REVIEW - ) - - # Statistics - like_count: Mapped[int] = mapped_column(Integer, default=0) - - # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - updated_at: Mapped[datetime] = mapped_column( - DateTime, default=datetime.utcnow, onupdate=datetime.utcnow - ) - - # Relationships - answer: Mapped["Answer"] = relationship("Answer", back_populates="comments") - author: Mapped["User"] = relationship( - "User", back_populates="comments", foreign_keys=[author_id] - ) - parent: Mapped["Comment | None"] = relationship( - "Comment", remote_side="Comment.id", backref="replies" - ) - reply_to_user: Mapped["User | None"] = relationship( - "User", foreign_keys=[reply_to_user_id] - ) - - -# ============================================================ -# Interaction Tables -# ============================================================ - - -class Like(Base): - """Like table (polymorphic association).""" - - __tablename__ = "likes" - __table_args__ = ( - UniqueConstraint("user_id", "target_type", "target_id", name="uq_user_like"), - ) - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - user_id: Mapped[str] = mapped_column( - String(36), ForeignKey("users.id", ondelete="CASCADE"), index=True - ) - - # Polymorphic association - target_type: Mapped[str] = mapped_column( - String(20), index=True - ) # question, answer, comment - target_id: Mapped[str] = mapped_column(String(36), index=True) - - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - - # Relationships - user: Mapped["User"] = relationship("User", back_populates="likes") - - -class Collection(Base): - """Collection/Bookmark table.""" - - __tablename__ = "collections" - __table_args__ = ( - UniqueConstraint("user_id", "question_id", name="uq_user_collection"), - ) - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - user_id: Mapped[str] = mapped_column( - String(36), ForeignKey("users.id", ondelete="CASCADE"), index=True - ) - question_id: Mapped[str] = mapped_column( - String(36), ForeignKey("questions.id", ondelete="CASCADE"), index=True - ) - - # Collection folder (optional) - folder_name: Mapped[str | None] = mapped_column(String(50), nullable=True) - note: Mapped[str | None] = mapped_column(String(500), nullable=True) - - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - - # Relationships - user: Mapped["User"] = relationship("User", back_populates="collections") - question: Mapped["Question"] = relationship("Question") - - -# ============================================================ -# Moderation Tables -# ============================================================ - - -class ModerationLog(Base): - """Moderation log table.""" - - __tablename__ = "moderation_logs" - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - - # Moderation target (polymorphic) - target_type: Mapped[str] = mapped_column( - String(20), index=True - ) # question, answer, comment - target_id: Mapped[str] = mapped_column(String(36), index=True) - - # Moderation info - moderation_type: Mapped[str] = mapped_column(String(20)) # auto, manual - result: Mapped[ModerationResult] = mapped_column(SAEnum(ModerationResult)) - sensitive_categories: Mapped[str | None] = mapped_column( - Text, nullable=True - ) # JSON array - confidence_score: Mapped[float | None] = mapped_column(Float, nullable=True) - - # Moderation details - reason: Mapped[str | None] = mapped_column(Text, nullable=True) - original_content: Mapped[str | None] = mapped_column( - Text, nullable=True - ) # Store original content - - # Reviewer (for manual review) - reviewer_id: Mapped[str | None] = mapped_column( - String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True - ) - - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - - # Relationships - reviewer: Mapped["User | None"] = relationship("User") diff --git a/backend/app/services/community/moderation/__init__.py b/backend/app/services/community/moderation/__init__.py deleted file mode 100644 index a0b6c751..00000000 --- a/backend/app/services/community/moderation/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Moderation module for content safety.""" - -from .crisis import ( - CRISIS_MESSAGE, - CRISIS_RESOURCES, - CrisisResource, - get_crisis_resources, - trigger_crisis_intervention, -) -from .keywords import KeywordFilter -from .service import ModerationDecision, ModerationService, get_moderation_service - -__all__ = [ - # Service - "ModerationService", - "ModerationDecision", - "get_moderation_service", - # Keywords - "KeywordFilter", - # Crisis - "CrisisResource", - "CRISIS_RESOURCES", - "CRISIS_MESSAGE", - "trigger_crisis_intervention", - "get_crisis_resources", -] diff --git a/backend/app/services/community/moderation/crisis.py b/backend/app/services/community/moderation/crisis.py deleted file mode 100644 index 0550a2f3..00000000 --- a/backend/app/services/community/moderation/crisis.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Crisis intervention module for mental health emergencies.""" - -from dataclasses import dataclass - - -@dataclass -class CrisisResource: - """Crisis intervention resource.""" - - name: str - description: str - hotline: str - url: str | None = None - - -# Crisis intervention resources in China -CRISIS_RESOURCES: list[CrisisResource] = [ - CrisisResource( - name="全国心理援助热线", - description="24小时免费心理援助", - hotline="400-161-9995", - ), - CrisisResource( - name="北京心理危机研究与干预中心", - description="专业心理危机干预", - hotline="010-82951332", - ), - CrisisResource( - name="生命热线", - description="倾听与陪伴", - hotline="400-821-1215", - ), - CrisisResource( - name="希望24热线", - description="全国性心理援助", - hotline="400-161-9995", - ), -] - - -CRISIS_MESSAGE = """ -我们注意到你可能正在经历一段困难的时期。请记住,你并不孤单,有很多人愿意帮助你。 - -如果你正在经历心理困扰,请考虑拨打以下热线寻求帮助: - -📞 全国心理援助热线:400-161-9995(24小时) -📞 生命热线:400-821-1215 -📞 北京心理危机研究与干预中心:010-82951332 - -你的感受很重要,寻求帮助是勇敢的表现。 -""" - - -async def trigger_crisis_intervention( - user_id: str, - content: str, - detected_categories: list[str], -) -> dict: - """ - Trigger crisis intervention when dangerous signals are detected. - - Args: - user_id: User ID who posted the content - content: Original content - detected_categories: List of detected sensitive categories - - Returns: - Crisis intervention response - """ - # TODO: In production, implement: - # 1. Log to special crisis intervention table - # 2. Notify moderators/admins - # 3. Consider notifying emergency contacts if configured - # 4. Track follow-up - - return { - "intervention_triggered": True, - "message": CRISIS_MESSAGE, - "resources": [ - { - "name": r.name, - "description": r.description, - "hotline": r.hotline, - "url": r.url, - } - for r in CRISIS_RESOURCES - ], - "user_id": user_id, - "detected_categories": detected_categories, - } - - -def get_crisis_resources() -> list[dict]: - """Get list of crisis intervention resources.""" - return [ - { - "name": r.name, - "description": r.description, - "hotline": r.hotline, - "url": r.url, - } - for r in CRISIS_RESOURCES - ] diff --git a/backend/app/services/community/moderation/keywords.py b/backend/app/services/community/moderation/keywords.py deleted file mode 100644 index 40d35896..00000000 --- a/backend/app/services/community/moderation/keywords.py +++ /dev/null @@ -1,148 +0,0 @@ -"""Sensitive keyword filter for content moderation.""" - -from ..enums import SensitiveCategory - - -class KeywordFilter: - """Sensitive keyword filter using keyword matching.""" - - # Sensitive keyword dictionary - KEYWORDS: dict[SensitiveCategory, list[str]] = { - SensitiveCategory.PSEUDOSCIENCE: [ - "偏方", - "土方子", - "民间秘方", - "祖传秘方", - "包治百病", - "排毒养颜", - "清宫表", - "酸碱体质", - "以形补形", - "神药", - "祖传", - "秘制", - "包好", - "根治", - ], - SensitiveCategory.DEPRESSION_TRIGGER: [ - "不想活", - "想死", - "活着没意思", - "自杀", - "自残", - "割腕", - "跳楼", - "解脱", - "了结", - "轻生", - "死了算了", - "不如死", - "活不下去", - "去死", - "寻死", - ], - SensitiveCategory.SELF_HARM: [ - "自我伤害", - "伤害自己", - "划手臂", - "烫伤自己", - "故意受伤", - ], - SensitiveCategory.SPAM: [ - "加微信", - "加VX", - "加v", - "加QQ", - "私聊", - "免费领取", - "限时优惠", - "代购", - "招代理", - "兼职赚钱", - "躺赚", - "日入过万", - "扫码领取", - "点击链接", - ], - SensitiveCategory.SOFT_PORNOGRAPHY: [ - "约炮", - "一夜情", - "找小姐", - "开房", - "裸聊", - "色情", - "成人视频", - ], - SensitiveCategory.VIOLENCE: [ - "打死", - "杀了", - "弄死", - "虐待", - "殴打", - "暴打", - ], - SensitiveCategory.HARASSMENT: [ - "傻逼", - "脑残", - "智障", - "垃圾", - "废物", - "去死", - "滚蛋", - "贱人", - ], - SensitiveCategory.MISINFORMATION: [ - "不用吃药", - "不用去医院", - "医生都是骗子", - "疫苗有毒", - "转基因致癌", - ], - SensitiveCategory.POLITICAL: [ - # Intentionally minimal - most political content needs manual review - ], - } - - def scan(self, content: str) -> list[SensitiveCategory]: - """ - Scan text content for sensitive keywords. - - Args: - content: Text content to scan - - Returns: - List of detected sensitive categories - """ - detected: list[SensitiveCategory] = [] - content_lower = content.lower() - - for category, keywords in self.KEYWORDS.items(): - for keyword in keywords: - if keyword.lower() in content_lower: - detected.append(category) - break # One match per category is enough - - return detected - - def get_matched_keywords(self, content: str) -> dict[SensitiveCategory, list[str]]: - """ - Get all matched keywords for each category. - - Args: - content: Text content to scan - - Returns: - Dictionary mapping categories to matched keywords - """ - matched: dict[SensitiveCategory, list[str]] = {} - content_lower = content.lower() - - for category, keywords in self.KEYWORDS.items(): - category_matches = [] - for keyword in keywords: - if keyword.lower() in content_lower: - category_matches.append(keyword) - if category_matches: - matched[category] = category_matches - - return matched diff --git a/backend/app/services/community/moderation/service.py b/backend/app/services/community/moderation/service.py deleted file mode 100644 index 80670efc..00000000 --- a/backend/app/services/community/moderation/service.py +++ /dev/null @@ -1,199 +0,0 @@ -"""Content moderation service.""" - -from typing import NamedTuple - -from ..enums import ModerationResult, SensitiveCategory -from .crisis import trigger_crisis_intervention -from .keywords import KeywordFilter - - -class ModerationDecision(NamedTuple): - """Moderation decision result.""" - - result: ModerationResult - categories: list[SensitiveCategory] - confidence: float - reason: str | None - crisis_intervention: dict | None = None - - -class ModerationService: - """Content moderation service.""" - - # Categories that trigger automatic rejection (high risk) - AUTO_REJECT_CATEGORIES = { - SensitiveCategory.PSEUDOSCIENCE, - SensitiveCategory.SOFT_PORNOGRAPHY, - SensitiveCategory.VIOLENCE, - SensitiveCategory.SPAM, - SensitiveCategory.HARASSMENT, - } - - # Categories that trigger crisis intervention - CRISIS_INTERVENTION_CATEGORIES = { - SensitiveCategory.DEPRESSION_TRIGGER, - SensitiveCategory.SELF_HARM, - } - - # Categories that need manual review - MANUAL_REVIEW_CATEGORIES = { - SensitiveCategory.MISINFORMATION, - SensitiveCategory.POLITICAL, - } - - def __init__(self, keyword_filter: KeywordFilter | None = None): - """ - Initialize moderation service. - - Args: - keyword_filter: Custom keyword filter, uses default if not provided - """ - self._keyword_filter = keyword_filter or KeywordFilter() - - async def moderate_text( - self, - content: str, - user_id: str | None = None, - ) -> ModerationDecision: - """ - Moderate text content. - - Args: - content: Text content to moderate - user_id: User ID for crisis intervention - - Returns: - ModerationDecision with result and details - """ - detected_categories: list[SensitiveCategory] = [] - - # Step 1: Keyword filtering - keyword_matches = self._keyword_filter.scan(content) - detected_categories.extend(keyword_matches) - - # Step 2: TODO - LLM-based analysis (optional enhancement) - # This could be added later for more sophisticated detection - - # Remove duplicates - detected_categories = list(set(detected_categories)) - - # Step 3: Make decision - return await self._make_decision(detected_categories, user_id) - - async def moderate_images( - self, - image_urls: list[str], - ) -> ModerationDecision: - """ - Moderate image content. - - Args: - image_urls: List of image URLs to moderate - - Returns: - ModerationDecision with result and details - - Note: - This is a placeholder. In production, integrate with - image moderation APIs (e.g., Aliyun Content Security, - Tencent Cloud Image Moderation, etc.) - """ - # TODO: Implement actual image moderation - # For now, pass all images - return ModerationDecision( - result=ModerationResult.PASSED, - categories=[], - confidence=1.0, - reason=None, - ) - - async def _make_decision( - self, - categories: list[SensitiveCategory], - user_id: str | None = None, - ) -> ModerationDecision: - """ - Make moderation decision based on detected categories. - - Args: - categories: List of detected sensitive categories - user_id: User ID for crisis intervention - - Returns: - ModerationDecision with appropriate action - """ - if not categories: - return ModerationDecision( - result=ModerationResult.PASSED, - categories=[], - confidence=1.0, - reason=None, - ) - - # Check for crisis intervention categories - crisis_cats = [ - c for c in categories if c in self.CRISIS_INTERVENTION_CATEGORIES - ] - if crisis_cats: - crisis_response = None - if user_id: - crisis_response = await trigger_crisis_intervention( - user_id=user_id, - content="", # Don't log actual content for privacy - detected_categories=[c.value for c in crisis_cats], - ) - return ModerationDecision( - result=ModerationResult.REJECTED, - categories=crisis_cats, - confidence=0.95, - reason="检测到可能存在心理危机,已推送帮助资源。如需发布,请修改内容后重试。", - crisis_intervention=crisis_response, - ) - - # Check for auto-reject categories - reject_cats = [c for c in categories if c in self.AUTO_REJECT_CATEGORIES] - if reject_cats: - reasons = { - SensitiveCategory.PSEUDOSCIENCE: "内容可能包含未经证实的医疗信息", - SensitiveCategory.SOFT_PORNOGRAPHY: "内容包含不适当信息", - SensitiveCategory.VIOLENCE: "内容包含暴力相关信息", - SensitiveCategory.SPAM: "内容疑似广告或垃圾信息", - SensitiveCategory.HARASSMENT: "内容包含不友善言论", - } - reason_parts = [reasons.get(c, "内容违反社区规范") for c in reject_cats] - return ModerationDecision( - result=ModerationResult.REJECTED, - categories=reject_cats, - confidence=0.9, - reason=";".join(set(reason_parts)), - ) - - # Check for manual review categories - manual_cats = [c for c in categories if c in self.MANUAL_REVIEW_CATEGORIES] - if manual_cats: - return ModerationDecision( - result=ModerationResult.NEED_MANUAL_REVIEW, - categories=manual_cats, - confidence=0.7, - reason="内容需要人工审核", - ) - - # Unknown categories - default to pass with lower confidence - return ModerationDecision( - result=ModerationResult.PASSED, - categories=categories, - confidence=0.8, - reason=None, - ) - - -# Module-level service instance (singleton pattern) -_moderation_service: ModerationService | None = None - - -def get_moderation_service() -> ModerationService: - """Get ModerationService singleton instance.""" - global _moderation_service - if _moderation_service is None: - _moderation_service = ModerationService() - return _moderation_service diff --git a/backend/app/services/community/router/__init__.py b/backend/app/services/community/router/__init__.py deleted file mode 100644 index 22acf9f8..00000000 --- a/backend/app/services/community/router/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Router module for community.""" - -from fastapi import APIRouter - -from .answers import router as answers_router -from .certifications import router as certifications_router -from .comments import router as comments_router -from .interactions import router as interactions_router -from .moderation import router as moderation_router -from .questions import router as questions_router -from .tags import router as tags_router -from .user import router as user_router - -# Create main community router -router = APIRouter() - -# Include all sub-routers -router.include_router(questions_router) -router.include_router(answers_router) -router.include_router(comments_router) -router.include_router(interactions_router) -router.include_router(tags_router) -router.include_router(certifications_router) -router.include_router(moderation_router) -router.include_router(user_router) - -__all__ = ["router"] diff --git a/backend/app/services/community/router/answers.py b/backend/app/services/community/router/answers.py deleted file mode 100644 index 92af6e99..00000000 --- a/backend/app/services/community/router/answers.py +++ /dev/null @@ -1,226 +0,0 @@ -"""Answer routes for community module.""" - -import asyncio -from typing import Literal - -from fastapi import APIRouter, HTTPException, Query - -from ..ai_reply import trigger_ai_comment_on_answer -from ..dependencies import ( - CommunityServiceDep, - CurrentUser, - DbSession, - OptionalUser, -) -from ..enums import UserRole -from ..models import Question -from ..schemas import ( - AnswerCreate, - AnswerDetail, - AnswerListItem, - AnswerUpdate, - PaginatedResponse, -) - -router = APIRouter(tags=["Community - Answers"]) - - -@router.get( - "/questions/{question_id}/answers", - response_model=PaginatedResponse[AnswerListItem], -) -async def list_answers( - question_id: str, - db: DbSession, - service: CommunityServiceDep, - current_user: OptionalUser, - is_professional: bool | None = None, - sort_by: Literal["created_at", "like_count"] = "created_at", - order: Literal["asc", "desc"] = "desc", - page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), -) -> PaginatedResponse[AnswerListItem]: - """Get paginated list of answers for a question.""" - return await service.get_answers( - db=db, - question_id=question_id, - is_professional=is_professional, - sort_by=sort_by, - order=order, - page=page, - page_size=page_size, - current_user_id=current_user.id if current_user else None, - ) - - -@router.get( - "/answers/professional/{question_id}", - response_model=PaginatedResponse[AnswerListItem], -) -async def list_professional_answers( - question_id: str, - db: DbSession, - service: CommunityServiceDep, - current_user: OptionalUser, - page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), -) -> PaginatedResponse[AnswerListItem]: - """Get professional answers for a question.""" - return await service.get_answers( - db=db, - question_id=question_id, - is_professional=True, - page=page, - page_size=page_size, - current_user_id=current_user.id if current_user else None, - ) - - -@router.get( - "/answers/experience/{question_id}", - response_model=PaginatedResponse[AnswerListItem], -) -async def list_experience_answers( - question_id: str, - db: DbSession, - service: CommunityServiceDep, - current_user: OptionalUser, - page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), -) -> PaginatedResponse[AnswerListItem]: - """Get experience answers for a question.""" - return await service.get_answers( - db=db, - question_id=question_id, - is_professional=False, - page=page, - page_size=page_size, - current_user_id=current_user.id if current_user else None, - ) - - -@router.post( - "/questions/{question_id}/answers", - response_model=AnswerDetail, - status_code=201, -) -async def create_answer( - question_id: str, - answer_in: AnswerCreate, - db: DbSession, - service: CommunityServiceDep, - current_user: CurrentUser, -) -> AnswerDetail: - """Create a new answer.""" - answer = await service.create_answer(db, question_id, answer_in, current_user) - - # Only trigger AI reply if @贝壳姐姐 is mentioned - # Regular answers to posts do NOT trigger AI reply - if current_user.role != UserRole.AI_ASSISTANT: - if "@贝壳姐姐" in answer_in.content: - # Reply as a comment inside this person's answer floor - await trigger_ai_comment_on_answer(answer.id, question_id) - - # Save to user's chat memory - from app.services.chat.service import save_community_interaction - - question = await db.get(Question, question_id) - question_title = question.title if question else "未知问题" - content_preview = ( - answer_in.content[:50] + "..." - if len(answer_in.content) > 50 - else answer_in.content - ) - interaction = f"回复问题《{question_title}》:{content_preview}" - asyncio.create_task(save_community_interaction(current_user.id, interaction)) - - # Build response - import json - - from ..schemas import AuthorInfo - - return AnswerDetail( - id=answer.id, - question_id=answer.question_id, - author=AuthorInfo( - id=current_user.id, - nickname=current_user.nickname, - avatar_url=current_user.avatar_url, - role=current_user.role, - is_certified=service.is_certified_professional(current_user), - ), - content=answer.content, - image_urls=json.loads(answer.image_urls) if answer.image_urls else [], - author_role=answer.author_role, - is_professional=answer.is_professional, - is_accepted=answer.is_accepted, - status=answer.status, - like_count=answer.like_count, - comment_count=answer.comment_count, - is_liked=False, - created_at=answer.created_at, - updated_at=answer.updated_at, - ) - - -@router.get("/answers/{answer_id}", response_model=AnswerDetail) -async def get_answer( - answer_id: str, - db: DbSession, - service: CommunityServiceDep, - current_user: OptionalUser, -) -> AnswerDetail: - """Get answer detail by ID.""" - # TODO: Implement get answer detail - raise HTTPException(status_code=501, detail="功能开发中") - - -@router.put("/answers/{answer_id}", response_model=AnswerDetail) -async def update_answer( - answer_id: str, - answer_in: AnswerUpdate, - db: DbSession, - service: CommunityServiceDep, - current_user: CurrentUser, -) -> AnswerDetail: - """Update an answer (author or admin only).""" - import json - - from ..schemas import AuthorInfo - - answer = await service.update_answer(db, answer_id, answer_in, current_user) - - # Build response - return AnswerDetail( - id=answer.id, - question_id=answer.question_id, - author=AuthorInfo( - id=answer.author.id, - nickname=answer.author.nickname, - avatar_url=answer.author.avatar_url, - role=answer.author.role, - is_certified=service.is_certified_professional(answer.author), - ), - content=answer.content, - image_urls=json.loads(answer.image_urls) if answer.image_urls else [], - author_role=answer.author_role, - is_professional=answer.is_professional, - is_accepted=answer.is_accepted, - status=answer.status, - like_count=answer.like_count, - comment_count=answer.comment_count, - is_liked=False, - created_at=answer.created_at, - updated_at=answer.updated_at, - ) - - -@router.delete("/answers/{answer_id}", status_code=204) -async def delete_answer( - answer_id: str, - db: DbSession, - service: CommunityServiceDep, - current_user: CurrentUser, -) -> None: - """Delete an answer (author, question author, or admin only).""" - await service.delete_answer(db, answer_id, current_user) diff --git a/backend/app/services/community/router/certifications.py b/backend/app/services/community/router/certifications.py deleted file mode 100644 index e394a90f..00000000 --- a/backend/app/services/community/router/certifications.py +++ /dev/null @@ -1,285 +0,0 @@ -"""Certification routes for community module.""" - -from fastapi import APIRouter, HTTPException, Query - -from ..dependencies import ( - AdminUser, - CurrentUser, - DbSession, -) -from ..enums import PROFESSIONAL_ROLES, CertificationStatus, UserRole -from ..schemas import ( - CertificationCreate, - CertificationListItem, - CertificationReview, - CertificationStatus_, - PaginatedResponse, -) - -router = APIRouter(prefix="/certifications", tags=["Community - Certifications"]) - - -@router.post("", response_model=CertificationStatus_, status_code=201) -async def create_certification( - cert_in: CertificationCreate, - db: DbSession, - current_user: CurrentUser, -) -> CertificationStatus_: - """Submit a certification application.""" - import json - - from ..models import UserCertification - - # Check if certification type is valid - if cert_in.certification_type not in PROFESSIONAL_ROLES: - raise HTTPException( - status_code=400, - detail="认证类型必须是: certified_doctor, certified_therapist, certified_nurse", - ) - - # Check if user already has a certification - if current_user.certification: - if current_user.certification.status == CertificationStatus.PENDING: - raise HTTPException(status_code=400, detail="您已有待审核的认证申请") - if current_user.certification.status == CertificationStatus.APPROVED: - raise HTTPException(status_code=400, detail="您已通过认证") - - # Create certification - cert = UserCertification( - user_id=current_user.id, - certification_type=cert_in.certification_type, - real_name=cert_in.real_name, - id_card_number=cert_in.id_card_number, - license_number=cert_in.license_number, - hospital_or_institution=cert_in.hospital_or_institution, - department=cert_in.department, - title=cert_in.title, - license_image_url=cert_in.license_image_url, - id_card_image_url=cert_in.id_card_image_url, - additional_docs_urls=( - json.dumps(cert_in.additional_docs_urls) - if cert_in.additional_docs_urls - else None - ), - status=CertificationStatus.PENDING, - ) - db.add(cert) - await db.commit() - await db.refresh(cert) - - return CertificationStatus_( - id=cert.id, - user_id=cert.user_id, - certification_type=cert.certification_type, - real_name=cert.real_name, - license_number=cert.license_number, - hospital_or_institution=cert.hospital_or_institution, - department=cert.department, - title=cert.title, - status=cert.status, - review_comment=cert.review_comment, - reviewed_at=cert.reviewed_at, - valid_from=cert.valid_from, - valid_until=cert.valid_until, - created_at=cert.created_at, - updated_at=cert.updated_at, - ) - - -@router.get("/my", response_model=CertificationStatus_ | None) -async def get_my_certification( - db: DbSession, - current_user: CurrentUser, -) -> CertificationStatus_ | None: - """Get current user's certification status.""" - cert = current_user.certification - if not cert: - return None - - return CertificationStatus_( - id=cert.id, - user_id=cert.user_id, - certification_type=cert.certification_type, - real_name=cert.real_name, - license_number=cert.license_number, - hospital_or_institution=cert.hospital_or_institution, - department=cert.department, - title=cert.title, - status=cert.status, - review_comment=cert.review_comment, - reviewed_at=cert.reviewed_at, - valid_from=cert.valid_from, - valid_until=cert.valid_until, - created_at=cert.created_at, - updated_at=cert.updated_at, - ) - - -@router.get("", response_model=PaginatedResponse[CertificationListItem]) -async def list_certifications( - db: DbSession, - admin: AdminUser, - status: CertificationStatus | None = None, - page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), -) -> PaginatedResponse[CertificationListItem]: - """Get list of certification applications (admin only).""" - from sqlalchemy import func, select - from sqlalchemy.orm import selectinload - - from ..models import UserCertification - - query = select(UserCertification).options(selectinload(UserCertification.user)) - - if status: - query = query.where(UserCertification.status == status) - - # Count total - count_query = select(func.count()).select_from(query.subquery()) - total = (await db.execute(count_query)).scalar() or 0 - - # Order by created_at desc - query = query.order_by(UserCertification.created_at.desc()) - query = query.offset((page - 1) * page_size).limit(page_size) - - result = await db.execute(query) - certs = result.scalars().all() - - items = [ - CertificationListItem( - id=c.id, - user_id=c.user_id, - user_nickname=c.user.nickname if c.user else "Unknown", - certification_type=c.certification_type, - real_name=c.real_name, - license_number=c.license_number, - hospital_or_institution=c.hospital_or_institution, - status=c.status, - created_at=c.created_at, - ) - for c in certs - ] - - return PaginatedResponse( - items=items, - total=total, - page=page, - page_size=page_size, - total_pages=(total + page_size - 1) // page_size, - ) - - -@router.put("/{cert_id}/review", response_model=CertificationStatus_) -async def review_certification( - cert_id: str, - review_in: CertificationReview, - db: DbSession, - admin: AdminUser, -) -> CertificationStatus_: - """Review a certification application (admin only).""" - from datetime import datetime - - from ..models import User, UserCertification - - cert = await db.get(UserCertification, cert_id) - if not cert: - raise HTTPException(status_code=404, detail="认证申请不存在") - - if cert.status != CertificationStatus.PENDING: - raise HTTPException(status_code=400, detail="该申请已被处理") - - if review_in.status not in ( - CertificationStatus.APPROVED, - CertificationStatus.REJECTED, - ): - raise HTTPException( - status_code=400, detail="审核状态必须是: approved 或 rejected" - ) - - # Update certification - cert.status = review_in.status - cert.review_comment = review_in.review_comment - cert.reviewer_id = admin.id - cert.reviewed_at = datetime.utcnow() - - if review_in.status == CertificationStatus.APPROVED: - cert.valid_from = review_in.valid_from or datetime.utcnow() - cert.valid_until = review_in.valid_until - - # Update user role (but keep admin role if user is admin) - user = await db.get(User, cert.user_id) - if user and user.role != UserRole.ADMIN: - user.role = cert.certification_type - - await db.commit() - await db.refresh(cert) - - return CertificationStatus_( - id=cert.id, - user_id=cert.user_id, - certification_type=cert.certification_type, - real_name=cert.real_name, - license_number=cert.license_number, - hospital_or_institution=cert.hospital_or_institution, - department=cert.department, - title=cert.title, - status=cert.status, - review_comment=cert.review_comment, - reviewed_at=cert.reviewed_at, - valid_from=cert.valid_from, - valid_until=cert.valid_until, - created_at=cert.created_at, - updated_at=cert.updated_at, - ) - - -@router.put("/{cert_id}/revoke", response_model=CertificationStatus_) -async def revoke_certification( - cert_id: str, - db: DbSession, - admin: AdminUser, - reason: str | None = None, -) -> CertificationStatus_: - """Revoke an approved certification (admin only).""" - from datetime import datetime - - from ..models import User, UserCertification - - cert = await db.get(UserCertification, cert_id) - if not cert: - raise HTTPException(status_code=404, detail="认证申请不存在") - - if cert.status != CertificationStatus.APPROVED: - raise HTTPException(status_code=400, detail="只能撤销已通过的认证") - - # Update certification status - cert.status = CertificationStatus.REVOKED - cert.review_comment = reason or "认证已被管理员撤销" - cert.reviewer_id = admin.id - cert.reviewed_at = datetime.utcnow() - - # Reset user role to MOM (default) if not admin - user = await db.get(User, cert.user_id) - if user and user.role != UserRole.ADMIN: - user.role = UserRole.MOM - - await db.commit() - await db.refresh(cert) - - return CertificationStatus_( - id=cert.id, - user_id=cert.user_id, - certification_type=cert.certification_type, - real_name=cert.real_name, - license_number=cert.license_number, - hospital_or_institution=cert.hospital_or_institution, - department=cert.department, - title=cert.title, - status=cert.status, - review_comment=cert.review_comment, - reviewed_at=cert.reviewed_at, - valid_from=cert.valid_from, - valid_until=cert.valid_until, - created_at=cert.created_at, - updated_at=cert.updated_at, - ) diff --git a/backend/app/services/community/router/comments.py b/backend/app/services/community/router/comments.py deleted file mode 100644 index b91bb439..00000000 --- a/backend/app/services/community/router/comments.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Comment routes for community module.""" - -import asyncio - -from fastapi import APIRouter - -from ..ai_reply import trigger_ai_reply_to_ai_content, trigger_ai_reply_to_comment -from ..dependencies import ( - CommunityServiceDep, - CurrentUser, - DbSession, - OptionalUser, -) -from ..enums import UserRole -from ..models import Answer, Comment -from ..schemas import CommentCreate, CommentListItem - -router = APIRouter(tags=["Community - Comments"]) - - -@router.get("/answers/{answer_id}/comments", response_model=list[CommentListItem]) -async def list_comments( - answer_id: str, - db: DbSession, - service: CommunityServiceDep, - current_user: OptionalUser, -) -> list[CommentListItem]: - """Get comments for an answer.""" - user_id = current_user.id if current_user else None - return await service.get_comments(db, answer_id, user_id) - - -@router.post( - "/answers/{answer_id}/comments", - response_model=CommentListItem, - status_code=201, -) -async def create_comment( - answer_id: str, - comment_in: CommentCreate, - db: DbSession, - service: CommunityServiceDep, - current_user: CurrentUser, -) -> CommentListItem: - """Create a new comment on an answer.""" - result = await service.create_comment(db, answer_id, current_user, comment_in) - - # Skip AI reply logic if the commenter is AI - if current_user.role == UserRole.AI_ASSISTANT: - return result - - # Save to user's chat memory - from app.services.chat.service import save_community_interaction - - content_preview = ( - comment_in.content[:50] + "..." - if len(comment_in.content) > 50 - else comment_in.content - ) - interaction = f"评论:{content_preview}" - asyncio.create_task(save_community_interaction(current_user.id, interaction)) - - # Trigger AI reply if comment mentions @贝壳姐姐 - if "@贝壳姐姐" in comment_in.content: - await trigger_ai_reply_to_comment(result.id, answer_id) - return result - - # Check if commenting on AI's answer or replying to AI's comment - # Get the answer to check its author - answer = await db.get(Answer, answer_id) - if answer and answer.author_role == UserRole.AI_ASSISTANT: - # Commenting on AI's answer - await trigger_ai_reply_to_ai_content(result.id, answer_id, comment_in.parent_id) - elif comment_in.parent_id: - # Check if replying to AI's comment - parent_comment = await db.get(Comment, comment_in.parent_id) - if parent_comment: - from ..models import User - - parent_author = await db.get(User, parent_comment.author_id) - if parent_author and parent_author.role == UserRole.AI_ASSISTANT: - await trigger_ai_reply_to_ai_content( - result.id, answer_id, comment_in.parent_id - ) - - return result - - -@router.delete("/comments/{comment_id}", status_code=204) -async def delete_comment( - comment_id: str, - db: DbSession, - service: CommunityServiceDep, - current_user: CurrentUser, -) -> None: - """Delete a comment (author or admin only).""" - await service.delete_comment(db, comment_id, current_user) diff --git a/backend/app/services/community/router/interactions.py b/backend/app/services/community/router/interactions.py deleted file mode 100644 index 1de55a49..00000000 --- a/backend/app/services/community/router/interactions.py +++ /dev/null @@ -1,165 +0,0 @@ -"""Interaction routes (likes, collections) for community module.""" - -from typing import Literal - -from fastapi import APIRouter, Query - -from ..dependencies import ( - CommunityServiceDep, - CurrentUser, - DbSession, -) -from ..schemas import ( - CollectionCreate, - CollectionItem, - LikeCreate, - LikeDelete, - LikeStatus, - PaginatedResponse, -) - -router = APIRouter(tags=["Community - Interactions"]) - - -# ==================== Likes ==================== - - -@router.post("/likes", response_model=LikeStatus) -async def create_like( - like_in: LikeCreate, - db: DbSession, - service: CommunityServiceDep, - current_user: CurrentUser, -) -> LikeStatus: - """Like a question, answer, or comment.""" - is_liked, like_count = await service.toggle_like( - db=db, - user_id=current_user.id, - target_type=like_in.target_type, - target_id=like_in.target_id, - ) - return LikeStatus(is_liked=is_liked, like_count=like_count) - - -@router.delete("/likes", response_model=LikeStatus) -async def delete_like( - like_in: LikeDelete, - db: DbSession, - service: CommunityServiceDep, - current_user: CurrentUser, -) -> LikeStatus: - """Unlike a question, answer, or comment.""" - is_liked, like_count = await service.toggle_like( - db=db, - user_id=current_user.id, - target_type=like_in.target_type, - target_id=like_in.target_id, - ) - return LikeStatus(is_liked=is_liked, like_count=like_count) - - -@router.get("/likes/status", response_model=LikeStatus) -async def get_like_status( - target_type: Literal["question", "answer", "comment"], - target_id: str, - db: DbSession, - service: CommunityServiceDep, - current_user: CurrentUser, -) -> LikeStatus: - """Check if current user has liked a target.""" - from sqlalchemy import select - - from ..models import Answer, Comment, Like, Question - - # Check like status - query = select(Like).where( - Like.user_id == current_user.id, - Like.target_type == target_type, - Like.target_id == target_id, - ) - like = (await db.execute(query)).scalar_one_or_none() - - # Get like count - model_map = { - "question": Question, - "answer": Answer, - "comment": Comment, - } - model = model_map[target_type] - target = await db.get(model, target_id) - - return LikeStatus( - is_liked=like is not None, - like_count=target.like_count if target else 0, # type: ignore[attr-defined] - ) - - -# ==================== Collections ==================== - - -@router.post("/collections", response_model=dict) -async def create_collection( - collection_in: CollectionCreate, - db: DbSession, - service: CommunityServiceDep, - current_user: CurrentUser, -) -> dict: - """Collect a question.""" - is_collected, collection_count = await service.toggle_collection( - db=db, - user_id=current_user.id, - question_id=collection_in.question_id, - folder_name=collection_in.folder_name, - note=collection_in.note, - ) - return { - "is_collected": is_collected, - "collection_count": collection_count, - } - - -@router.delete("/collections/{collection_id}", status_code=204) -async def delete_collection( - collection_id: str, - db: DbSession, - service: CommunityServiceDep, - current_user: CurrentUser, -) -> None: - """Remove a collection.""" - from ..models import Collection - - collection = await db.get(Collection, collection_id) - if not collection: - from fastapi import HTTPException - - raise HTTPException(status_code=404, detail="收藏不存在") - - if collection.user_id != current_user.id: - from fastapi import HTTPException - - raise HTTPException(status_code=403, detail="无权操作") - - await service.toggle_collection( - db=db, - user_id=current_user.id, - question_id=collection.question_id, - ) - - -@router.get("/collections/my", response_model=PaginatedResponse[CollectionItem]) -async def list_my_collections( - db: DbSession, - service: CommunityServiceDep, - current_user: CurrentUser, - folder_name: str | None = None, - page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), -) -> PaginatedResponse[CollectionItem]: - """Get current user's collections.""" - return await service.get_user_collections( - db=db, - user_id=current_user.id, - folder_name=folder_name, - page=page, - page_size=page_size, - ) diff --git a/backend/app/services/community/router/moderation.py b/backend/app/services/community/router/moderation.py deleted file mode 100644 index 15954cf7..00000000 --- a/backend/app/services/community/router/moderation.py +++ /dev/null @@ -1,261 +0,0 @@ -"""Moderation routes for community module.""" - -from fastapi import APIRouter, HTTPException, Query - -from ..dependencies import AdminUser, DbSession -from ..enums import ContentStatus, ModerationResult - -router = APIRouter(prefix="/moderation", tags=["Community - Moderation"]) - - -@router.get("/pending") -async def list_pending_content( - db: DbSession, - admin: AdminUser, - content_type: str | None = None, # question, answer, comment - page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), -) -> dict: - """Get list of content pending moderation (admin only).""" - from sqlalchemy import select - - from ..models import Answer, Comment, Question - - items: list[dict] = [] - - # Get pending questions - if content_type is None or content_type == "question": - q_query = select(Question).where( - Question.status == ContentStatus.PENDING_REVIEW - ) - result = await db.execute(q_query) - questions = result.scalars().all() - for q in questions: - items.append( - { - "type": "question", - "id": q.id, - "author_id": q.author_id, - "title": q.title, - "content_preview": q.content[:200] if q.content else "", - "created_at": q.created_at.isoformat(), - } - ) - - # Get pending answers - if content_type is None or content_type == "answer": - a_query = select(Answer).where(Answer.status == ContentStatus.PENDING_REVIEW) - result = await db.execute(a_query) - answers = result.scalars().all() - for a in answers: - items.append( - { - "type": "answer", - "id": a.id, - "author_id": a.author_id, - "question_id": a.question_id, # type: ignore[attr-defined] - "content_preview": a.content[:200] if a.content else "", - "created_at": a.created_at.isoformat(), - } - ) - - # Get pending comments - if content_type is None or content_type == "comment": - c_query = select(Comment).where(Comment.status == ContentStatus.PENDING_REVIEW) - result = await db.execute(c_query) - comments = result.scalars().all() - for c in comments: - items.append( - { - "type": "comment", - "id": c.id, - "author_id": c.author_id, - "answer_id": c.answer_id, # type: ignore[attr-defined] - "content_preview": c.content[:200] if c.content else "", - "created_at": c.created_at.isoformat(), - } - ) - - # Sort by created_at - items.sort(key=lambda x: x["created_at"], reverse=True) - - # Paginate - total = len(items) - start = (page - 1) * page_size - end = start + page_size - paginated_items = items[start:end] - - return { - "items": paginated_items, - "total": total, - "page": page, - "page_size": page_size, - "total_pages": (total + page_size - 1) // page_size, - } - - -@router.post("/{content_type}/{content_id}/approve") -async def approve_content( - content_type: str, - content_id: str, - db: DbSession, - admin: AdminUser, -) -> dict: - """Approve pending content (admin only).""" - from datetime import datetime - - from ..models import Answer, Comment, ModerationLog, Question - - model_map = { - "question": Question, - "answer": Answer, - "comment": Comment, - } - - if content_type not in model_map: - raise HTTPException(status_code=400, detail="无效的内容类型") - - model = model_map[content_type] - content = await db.get(model, content_id) - - if not content: - raise HTTPException(status_code=404, detail="内容不存在") - - if content.status != ContentStatus.PENDING_REVIEW: # type: ignore[attr-defined] - raise HTTPException(status_code=400, detail="内容不在待审核状态") - - # Update status - content.status = ContentStatus.PUBLISHED # type: ignore[attr-defined] - if hasattr(content, "published_at"): - content.published_at = datetime.utcnow() - - # Log moderation - db.add( - ModerationLog( - target_type=content_type, - target_id=content_id, - moderation_type="manual", - result=ModerationResult.PASSED, - reviewer_id=admin.id, - ) - ) - - await db.commit() - - return { - "status": "approved", - "content_type": content_type, - "content_id": content_id, - } - - -@router.post("/{content_type}/{content_id}/reject") -async def reject_content( - content_type: str, - content_id: str, - reason: str | None = None, - db: DbSession = None, # type: ignore[assignment] - admin: AdminUser = None, # type: ignore[assignment] -) -> dict: - """Reject pending content (admin only).""" - from ..models import Answer, Comment, ModerationLog, Question - - model_map = { - "question": Question, - "answer": Answer, - "comment": Comment, - } - - if content_type not in model_map: - raise HTTPException(status_code=400, detail="无效的内容类型") - - model = model_map[content_type] - content = await db.get(model, content_id) - - if not content: - raise HTTPException(status_code=404, detail="内容不存在") - - if content.status != ContentStatus.PENDING_REVIEW: # type: ignore[attr-defined] - raise HTTPException(status_code=400, detail="内容不在待审核状态") - - # Update status - content.status = ContentStatus.HIDDEN # type: ignore[attr-defined] - - # Log moderation - db.add( - ModerationLog( - target_type=content_type, - target_id=content_id, - moderation_type="manual", - result=ModerationResult.REJECTED, - reason=reason, - original_content=content.content if hasattr(content, "content") else None, - reviewer_id=admin.id, - ) - ) - - await db.commit() - - return { - "status": "rejected", - "content_type": content_type, - "content_id": content_id, - } - - -@router.get("/logs") -async def list_moderation_logs( - db: DbSession, - admin: AdminUser, - content_type: str | None = None, - result: ModerationResult | None = None, - page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), -) -> dict: - """Get moderation logs (admin only).""" - from sqlalchemy import func, select - - from ..models import ModerationLog - - query = select(ModerationLog) - - if content_type: - query = query.where(ModerationLog.target_type == content_type) - - if result: - query = query.where(ModerationLog.result == result) - - # Count total - count_query = select(func.count()).select_from(query.subquery()) - total = (await db.execute(count_query)).scalar() or 0 - - # Order and paginate - query = query.order_by(ModerationLog.created_at.desc()) - query = query.offset((page - 1) * page_size).limit(page_size) - - db_result = await db.execute(query) - logs = db_result.scalars().all() - - items = [ - { - "id": log.id, - "target_type": log.target_type, - "target_id": log.target_id, - "moderation_type": log.moderation_type, - "result": log.result.value, - "sensitive_categories": log.sensitive_categories, - "confidence_score": log.confidence_score, - "reason": log.reason, - "reviewer_id": log.reviewer_id, - "created_at": log.created_at.isoformat(), - } - for log in logs - ] - - return { - "items": items, - "total": total, - "page": page, - "page_size": page_size, - "total_pages": (total + page_size - 1) // page_size, - } diff --git a/backend/app/services/community/router/questions.py b/backend/app/services/community/router/questions.py deleted file mode 100644 index e92be4eb..00000000 --- a/backend/app/services/community/router/questions.py +++ /dev/null @@ -1,191 +0,0 @@ -"""Question routes for community module.""" - -import asyncio -from typing import Literal - -from fastapi import APIRouter, HTTPException, Query - -from ..ai_reply import trigger_ai_reply_to_question -from ..dependencies import ( - CommunityServiceDep, - CurrentUser, - DbSession, - OptionalUser, -) -from ..enums import ChannelType, UserRole -from ..schemas import ( - PaginatedResponse, - QuestionCreate, - QuestionDetail, - QuestionListItem, - QuestionUpdate, -) - -router = APIRouter(prefix="/questions", tags=["Community - Questions"]) - - -@router.get("", response_model=PaginatedResponse[QuestionListItem]) -async def list_questions( - db: DbSession, - service: CommunityServiceDep, - current_user: OptionalUser, - channel: ChannelType | None = None, - tag_id: str | None = None, - sort_by: Literal[ - "created_at", "view_count", "answer_count", "like_count" - ] = "created_at", - order: Literal["asc", "desc"] = "desc", - page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), -) -> PaginatedResponse[QuestionListItem]: - """Get paginated list of questions.""" - return await service.get_questions( - db=db, - channel=channel, - tag_id=tag_id, - sort_by=sort_by, - order=order, - page=page, - page_size=page_size, - current_user_id=current_user.id if current_user else None, - ) - - -@router.get("/hot", response_model=PaginatedResponse[QuestionListItem]) -async def list_hot_questions( - db: DbSession, - service: CommunityServiceDep, - current_user: OptionalUser, - page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), -) -> PaginatedResponse[QuestionListItem]: - """Get hot questions sorted by view count.""" - return await service.get_questions( - db=db, - sort_by="view_count", - order="desc", - page=page, - page_size=page_size, - current_user_id=current_user.id if current_user else None, - ) - - -@router.get("/channel/{channel}", response_model=PaginatedResponse[QuestionListItem]) -async def list_questions_by_channel( - channel: ChannelType, - db: DbSession, - service: CommunityServiceDep, - current_user: OptionalUser, - page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), -) -> PaginatedResponse[QuestionListItem]: - """Get questions by channel.""" - return await service.get_questions( - db=db, - channel=channel, - page=page, - page_size=page_size, - current_user_id=current_user.id if current_user else None, - ) - - -@router.get("/{question_id}", response_model=QuestionDetail) -async def get_question( - question_id: str, - db: DbSession, - service: CommunityServiceDep, - current_user: OptionalUser, -) -> QuestionDetail: - """Get question detail by ID.""" - question = await service.get_question( - db=db, - question_id=question_id, - current_user_id=current_user.id if current_user else None, - ) - if not question: - raise HTTPException(status_code=404, detail="问题不存在") - return question - - -@router.post("", response_model=QuestionDetail, status_code=201) -async def create_question( - question_in: QuestionCreate, - db: DbSession, - service: CommunityServiceDep, - current_user: CurrentUser, -) -> QuestionDetail: - """Create a new question.""" - question = await service.create_question(db, question_in, current_user) - - # Trigger AI auto-reply in background - await trigger_ai_reply_to_question(question.id) - - # Save to user's chat memory (for non-AI users) - if current_user.role != UserRole.AI_ASSISTANT: - from app.services.chat.service import save_community_interaction - - content_preview = ( - question_in.content[:50] + "..." - if len(question_in.content) > 50 - else question_in.content - ) - interaction = f"发帖:《{question_in.title}》- {content_preview}" - asyncio.create_task(save_community_interaction(current_user.id, interaction)) - - # Return full detail - detail = await service.get_question( - db=db, - question_id=question.id, - current_user_id=current_user.id, - increment_view=False, - ) - if not detail: - raise HTTPException(status_code=500, detail="创建问题失败") - return detail - - -@router.put("/{question_id}", response_model=QuestionDetail) -async def update_question( - question_id: str, - question_in: QuestionUpdate, - db: DbSession, - service: CommunityServiceDep, - current_user: CurrentUser, -) -> QuestionDetail: - """Update a question (author or admin only).""" - await service.update_question(db, question_id, question_in, current_user) - - # Return full detail - detail = await service.get_question( - db=db, - question_id=question_id, - current_user_id=current_user.id, - increment_view=False, - ) - if not detail: - raise HTTPException(status_code=500, detail="更新问题失败") - return detail - - -@router.delete("/{question_id}", status_code=204) -async def delete_question( - question_id: str, - db: DbSession, - service: CommunityServiceDep, - current_user: CurrentUser, -) -> None: - """Delete a question (author or admin only).""" - await service.delete_question(db, question_id, current_user) - - -@router.post("/{question_id}/accept/{answer_id}", status_code=200) -async def accept_answer( - question_id: str, - answer_id: str, - db: DbSession, - service: CommunityServiceDep, - current_user: CurrentUser, -) -> dict: - """Accept an answer (question author only).""" - # TODO: Implement accept logic - raise HTTPException(status_code=501, detail="功能开发中") diff --git a/backend/app/services/community/router/tags.py b/backend/app/services/community/router/tags.py deleted file mode 100644 index 2bbd5156..00000000 --- a/backend/app/services/community/router/tags.py +++ /dev/null @@ -1,203 +0,0 @@ -"""Tag routes for community module.""" - -from fastapi import APIRouter, HTTPException, Query - -from ..dependencies import ( - AdminUser, - DbSession, -) -from ..schemas import TagCreate, TagDetail, TagListItem, TagUpdate - -router = APIRouter(prefix="/tags", tags=["Community - Tags"]) - - -@router.get("", response_model=list[TagListItem]) -async def list_tags( - db: DbSession, - is_featured: bool | None = None, - page: int = Query(1, ge=1), - page_size: int = Query(50, ge=1, le=100), -) -> list[TagListItem]: - """Get list of tags.""" - from sqlalchemy import select - - from ..models import Tag - - query = select(Tag).where(Tag.is_active.is_(True)) - - if is_featured is not None: - query = query.where(Tag.is_featured == is_featured) - - query = query.order_by(Tag.question_count.desc()) - query = query.offset((page - 1) * page_size).limit(page_size) - - result = await db.execute(query) - tags = result.scalars().all() - - return [ - TagListItem( - id=t.id, - name=t.name, - slug=t.slug, - description=t.description, - question_count=t.question_count, - follower_count=t.follower_count, - is_featured=t.is_featured, - ) - for t in tags - ] - - -@router.get("/hot", response_model=list[TagListItem]) -async def list_hot_tags( - db: DbSession, - limit: int = Query(10, ge=1, le=50), -) -> list[TagListItem]: - """Get hot tags sorted by question count.""" - from sqlalchemy import select - - from ..models import Tag - - query = ( - select(Tag) - .where(Tag.is_active.is_(True)) - .order_by(Tag.question_count.desc()) - .limit(limit) - ) - - result = await db.execute(query) - tags = result.scalars().all() - - return [ - TagListItem( - id=t.id, - name=t.name, - slug=t.slug, - description=t.description, - question_count=t.question_count, - follower_count=t.follower_count, - is_featured=t.is_featured, - ) - for t in tags - ] - - -@router.get("/{tag_id}", response_model=TagDetail) -async def get_tag( - tag_id: str, - db: DbSession, -) -> TagDetail: - """Get tag detail by ID.""" - from ..models import Tag - - tag = await db.get(Tag, tag_id) - if not tag: - raise HTTPException(status_code=404, detail="标签不存在") - - return TagDetail( - id=tag.id, - name=tag.name, - slug=tag.slug, - description=tag.description, - question_count=tag.question_count, - follower_count=tag.follower_count, - is_featured=tag.is_featured, - is_active=tag.is_active, - created_at=tag.created_at, - ) - - -@router.post("", response_model=TagDetail, status_code=201) -async def create_tag( - tag_in: TagCreate, - db: DbSession, - admin: AdminUser, -) -> TagDetail: - """Create a new tag (admin only).""" - from sqlalchemy import select - - from ..models import Tag - - # Check if tag already exists - query = select(Tag).where((Tag.name == tag_in.name) | (Tag.slug == tag_in.slug)) - existing = (await db.execute(query)).scalar_one_or_none() - if existing: - raise HTTPException(status_code=400, detail="标签名或 slug 已存在") - - tag = Tag( - name=tag_in.name, - slug=tag_in.slug, - description=tag_in.description, - is_featured=tag_in.is_featured, - ) - db.add(tag) - await db.commit() - await db.refresh(tag) - - return TagDetail( - id=tag.id, - name=tag.name, - slug=tag.slug, - description=tag.description, - question_count=tag.question_count, - follower_count=tag.follower_count, - is_featured=tag.is_featured, - is_active=tag.is_active, - created_at=tag.created_at, - ) - - -@router.put("/{tag_id}", response_model=TagDetail) -async def update_tag( - tag_id: str, - tag_in: TagUpdate, - db: DbSession, - admin: AdminUser, -) -> TagDetail: - """Update a tag (admin only).""" - from ..models import Tag - - tag = await db.get(Tag, tag_id) - if not tag: - raise HTTPException(status_code=404, detail="标签不存在") - - if tag_in.name is not None: - tag.name = tag_in.name - if tag_in.description is not None: - tag.description = tag_in.description - if tag_in.is_featured is not None: - tag.is_featured = tag_in.is_featured - if tag_in.is_active is not None: - tag.is_active = tag_in.is_active - - await db.commit() - await db.refresh(tag) - - return TagDetail( - id=tag.id, - name=tag.name, - slug=tag.slug, - description=tag.description, - question_count=tag.question_count, - follower_count=tag.follower_count, - is_featured=tag.is_featured, - is_active=tag.is_active, - created_at=tag.created_at, - ) - - -@router.delete("/{tag_id}", status_code=204) -async def delete_tag( - tag_id: str, - db: DbSession, - admin: AdminUser, -) -> None: - """Delete a tag (admin only). Actually soft-deletes by setting is_active=False.""" - from ..models import Tag - - tag = await db.get(Tag, tag_id) - if not tag: - raise HTTPException(status_code=404, detail="标签不存在") - - tag.is_active = False - await db.commit() diff --git a/backend/app/services/community/router/user.py b/backend/app/services/community/router/user.py deleted file mode 100644 index a40ebdc0..00000000 --- a/backend/app/services/community/router/user.py +++ /dev/null @@ -1,73 +0,0 @@ -"""User routes for community module.""" - -from fastapi import APIRouter, Query - -from ..dependencies import ( - CommunityServiceDep, - CurrentUser, - DbSession, -) -from ..schemas import ( - MyAnswerListItem, - MyQuestionListItem, - PaginatedResponse, - UserProfile, - UserProfileUpdate, -) - -router = APIRouter(prefix="/users", tags=["Community - Users"]) - - -@router.get("/me", response_model=UserProfile) -async def get_my_profile( - db: DbSession, - service: CommunityServiceDep, - current_user: CurrentUser, -) -> UserProfile: - """Get current user's profile with statistics.""" - return await service.get_user_profile(db, current_user) - - -@router.put("/me", response_model=UserProfile) -async def update_my_profile( - profile_update: UserProfileUpdate, - db: DbSession, - service: CommunityServiceDep, - current_user: CurrentUser, -) -> UserProfile: - """Update current user's profile (nickname/avatar).""" - return await service.update_user_profile(db, current_user, profile_update) - - -@router.get("/me/questions", response_model=PaginatedResponse[MyQuestionListItem]) -async def get_my_questions( - db: DbSession, - service: CommunityServiceDep, - current_user: CurrentUser, - page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), -) -> PaginatedResponse[MyQuestionListItem]: - """Get questions created by current user.""" - return await service.get_user_questions( - db=db, - user_id=current_user.id, - page=page, - page_size=page_size, - ) - - -@router.get("/me/answers", response_model=PaginatedResponse[MyAnswerListItem]) -async def get_my_answers( - db: DbSession, - service: CommunityServiceDep, - current_user: CurrentUser, - page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), -) -> PaginatedResponse[MyAnswerListItem]: - """Get answers created by current user.""" - return await service.get_user_answers( - db=db, - user_id=current_user.id, - page=page, - page_size=page_size, - ) diff --git a/backend/app/services/community/schemas/__init__.py b/backend/app/services/community/schemas/__init__.py deleted file mode 100644 index 3919367c..00000000 --- a/backend/app/services/community/schemas/__init__.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Schemas module for community.""" - -from .answers import AnswerCreate, AnswerDetail, AnswerListItem, AnswerUpdate -from .base import AuthorInfo, PaginatedResponse, TagInfo, TimestampMixin -from .certifications import ( - CertificationCreate, - CertificationListItem, - CertificationReview, - CertificationStatus_, -) -from .comments import CommentCreate, CommentListItem -from .interactions import ( - CollectionCreate, - CollectionItem, - CollectionUpdate, - LikeCreate, - LikeDelete, - LikeStatus, -) -from .questions import QuestionCreate, QuestionDetail, QuestionListItem, QuestionUpdate -from .tags import TagCreate, TagDetail, TagListItem, TagUpdate -from .user import ( - MyAnswerListItem, - MyQuestionListItem, - QuestionBrief, - UserProfile, - UserProfileUpdate, - UserStats, -) - -__all__ = [ - # Base - "PaginatedResponse", - "AuthorInfo", - "TagInfo", - "TimestampMixin", - # Questions - "QuestionCreate", - "QuestionUpdate", - "QuestionListItem", - "QuestionDetail", - # Answers - "AnswerCreate", - "AnswerUpdate", - "AnswerListItem", - "AnswerDetail", - # Comments - "CommentCreate", - "CommentListItem", - # Interactions - "LikeCreate", - "LikeDelete", - "LikeStatus", - "CollectionCreate", - "CollectionUpdate", - "CollectionItem", - # Tags - "TagCreate", - "TagUpdate", - "TagListItem", - "TagDetail", - # Certifications - "CertificationCreate", - "CertificationReview", - "CertificationStatus_", - "CertificationListItem", - # User - "UserProfileUpdate", - "UserStats", - "UserProfile", - "MyQuestionListItem", - "QuestionBrief", - "MyAnswerListItem", -] diff --git a/backend/app/services/community/schemas/answers.py b/backend/app/services/community/schemas/answers.py deleted file mode 100644 index d0ba259e..00000000 --- a/backend/app/services/community/schemas/answers.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Answer schemas for community module.""" - -from datetime import datetime - -from pydantic import BaseModel, Field - -from ..enums import ContentStatus, UserRole -from .base import AuthorInfo - - -class AnswerCreate(BaseModel): - """Request schema for creating an answer.""" - - content: str = Field(..., min_length=1, max_length=20000) - image_urls: list[str] = Field(default_factory=list, max_length=9) - - -class AnswerUpdate(BaseModel): - """Request schema for updating an answer.""" - - content: str | None = Field(None, min_length=1, max_length=20000) - - -class AnswerListItem(BaseModel): - """Answer list item response.""" - - id: str - question_id: str - author: AuthorInfo - content: str = Field(description="Full content") - content_preview: str = Field(description="Content preview (first 200 chars)") - is_professional: bool - is_accepted: bool - like_count: int - comment_count: int - is_liked: bool = False # Current user has liked - created_at: datetime - - -class AnswerDetail(BaseModel): - """Answer detail response.""" - - id: str - question_id: str - author: AuthorInfo - content: str - image_urls: list[str] - author_role: UserRole - is_professional: bool - is_accepted: bool - status: ContentStatus - like_count: int - comment_count: int - is_liked: bool = False # Current user has liked - created_at: datetime - updated_at: datetime diff --git a/backend/app/services/community/schemas/base.py b/backend/app/services/community/schemas/base.py deleted file mode 100644 index b48fd2cd..00000000 --- a/backend/app/services/community/schemas/base.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Base schemas for community module.""" - -from datetime import datetime -from typing import Generic, TypeVar - -from pydantic import BaseModel - -from ..enums import UserRole - -T = TypeVar("T") - - -class PaginatedResponse(BaseModel, Generic[T]): - """Paginated response wrapper.""" - - items: list[T] - total: int - page: int - page_size: int - total_pages: int - - -class AuthorInfo(BaseModel): - """Author information (embedded in responses).""" - - id: str - nickname: str - avatar_url: str | None = None - role: UserRole - is_certified: bool = False - certification_title: str | None = None # e.g., "某医院 妇产科 主任医师" - - -class TagInfo(BaseModel): - """Tag information (embedded in responses).""" - - id: str - name: str - slug: str - - -class TimestampMixin(BaseModel): - """Mixin for timestamp fields.""" - - created_at: datetime - updated_at: datetime diff --git a/backend/app/services/community/schemas/certifications.py b/backend/app/services/community/schemas/certifications.py deleted file mode 100644 index 2f2b2895..00000000 --- a/backend/app/services/community/schemas/certifications.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Certification schemas for community module.""" - -from datetime import datetime - -from pydantic import BaseModel, Field - -from ..enums import CertificationStatus, UserRole - - -class CertificationCreate(BaseModel): - """Request schema for creating a certification.""" - - certification_type: UserRole = Field( - ..., - description="Must be one of: certified_doctor, certified_therapist, certified_nurse", - ) - real_name: str = Field(..., min_length=2, max_length=50) - id_card_number: str | None = Field(None, min_length=18, max_length=18) - license_number: str = Field(..., min_length=5, max_length=100) - hospital_or_institution: str = Field(..., min_length=2, max_length=200) - department: str | None = Field(None, max_length=100) - title: str | None = Field(None, max_length=50) - license_image_url: str = Field(..., max_length=500) - id_card_image_url: str | None = Field(None, max_length=500) - additional_docs_urls: list[str] = Field(default_factory=list, max_length=5) - - -class CertificationReview(BaseModel): - """Request schema for reviewing a certification.""" - - status: CertificationStatus = Field(..., description="Must be: approved, rejected") - review_comment: str | None = Field(None, max_length=500) - valid_from: datetime | None = None - valid_until: datetime | None = None - - -class CertificationStatus_(BaseModel): - """Certification status response.""" - - id: str - user_id: str - certification_type: UserRole - real_name: str - license_number: str - hospital_or_institution: str - department: str | None - title: str | None - status: CertificationStatus - review_comment: str | None - reviewed_at: datetime | None - valid_from: datetime | None - valid_until: datetime | None - created_at: datetime - updated_at: datetime - - -class CertificationListItem(BaseModel): - """Certification list item for admin review.""" - - id: str - user_id: str - user_nickname: str - certification_type: UserRole - real_name: str - license_number: str - hospital_or_institution: str - status: CertificationStatus - created_at: datetime diff --git a/backend/app/services/community/schemas/comments.py b/backend/app/services/community/schemas/comments.py deleted file mode 100644 index 184560bb..00000000 --- a/backend/app/services/community/schemas/comments.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Comment schemas for community module.""" - -from datetime import datetime - -from pydantic import BaseModel, Field - -from .base import AuthorInfo - - -class CommentCreate(BaseModel): - """Request schema for creating a comment.""" - - content: str = Field(..., min_length=1, max_length=1000) - parent_id: str | None = None # For nested replies - - -class CommentListItem(BaseModel): - """Comment list item response.""" - - id: str - answer_id: str - author: AuthorInfo - content: str - parent_id: str | None - reply_to_user: AuthorInfo | None = None - like_count: int - is_liked: bool = False - created_at: datetime - replies: list["CommentListItem"] = Field(default_factory=list) - - -# Update forward reference -CommentListItem.model_rebuild() diff --git a/backend/app/services/community/schemas/interactions.py b/backend/app/services/community/schemas/interactions.py deleted file mode 100644 index ad2fda6b..00000000 --- a/backend/app/services/community/schemas/interactions.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Interaction schemas (likes, collections) for community module.""" - -from datetime import datetime -from typing import Literal - -from pydantic import BaseModel, Field - -from .questions import QuestionListItem - - -class LikeCreate(BaseModel): - """Request schema for creating a like.""" - - target_type: Literal["question", "answer", "comment"] - target_id: str - - -class LikeDelete(BaseModel): - """Request schema for deleting a like.""" - - target_type: Literal["question", "answer", "comment"] - target_id: str - - -class LikeStatus(BaseModel): - """Like status response.""" - - is_liked: bool - like_count: int - - -class CollectionCreate(BaseModel): - """Request schema for creating a collection.""" - - question_id: str - folder_name: str | None = Field(None, max_length=50) - note: str | None = Field(None, max_length=500) - - -class CollectionUpdate(BaseModel): - """Request schema for updating a collection.""" - - folder_name: str | None = Field(None, max_length=50) - note: str | None = Field(None, max_length=500) - - -class CollectionItem(BaseModel): - """Collection item response.""" - - id: str - question: QuestionListItem - folder_name: str | None - note: str | None - created_at: datetime diff --git a/backend/app/services/community/schemas/questions.py b/backend/app/services/community/schemas/questions.py deleted file mode 100644 index ade03508..00000000 --- a/backend/app/services/community/schemas/questions.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Question schemas for community module.""" - -from datetime import datetime - -from pydantic import BaseModel, Field - -from ..enums import ChannelType, ContentStatus -from .base import AuthorInfo, TagInfo - - -class QuestionCreate(BaseModel): - """Request schema for creating a question.""" - - title: str = Field(..., min_length=1, max_length=200) - content: str = Field(..., min_length=1, max_length=10000) - channel: ChannelType = ChannelType.EXPERIENCE - tag_ids: list[str] = Field(default_factory=list, max_length=5) - image_urls: list[str] = Field(default_factory=list, max_length=9) - - -class QuestionUpdate(BaseModel): - """Request schema for updating a question.""" - - title: str | None = Field(None, min_length=1, max_length=200) - content: str | None = Field(None, min_length=1, max_length=10000) - tag_ids: list[str] | None = Field(None, max_length=5) - - -class QuestionListItem(BaseModel): - """Question list item response.""" - - id: str - title: str - content_preview: str = Field(description="Content preview (first 100 chars)") - channel: ChannelType - author: AuthorInfo - tags: list[TagInfo] - view_count: int - answer_count: int - like_count: int - collection_count: int = 0 - is_pinned: bool - is_featured: bool - has_accepted_answer: bool - is_liked: bool = False # Current user has liked - is_collected: bool = False # Current user has collected - created_at: datetime - - -class QuestionDetail(BaseModel): - """Question detail response.""" - - id: str - title: str - content: str - content_preview: str - channel: ChannelType - status: ContentStatus - author: AuthorInfo - tags: list[TagInfo] - image_urls: list[str] - view_count: int - answer_count: int - like_count: int - collection_count: int - is_pinned: bool - is_featured: bool - has_accepted_answer: bool - accepted_answer_id: str | None - is_liked: bool = False # Current user has liked - is_collected: bool = False # Current user has collected - professional_answer_count: int = 0 - experience_answer_count: int = 0 - created_at: datetime - updated_at: datetime - published_at: datetime | None diff --git a/backend/app/services/community/schemas/tags.py b/backend/app/services/community/schemas/tags.py deleted file mode 100644 index 0b673992..00000000 --- a/backend/app/services/community/schemas/tags.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Tag schemas for community module.""" - -from datetime import datetime - -from pydantic import BaseModel, Field - - -class TagCreate(BaseModel): - """Request schema for creating a tag.""" - - name: str = Field(..., min_length=1, max_length=50) - slug: str = Field(..., min_length=1, max_length=50, pattern=r"^[a-z0-9-]+$") - description: str | None = Field(None, max_length=200) - is_featured: bool = False - - -class TagUpdate(BaseModel): - """Request schema for updating a tag.""" - - name: str | None = Field(None, min_length=1, max_length=50) - description: str | None = Field(None, max_length=200) - is_featured: bool | None = None - is_active: bool | None = None - - -class TagListItem(BaseModel): - """Tag list item response.""" - - id: str - name: str - slug: str - description: str | None - question_count: int - follower_count: int - is_featured: bool - - -class TagDetail(TagListItem): - """Tag detail response.""" - - is_active: bool - created_at: datetime diff --git a/backend/app/services/community/schemas/user.py b/backend/app/services/community/schemas/user.py deleted file mode 100644 index 4878a0d8..00000000 --- a/backend/app/services/community/schemas/user.py +++ /dev/null @@ -1,88 +0,0 @@ -"""User schemas for community module.""" - -from datetime import datetime -from typing import Literal - -from pydantic import BaseModel, EmailStr, Field - -from ..enums import UserRole -from .base import TagInfo - -# Family roles that users can freely switch between -FamilyRoleType = Literal["mom", "dad", "family"] - - -class UserProfileUpdate(BaseModel): - """Request schema for updating user profile.""" - - nickname: str | None = Field(None, min_length=1, max_length=50) - email: EmailStr | None = None - avatar_url: str | None = Field(None, max_length=500) - role: FamilyRoleType | None = Field( - None, description="Only family roles allowed: mom, dad, family" - ) - - -class UserStats(BaseModel): - """User statistics.""" - - question_count: int = 0 - answer_count: int = 0 - like_received_count: int = 0 - collection_count: int = 0 - - -class UserProfile(BaseModel): - """User profile response with statistics.""" - - id: str - nickname: str - email: str - avatar_url: str | None = None - role: UserRole - is_certified: bool = False - certification_title: str | None = None - stats: UserStats - created_at: datetime - - -class MyQuestionListItem(BaseModel): - """Question list item for my questions page.""" - - id: str - title: str - content_preview: str - channel: str - tags: list[TagInfo] - view_count: int - answer_count: int - like_count: int - collection_count: int - status: str - has_accepted_answer: bool - is_liked: bool = False - is_collected: bool = False - created_at: datetime - - -class QuestionBrief(BaseModel): - """Brief question info for answer context.""" - - id: str - title: str - channel: str - - -class MyAnswerListItem(BaseModel): - """Answer list item with question context.""" - - id: str - content_preview: str - question: QuestionBrief - is_professional: bool - is_accepted: bool - like_count: int - comment_count: int - status: str - is_liked: bool = False - created_at: datetime diff --git a/backend/app/services/community/service.py b/backend/app/services/community/service.py deleted file mode 100644 index 51858fa9..00000000 --- a/backend/app/services/community/service.py +++ /dev/null @@ -1,1596 +0,0 @@ -"""Community service - core business logic.""" - -import json -from datetime import datetime -from typing import Literal - -from fastapi import HTTPException -from sqlalchemy import func, select, update -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.orm import selectinload - -from .enums import ( - PROFESSIONAL_ROLES, - CertificationStatus, - ChannelType, - ContentStatus, - ModerationResult, - UserRole, -) -from .models import ( - Answer, - Collection, - Comment, - Like, - ModerationLog, - Question, - QuestionTag, - Tag, - User, -) -from .moderation import ModerationService, get_moderation_service -from .schemas import ( - AnswerCreate, - AnswerListItem, - AnswerUpdate, - AuthorInfo, - CollectionItem, - CommentCreate, - CommentListItem, - MyAnswerListItem, - MyQuestionListItem, - PaginatedResponse, - QuestionBrief, - QuestionCreate, - QuestionDetail, - QuestionListItem, - QuestionUpdate, - TagInfo, - UserProfile, - UserProfileUpdate, - UserStats, -) - - -class CommunityService: - """Community service - handles all community-related business logic.""" - - def __init__(self, moderation: ModerationService | None = None): - """Initialize community service.""" - self._moderation = moderation or get_moderation_service() - - # ==================== Helper Methods ==================== - - def is_certified_professional(self, user: User) -> bool: - """Check if user is a certified professional.""" - return ( - user.role in PROFESSIONAL_ROLES - and user.certification is not None - and user.certification.status == CertificationStatus.APPROVED - ) - - def _build_author_info(self, user: User) -> AuthorInfo: - """Build AuthorInfo from User model.""" - certification_title = None - is_certified = False - - if ( - user.certification - and user.certification.status == CertificationStatus.APPROVED - ): - is_certified = True - parts = [] - if user.certification.hospital_or_institution: - parts.append(user.certification.hospital_or_institution) - if user.certification.department: - parts.append(user.certification.department) - if user.certification.title: - parts.append(user.certification.title) - certification_title = " ".join(parts) if parts else None - - return AuthorInfo( - id=user.id, - nickname=user.nickname, - avatar_url=user.avatar_url, - role=user.role, - is_certified=is_certified, - certification_title=certification_title, - ) - - def _build_tag_info(self, tag: Tag) -> TagInfo: - """Build TagInfo from Tag model.""" - return TagInfo(id=tag.id, name=tag.name, slug=tag.slug) - - # ==================== Question Operations ==================== - - async def get_questions( - self, - db: AsyncSession, - channel: ChannelType | None = None, - tag_id: str | None = None, - status: ContentStatus = ContentStatus.PUBLISHED, - sort_by: Literal[ - "created_at", "view_count", "answer_count", "like_count" - ] = "created_at", - order: Literal["asc", "desc"] = "desc", - page: int = 1, - page_size: int = 20, - current_user_id: str | None = None, - ) -> PaginatedResponse[QuestionListItem]: - """Get paginated list of questions.""" - # Build query - query = ( - select(Question) - .options(selectinload(Question.author).selectinload(User.certification)) - .options(selectinload(Question.tags)) - .where(Question.status == status) - ) - - if channel: - query = query.where(Question.channel == channel) - - if tag_id: - query = query.join(QuestionTag).where(QuestionTag.tag_id == tag_id) - - # Count total - count_query = select(func.count()).select_from(query.subquery()) - total = (await db.execute(count_query)).scalar() or 0 - - # Sort - sort_column = getattr(Question, sort_by) - if order == "desc": - query = query.order_by(sort_column.desc()) - else: - query = query.order_by(sort_column.asc()) - - # Paginate - query = query.offset((page - 1) * page_size).limit(page_size) - - result = await db.execute(query) - questions = result.scalars().all() - - # Get user's likes and collections for these questions if logged in - liked_question_ids: set[str] = set() - collected_question_ids: set[str] = set() - if current_user_id and questions: - question_ids = [q.id for q in questions] - # Query likes - like_query = select(Like.target_id).where( - Like.user_id == current_user_id, - Like.target_type == "question", - Like.target_id.in_(question_ids), - ) - like_result = await db.execute(like_query) - liked_question_ids = set(like_result.scalars().all()) - # Query collections - collection_query = select(Collection.question_id).where( - Collection.user_id == current_user_id, - Collection.question_id.in_(question_ids), - ) - collection_result = await db.execute(collection_query) - collected_question_ids = set(collection_result.scalars().all()) - - # Build response - items = [] - for q in questions: - items.append( - QuestionListItem( - id=q.id, - title=q.title, - content_preview=q.content[:100] + "..." - if len(q.content) > 100 - else q.content, - channel=q.channel, - author=self._build_author_info(q.author), - tags=[self._build_tag_info(t) for t in (q.tags or [])], - view_count=q.view_count, - answer_count=q.answer_count, - like_count=q.like_count, - collection_count=q.collection_count, - is_pinned=q.is_pinned, - is_featured=q.is_featured, - has_accepted_answer=q.accepted_answer_id is not None, - is_liked=q.id in liked_question_ids, - is_collected=q.id in collected_question_ids, - created_at=q.created_at, - ) - ) - - return PaginatedResponse( - items=items, - total=total, - page=page, - page_size=page_size, - total_pages=(total + page_size - 1) // page_size, - ) - - async def get_question( - self, - db: AsyncSession, - question_id: str, - current_user_id: str | None = None, - increment_view: bool = True, - ) -> QuestionDetail | None: - """Get question detail by ID.""" - query = ( - select(Question) - .options(selectinload(Question.author).selectinload(User.certification)) - .options(selectinload(Question.tags)) - .where(Question.id == question_id) - ) - - result = await db.execute(query) - question = result.scalar_one_or_none() - - if not question: - return None - - # Increment view count - if increment_view: - await db.execute( - update(Question) - .where(Question.id == question_id) - .values(view_count=Question.view_count + 1) - ) - await db.commit() - await db.refresh(question) - - # Check user interactions - is_liked = False - is_collected = False - if current_user_id: - like_query = select(Like).where( - Like.user_id == current_user_id, - Like.target_type == "question", - Like.target_id == question_id, - ) - is_liked = (await db.execute(like_query)).scalar_one_or_none() is not None - - collection_query = select(Collection).where( - Collection.user_id == current_user_id, - Collection.question_id == question_id, - ) - is_collected = ( - await db.execute(collection_query) - ).scalar_one_or_none() is not None - - # Count professional vs experience answers - pro_count_query = select(func.count()).where( - Answer.question_id == question_id, - Answer.is_professional.is_(True), - Answer.status == ContentStatus.PUBLISHED, - ) - exp_count_query = select(func.count()).where( - Answer.question_id == question_id, - Answer.is_professional.is_(False), - Answer.status == ContentStatus.PUBLISHED, - ) - professional_count = (await db.execute(pro_count_query)).scalar() or 0 - experience_count = (await db.execute(exp_count_query)).scalar() or 0 - - image_urls = json.loads(question.image_urls) if question.image_urls else [] - - return QuestionDetail( - id=question.id, - title=question.title, - content=question.content, - content_preview=question.content[:100] + "..." - if len(question.content) > 100 - else question.content, - channel=question.channel, - status=question.status, - author=self._build_author_info(question.author), - tags=[self._build_tag_info(t) for t in (question.tags or [])], - image_urls=image_urls, - view_count=question.view_count, - answer_count=question.answer_count, - like_count=question.like_count, - collection_count=question.collection_count, - is_pinned=question.is_pinned, - is_featured=question.is_featured, - has_accepted_answer=question.accepted_answer_id is not None, - accepted_answer_id=question.accepted_answer_id, - is_liked=is_liked, - is_collected=is_collected, - professional_answer_count=professional_count, - experience_answer_count=experience_count, - created_at=question.created_at, - updated_at=question.updated_at, - published_at=question.published_at, - ) - - async def create_question( - self, - db: AsyncSession, - question_in: QuestionCreate, - author: User, - ) -> Question: - """Create a new question with moderation.""" - # Content moderation - check both title and content - title_decision = await self._moderation.moderate_text( - question_in.title, author.id - ) - if title_decision.result == ModerationResult.REJECTED: - raise HTTPException( - status_code=400, - detail={ - "message": f"标题审核未通过: {title_decision.reason}", - "categories": [c.value for c in title_decision.categories], - "crisis_intervention": title_decision.crisis_intervention, - }, - ) - - content_decision = await self._moderation.moderate_text( - question_in.content, author.id - ) - if content_decision.result == ModerationResult.REJECTED: - raise HTTPException( - status_code=400, - detail={ - "message": f"内容审核未通过: {content_decision.reason}", - "categories": [c.value for c in content_decision.categories], - "crisis_intervention": content_decision.crisis_intervention, - }, - ) - - # Determine initial status (use stricter result) - if ( - title_decision.result == ModerationResult.PASSED - and content_decision.result == ModerationResult.PASSED - ): - status = ContentStatus.PUBLISHED - published_at = datetime.utcnow() - else: # NEED_MANUAL_REVIEW - status = ContentStatus.PENDING_REVIEW - published_at = None - - # Create question - question = Question( - author_id=author.id, - title=question_in.title, - content=question_in.content, - channel=question_in.channel, - status=status, - published_at=published_at, - image_urls=json.dumps(question_in.image_urls) - if question_in.image_urls - else None, - ) - db.add(question) - await db.flush() - - # Associate tags - if question_in.tag_ids: - for tag_id in question_in.tag_ids: - db.add(QuestionTag(question_id=question.id, tag_id=tag_id)) - # Update tag question count - await db.execute( - update(Tag) - .where(Tag.id == tag_id) - .values(question_count=Tag.question_count + 1) - ) - - # Log moderation result (use content decision as primary) - db.add( - ModerationLog( - target_type="question", - target_id=question.id, - moderation_type="auto", - result=content_decision.result, - sensitive_categories=( - json.dumps([c.value for c in content_decision.categories]) - if content_decision.categories - else None - ), - confidence_score=content_decision.confidence, - reason=content_decision.reason, - ) - ) - - await db.commit() - await db.refresh(question) - return question - - # ==================== Answer Operations ==================== - - async def get_answers( - self, - db: AsyncSession, - question_id: str, - is_professional: bool | None = None, - sort_by: Literal["created_at", "like_count"] = "created_at", - order: Literal["asc", "desc"] = "desc", - page: int = 1, - page_size: int = 20, - current_user_id: str | None = None, - ) -> PaginatedResponse[AnswerListItem]: - """Get paginated list of answers for a question.""" - query = ( - select(Answer) - .options(selectinload(Answer.author).selectinload(User.certification)) - .where(Answer.question_id == question_id) - .where(Answer.status == ContentStatus.PUBLISHED) - ) - - if is_professional is not None: - query = query.where(Answer.is_professional == is_professional) - - # Count total - count_query = select(func.count()).select_from(query.subquery()) - total = (await db.execute(count_query)).scalar() or 0 - - # Sort - sort_column = getattr(Answer, sort_by) - if order == "desc": - query = query.order_by(sort_column.desc()) - else: - query = query.order_by(sort_column.asc()) - - # Paginate - query = query.offset((page - 1) * page_size).limit(page_size) - - result = await db.execute(query) - answers = result.scalars().all() - - # Get user's likes for these answers if logged in - liked_answer_ids: set[str] = set() - if current_user_id: - answer_ids = [a.id for a in answers] - like_query = select(Like.target_id).where( - Like.user_id == current_user_id, - Like.target_type == "answer", - Like.target_id.in_(answer_ids), - ) - like_result = await db.execute(like_query) - liked_answer_ids = set(like_result.scalars().all()) - - items = [] - for a in answers: - items.append( - AnswerListItem( - id=a.id, - question_id=a.question_id, - author=self._build_author_info(a.author), - content=a.content, - content_preview=a.content[:200] + "..." - if len(a.content) > 200 - else a.content, - is_professional=a.is_professional, - is_accepted=a.is_accepted, - like_count=a.like_count, - comment_count=a.comment_count, - is_liked=a.id in liked_answer_ids, - created_at=a.created_at, - ) - ) - - return PaginatedResponse( - items=items, - total=total, - page=page, - page_size=page_size, - total_pages=(total + page_size - 1) // page_size, - ) - - async def create_answer( - self, - db: AsyncSession, - question_id: str, - answer_in: AnswerCreate, - author: User, - ) -> Answer: - """Create a new answer with moderation and permission check.""" - # Get question to check channel - question = await db.get(Question, question_id) - if not question: - raise HTTPException(status_code=404, detail="问题不存在") - - # Check permission for professional channel - if question.channel == ChannelType.PROFESSIONAL: - is_author = question.author_id == author.id - if ( - not self.is_certified_professional(author) - and author.role != UserRole.ADMIN - and not is_author - ): - raise HTTPException( - status_code=403, detail="专业频道仅限认证专业人士回答" - ) - - # Content moderation - decision = await self._moderation.moderate_text(answer_in.content, author.id) - - if decision.result == ModerationResult.REJECTED: - raise HTTPException( - status_code=400, - detail={ - "message": f"内容审核未通过: {decision.reason}", - "categories": [c.value for c in decision.categories], - "crisis_intervention": decision.crisis_intervention, - }, - ) - - # Determine status and professional flag - status = ( - ContentStatus.PUBLISHED - if decision.result == ModerationResult.PASSED - else ContentStatus.PENDING_REVIEW - ) - is_professional = self.is_certified_professional(author) - - # Create answer - answer = Answer( - question_id=question_id, - author_id=author.id, - content=answer_in.content, - author_role=author.role, - is_professional=is_professional, - status=status, - image_urls=json.dumps(answer_in.image_urls) - if answer_in.image_urls - else None, - ) - db.add(answer) - - # Update question answer count - await db.execute( - update(Question) - .where(Question.id == question_id) - .values(answer_count=Question.answer_count + 1) - ) - - # Log moderation - await db.flush() - db.add( - ModerationLog( - target_type="answer", - target_id=answer.id, - moderation_type="auto", - result=decision.result, - sensitive_categories=( - json.dumps([c.value for c in decision.categories]) - if decision.categories - else None - ), - confidence_score=decision.confidence, - reason=decision.reason, - ) - ) - - await db.commit() - await db.refresh(answer) - return answer - - # ==================== Like Operations ==================== - - async def toggle_like( - self, - db: AsyncSession, - user_id: str, - target_type: Literal["question", "answer", "comment"], - target_id: str, - ) -> tuple[bool, int]: - """Toggle like status. Returns (is_liked, new_count).""" - # Check existing like - query = select(Like).where( - Like.user_id == user_id, - Like.target_type == target_type, - Like.target_id == target_id, - ) - existing = (await db.execute(query)).scalar_one_or_none() - - # Get target model - model_map = { - "question": Question, - "answer": Answer, - "comment": Comment, - } - model = model_map[target_type] - - if existing: - # Unlike - await db.delete(existing) - await db.execute( - update(model) - .where(model.id == target_id) # type: ignore[attr-defined] - .values(like_count=model.like_count - 1) # type: ignore[attr-defined] - ) - await db.commit() - - # Get new count - target = await db.get(model, target_id) - return False, target.like_count if target else 0 # type: ignore[attr-defined] - else: - # Like - db.add( - Like( - user_id=user_id, - target_type=target_type, - target_id=target_id, - ) - ) - await db.execute( - update(model) - .where(model.id == target_id) # type: ignore[attr-defined] - .values(like_count=model.like_count + 1) # type: ignore[attr-defined] - ) - await db.commit() - - target = await db.get(model, target_id) - return True, target.like_count if target else 1 # type: ignore[attr-defined] - - # ==================== Collection Operations ==================== - - async def toggle_collection( - self, - db: AsyncSession, - user_id: str, - question_id: str, - folder_name: str | None = None, - note: str | None = None, - ) -> tuple[bool, int]: - """Toggle collection status. Returns (is_collected, new_count).""" - query = select(Collection).where( - Collection.user_id == user_id, - Collection.question_id == question_id, - ) - existing = (await db.execute(query)).scalar_one_or_none() - - if existing: - # Uncollect - await db.delete(existing) - await db.execute( - update(Question) - .where(Question.id == question_id) - .values(collection_count=Question.collection_count - 1) - ) - await db.commit() - - question = await db.get(Question, question_id) - return False, question.collection_count if question else 0 - else: - # Collect - db.add( - Collection( - user_id=user_id, - question_id=question_id, - folder_name=folder_name, - note=note, - ) - ) - await db.execute( - update(Question) - .where(Question.id == question_id) - .values(collection_count=Question.collection_count + 1) - ) - await db.commit() - - question = await db.get(Question, question_id) - return True, question.collection_count if question else 1 - - async def get_user_collections( - self, - db: AsyncSession, - user_id: str, - folder_name: str | None = None, - page: int = 1, - page_size: int = 20, - ) -> PaginatedResponse[CollectionItem]: - """Get user's collections.""" - # Build query - query = ( - select(Collection) - .options( - selectinload(Collection.question) - .selectinload(Question.author) - .selectinload(User.certification) - ) - .options(selectinload(Collection.question).selectinload(Question.tags)) - .where(Collection.user_id == user_id) - .order_by(Collection.created_at.desc()) - ) - - if folder_name: - query = query.where(Collection.folder_name == folder_name) - - # Count total - count_query = select(func.count()).select_from(query.subquery()) - total = (await db.execute(count_query)).scalar() or 0 - - # Paginate - query = query.offset((page - 1) * page_size).limit(page_size) - - result = await db.execute(query) - collections = result.scalars().all() - - # Get question IDs to check likes - question_ids = [c.question.id for c in collections if c.question] - - # Query liked question IDs for current user - liked_ids: set[str] = set() - if question_ids: - like_query = select(Like.target_id).where( - Like.user_id == user_id, - Like.target_type == "question", - Like.target_id.in_(question_ids), - ) - like_result = await db.execute(like_query) - liked_ids = {row[0] for row in like_result.fetchall()} - - # Build response - items = [] - for c in collections: - if c.question: - items.append( - CollectionItem( - id=c.id, - question=QuestionListItem( - id=c.question.id, - title=c.question.title, - content_preview=c.question.content[:100] + "..." - if len(c.question.content) > 100 - else c.question.content, - channel=c.question.channel, - author=self._build_author_info(c.question.author), - tags=[ - self._build_tag_info(t) for t in (c.question.tags or []) - ], - view_count=c.question.view_count, - answer_count=c.question.answer_count, - like_count=c.question.like_count, - collection_count=c.question.collection_count, - is_pinned=c.question.is_pinned, - is_featured=c.question.is_featured, - has_accepted_answer=c.question.accepted_answer_id - is not None, - is_liked=c.question.id in liked_ids, - is_collected=True, # User has this in collection - created_at=c.question.created_at, - ), - folder_name=c.folder_name, - note=c.note, - created_at=c.created_at, - ) - ) - - return PaginatedResponse( - items=items, - total=total, - page=page, - page_size=page_size, - total_pages=(total + page_size - 1) // page_size, - ) - - # ==================== Comment Operations ==================== - - async def get_comments( - self, - db: AsyncSession, - answer_id: str, - user_id: str | None = None, - ) -> list[CommentListItem]: - """Get comments for an answer with flat replies (max 1 level nesting).""" - # First check if answer exists - answer = await db.get(Answer, answer_id) - if not answer: - raise HTTPException(status_code=404, detail="回答不存在") - - # Get all comments for this answer with author.certification eagerly loaded - query = ( - select(Comment) - .options( - selectinload(Comment.author).selectinload(User.certification), - selectinload(Comment.reply_to_user).selectinload(User.certification), - ) - .where( - Comment.answer_id == answer_id, - Comment.status == ContentStatus.PUBLISHED, - ) - .order_by(Comment.created_at.asc()) - ) - result = await db.execute(query) - comments = result.scalars().all() - - # Get user's likes for these comments - liked_comment_ids: set[str] = set() - if user_id and comments: - comment_ids = [c.id for c in comments] - like_query = select(Like.target_id).where( - Like.user_id == user_id, - Like.target_type == "comment", - Like.target_id.in_(comment_ids), - ) - like_result = await db.execute(like_query) - liked_comment_ids = set(like_result.scalars().all()) - - # Build flat structure with max 1 level nesting - # Root comments (no parent_id) are at level 0 - # All replies (with parent_id) are at level 1, shown under their root ancestor - comment_map: dict[str, Comment] = {c.id: c for c in comments} - root_comments: list[CommentListItem] = [] - root_comment_items: dict[str, CommentListItem] = {} - - # Find root ancestor for any comment - def find_root_id(comment_id: str) -> str | None: - c = comment_map.get(comment_id) - if not c: - return None - if not c.parent_id: - return c.id - return find_root_id(c.parent_id) - - # First pass: create all CommentListItems - for c in comments: - if not c.parent_id: - # Root comment - item = CommentListItem( - id=c.id, - answer_id=c.answer_id, - author=self._build_author_info(c.author), - content=c.content, - parent_id=None, - reply_to_user=None, - like_count=c.like_count, - is_liked=c.id in liked_comment_ids, - created_at=c.created_at, - replies=[], - ) - root_comments.append(item) - root_comment_items[c.id] = item - - # Second pass: add all replies to their root ancestor - for c in comments: - if c.parent_id: - root_id = find_root_id(c.id) - if root_id and root_id in root_comment_items: - item = CommentListItem( - id=c.id, - answer_id=c.answer_id, - author=self._build_author_info(c.author), - content=c.content, - parent_id=root_id, # Always point to root - reply_to_user=self._build_author_info(c.reply_to_user) - if c.reply_to_user - else None, - like_count=c.like_count, - is_liked=c.id in liked_comment_ids, - created_at=c.created_at, - replies=[], # No further nesting - ) - root_comment_items[root_id].replies.append(item) - - return root_comments - - async def create_comment( - self, - db: AsyncSession, - answer_id: str, - user: User, - comment_in: CommentCreate, - ) -> CommentListItem: - """Create a new comment on an answer.""" - # Check if answer exists - answer = await db.get(Answer, answer_id) - if not answer: - raise HTTPException(status_code=404, detail="回答不存在") - - # If replying to a comment, verify parent exists - reply_to_user_id = None - if comment_in.parent_id: - parent = await db.get(Comment, comment_in.parent_id) - if not parent or parent.answer_id != answer_id: - raise HTTPException(status_code=404, detail="回复目标不存在") - reply_to_user_id = parent.author_id - - # Run moderation - moderation_result = await self._moderation.moderate_text( - comment_in.content, user.id - ) - if moderation_result.result == ModerationResult.REJECTED: - raise HTTPException( - status_code=400, - detail={ - "message": f"评论审核未通过: {moderation_result.reason}", - "categories": [c.value for c in moderation_result.categories], - "crisis_intervention": moderation_result.crisis_intervention, - }, - ) - status = ( - ContentStatus.PUBLISHED - if moderation_result.result == ModerationResult.PASSED - else ContentStatus.PENDING_REVIEW - ) - - # Create comment - comment = Comment( - answer_id=answer_id, - author_id=user.id, - parent_id=comment_in.parent_id, - reply_to_user_id=reply_to_user_id, - content=comment_in.content, - status=status, - ) - db.add(comment) - - # Update answer's comment count - await db.execute( - update(Answer) - .where(Answer.id == answer_id) - .values(comment_count=Answer.comment_count + 1) - ) - - await db.commit() - await db.refresh(comment) - - # Reload comment with author and certification eagerly loaded - comment_query = ( - select(Comment) - .options( - selectinload(Comment.author).selectinload(User.certification), - selectinload(Comment.reply_to_user).selectinload(User.certification), - ) - .where(Comment.id == comment.id) - ) - result = await db.execute(comment_query) - comment = result.scalar_one() - - return CommentListItem( - id=comment.id, - answer_id=comment.answer_id, - author=self._build_author_info(comment.author), - content=comment.content, - parent_id=comment.parent_id, - reply_to_user=self._build_author_info(comment.reply_to_user) - if comment.reply_to_user - else None, - like_count=0, - is_liked=False, - created_at=comment.created_at, - replies=[], - ) - - async def delete_comment( - self, - db: AsyncSession, - comment_id: str, - user: User, - ) -> None: - """Delete a comment (author only).""" - from .enums import UserRole - - comment = await db.get(Comment, comment_id) - if not comment: - raise HTTPException(status_code=404, detail="评论不存在") - - # Check permission - is_author = comment.author_id == user.id - is_admin = user.role == UserRole.ADMIN - - if not is_author and not is_admin: - raise HTTPException(status_code=403, detail="无权删除此评论") - - # Decrease answer's comment count - await db.execute( - update(Answer) - .where(Answer.id == comment.answer_id) - .values(comment_count=Answer.comment_count - 1) - ) - - # Delete child comments first - child_query = select(Comment).where(Comment.parent_id == comment_id) - child_result = await db.execute(child_query) - children = child_result.scalars().all() - for child in children: - await db.delete(child) - # Decrease count for each child - await db.execute( - update(Answer) - .where(Answer.id == comment.answer_id) - .values(comment_count=Answer.comment_count - 1) - ) - - await db.delete(comment) - await db.commit() - - # ==================== Delete Operations ==================== - - async def update_question( - self, - db: AsyncSession, - question_id: str, - question_in: QuestionUpdate, - user: User, - ) -> Question: - """ - Update a question. - - Author can update their own questions - - Admin can update any question - """ - result = await db.execute( - select(Question) - .options(selectinload(Question.tags), selectinload(Question.author)) - .where(Question.id == question_id) - ) - question = result.scalar_one_or_none() - - if not question: - raise HTTPException(status_code=404, detail="问题不存在") - - # Check permission - is_author = question.author_id == user.id - is_admin = user.role == UserRole.ADMIN - - if not is_author and not is_admin: - raise HTTPException(status_code=403, detail="无权修改此问题") - - # Update fields if provided - if question_in.title is not None: - # Title moderation - title_decision = await self._moderation.moderate_text( - question_in.title, user.id - ) - if title_decision.result == ModerationResult.REJECTED: - raise HTTPException( - status_code=400, - detail={ - "message": f"标题审核未通过: {title_decision.reason}", - "categories": [c.value for c in title_decision.categories], - }, - ) - question.title = question_in.title - if title_decision.result == ModerationResult.NEED_MANUAL_REVIEW: - question.status = ContentStatus.PENDING_REVIEW - - if question_in.content is not None: - # Content moderation for new content - decision = await self._moderation.moderate_text( - question_in.content, user.id - ) - if decision.result == ModerationResult.REJECTED: - raise HTTPException( - status_code=400, - detail={ - "message": f"内容审核未通过: {decision.reason}", - "categories": [c.value for c in decision.categories], - }, - ) - question.content = question_in.content - # If content changed, may need re-review - if decision.result == ModerationResult.NEED_MANUAL_REVIEW: - question.status = ContentStatus.PENDING_REVIEW - - # Update tags if provided - if question_in.tag_ids is not None: - # Remove existing tags - await db.execute( - select(QuestionTag).where(QuestionTag.question_id == question_id) - ) - existing_tags = ( - ( - await db.execute( - select(QuestionTag).where( - QuestionTag.question_id == question_id - ) - ) - ) - .scalars() - .all() - ) - for qt in existing_tags: - await db.delete(qt) - - # Add new tags - for tag_id in question_in.tag_ids: - tag = await db.get(Tag, tag_id) - if tag: - question_tag = QuestionTag(question_id=question_id, tag_id=tag_id) - db.add(question_tag) - - question.updated_at = datetime.utcnow() - await db.commit() - await db.refresh(question) - - return question - - async def update_answer( - self, - db: AsyncSession, - answer_id: str, - answer_in: AnswerUpdate, - user: User, - ) -> Answer: - """ - Update an answer. - - Author can update their own answers - - Admin can update any answer - """ - result = await db.execute( - select(Answer) - .options(selectinload(Answer.author), selectinload(Answer.question)) - .where(Answer.id == answer_id) - ) - answer = result.scalar_one_or_none() - - if not answer: - raise HTTPException(status_code=404, detail="回答不存在") - - # Check permission - is_author = answer.author_id == user.id - is_admin = user.role == UserRole.ADMIN - - if not is_author and not is_admin: - raise HTTPException(status_code=403, detail="无权修改此回答") - - # Update content if provided - if answer_in.content is not None: - # Content moderation - decision = await self._moderation.moderate_text(answer_in.content, user.id) - if decision.result == ModerationResult.REJECTED: - raise HTTPException( - status_code=400, - detail={ - "message": f"内容审核未通过: {decision.reason}", - "categories": [c.value for c in decision.categories], - }, - ) - answer.content = answer_in.content - if decision.result == ModerationResult.NEED_MANUAL_REVIEW: - answer.status = ContentStatus.PENDING_REVIEW - - answer.updated_at = datetime.utcnow() - await db.commit() - await db.refresh(answer) - - return answer - - async def delete_question( - self, - db: AsyncSession, - question_id: str, - user: User, - ) -> None: - """ - Delete a question. - - Author can delete their own questions - - Admin can delete any question - """ - from .enums import UserRole - - question = await db.get(Question, question_id) - if not question: - raise HTTPException(status_code=404, detail="问题不存在") - - # Check permission - is_author = question.author_id == user.id - is_admin = user.role == UserRole.ADMIN - - if not is_author and not is_admin: - raise HTTPException(status_code=403, detail="无权删除此问题") - - # Delete related data (answers, likes, collections, etc.) - # Delete answers for this question - await db.execute(select(Answer).where(Answer.question_id == question_id)) - answers = ( - (await db.execute(select(Answer).where(Answer.question_id == question_id))) - .scalars() - .all() - ) - for answer in answers: - await db.delete(answer) - - # Delete likes for this question - await db.execute( - select(Like).where( - Like.target_type == "question", Like.target_id == question_id - ) - ) - likes = ( - ( - await db.execute( - select(Like).where( - Like.target_type == "question", Like.target_id == question_id - ) - ) - ) - .scalars() - .all() - ) - for like in likes: - await db.delete(like) - - # Delete collections for this question - collections = ( - ( - await db.execute( - select(Collection).where(Collection.question_id == question_id) - ) - ) - .scalars() - .all() - ) - for collection in collections: - await db.delete(collection) - - # Delete question tags - await db.execute( - select(QuestionTag).where(QuestionTag.question_id == question_id) - ) - tags = ( - ( - await db.execute( - select(QuestionTag).where(QuestionTag.question_id == question_id) - ) - ) - .scalars() - .all() - ) - for tag in tags: - await db.delete(tag) - - # Delete the question - await db.delete(question) - await db.commit() - - async def delete_answer( - self, - db: AsyncSession, - answer_id: str, - user: User, - ) -> None: - """ - Delete an answer. - - Author can delete their own answers - - Question author can delete answers on their question - - Admin can delete any answer - """ - from .enums import UserRole - - result = await db.execute( - select(Answer) - .options(selectinload(Answer.question)) - .where(Answer.id == answer_id) - ) - answer = result.scalar_one_or_none() - - if not answer: - raise HTTPException(status_code=404, detail="回答不存在") - - # Check permission - is_answer_author = answer.author_id == user.id - is_question_author = answer.question.author_id == user.id - is_admin = user.role == UserRole.ADMIN - - if not is_answer_author and not is_question_author and not is_admin: - raise HTTPException(status_code=403, detail="无权删除此回答") - - # Update question answer count - await db.execute( - update(Question) - .where(Question.id == answer.question_id) - .values(answer_count=Question.answer_count - 1) - ) - - # Delete likes for this answer - likes = ( - ( - await db.execute( - select(Like).where( - Like.target_type == "answer", Like.target_id == answer_id - ) - ) - ) - .scalars() - .all() - ) - for like in likes: - await db.delete(like) - - # Delete the answer - await db.delete(answer) - await db.commit() - - # ==================== User Profile Operations ==================== - - async def get_user_profile( - self, - db: AsyncSession, - user: User, - ) -> UserProfile: - """Get user profile with statistics.""" - from .enums import CertificationStatus - - # Reload user with certification to avoid lazy loading issues - user_query = ( - select(User) - .options(selectinload(User.certification)) - .where(User.id == user.id) - ) - result = await db.execute(user_query) - user = result.scalar_one() - - # Count questions - question_count_query = select(func.count()).where(Question.author_id == user.id) - question_count = (await db.execute(question_count_query)).scalar() or 0 - - # Count answers - answer_count_query = select(func.count()).where(Answer.author_id == user.id) - answer_count = (await db.execute(answer_count_query)).scalar() or 0 - - # Count likes received (on questions and answers) - question_likes_query = select(func.sum(Question.like_count)).where( - Question.author_id == user.id - ) - question_likes = (await db.execute(question_likes_query)).scalar() or 0 - - answer_likes_query = select(func.sum(Answer.like_count)).where( - Answer.author_id == user.id - ) - answer_likes = (await db.execute(answer_likes_query)).scalar() or 0 - - like_received_count = question_likes + answer_likes - - # Count collections - collection_count_query = select(func.count()).where( - Collection.user_id == user.id - ) - collection_count = (await db.execute(collection_count_query)).scalar() or 0 - - # Build certification info - certification_title = None - is_certified = False - if ( - user.certification - and user.certification.status == CertificationStatus.APPROVED - ): - is_certified = True - parts = [] - if user.certification.hospital_or_institution: - parts.append(user.certification.hospital_or_institution) - if user.certification.department: - parts.append(user.certification.department) - if user.certification.title: - parts.append(user.certification.title) - certification_title = " ".join(parts) if parts else None - - return UserProfile( - id=user.id, - nickname=user.nickname, - email=user.email, - avatar_url=user.avatar_url, - role=user.role, - is_certified=is_certified, - certification_title=certification_title, - stats=UserStats( - question_count=question_count, - answer_count=answer_count, - like_received_count=like_received_count, - collection_count=collection_count, - ), - created_at=user.created_at, - ) - - async def update_user_profile( - self, - db: AsyncSession, - user: User, - profile_update: UserProfileUpdate, - ) -> UserProfile: - """Update user profile (nickname/avatar/role).""" - from .enums import FAMILY_ROLES, PROFESSIONAL_ROLES - - if profile_update.nickname is not None: - # Moderate nickname - nickname_decision = await self._moderation.moderate_text( - profile_update.nickname - ) - if nickname_decision.result == ModerationResult.REJECTED: - raise HTTPException( - status_code=400, - detail=f"昵称包含敏感内容: {nickname_decision.reason}", - ) - user.nickname = profile_update.nickname - - if profile_update.email is not None and profile_update.email != user.email: - # Check if email is already taken - existing = await db.execute( - select(User).where(User.email == profile_update.email) - ) - if existing.scalar_one_or_none(): - raise HTTPException(status_code=400, detail="该邮箱已被使用") - user.email = profile_update.email - - if profile_update.avatar_url is not None: - user.avatar_url = profile_update.avatar_url - - if profile_update.role is not None: - # Map string to UserRole enum - role_map = { - "mom": UserRole.MOM, - "dad": UserRole.DAD, - "family": UserRole.FAMILY, - } - new_role = role_map.get(profile_update.role) - - if new_role is None or new_role not in FAMILY_ROLES: - raise HTTPException( - status_code=400, - detail="角色只能是: mom, dad, family", - ) - - # Certified professionals cannot change their role - if user.role in PROFESSIONAL_ROLES: - raise HTTPException( - status_code=403, - detail="认证专业人员不能修改角色", - ) - - # Admin cannot change their role this way - if user.role == UserRole.ADMIN: - raise HTTPException( - status_code=403, - detail="管理员不能通过此接口修改角色", - ) - - user.role = new_role - - await db.commit() - await db.refresh(user) - - return await self.get_user_profile(db, user) - - async def get_user_questions( - self, - db: AsyncSession, - user_id: str, - page: int = 1, - page_size: int = 20, - ) -> PaginatedResponse[MyQuestionListItem]: - """Get questions created by user.""" - query = ( - select(Question) - .options(selectinload(Question.tags)) - .where(Question.author_id == user_id) - .order_by(Question.created_at.desc()) - ) - - # Count total - count_query = select(func.count()).select_from(query.subquery()) - total = (await db.execute(count_query)).scalar() or 0 - - # Paginate - query = query.offset((page - 1) * page_size).limit(page_size) - - result = await db.execute(query) - questions = result.scalars().all() - - # Get user's likes and collections for these questions - liked_question_ids: set[str] = set() - collected_question_ids: set[str] = set() - if questions: - question_ids = [q.id for q in questions] - # Query likes - like_query = select(Like.target_id).where( - Like.user_id == user_id, - Like.target_type == "question", - Like.target_id.in_(question_ids), - ) - like_result = await db.execute(like_query) - liked_question_ids = set(like_result.scalars().all()) - # Query collections - collection_query = select(Collection.question_id).where( - Collection.user_id == user_id, - Collection.question_id.in_(question_ids), - ) - collection_result = await db.execute(collection_query) - collected_question_ids = set(collection_result.scalars().all()) - - items = [] - for q in questions: - items.append( - MyQuestionListItem( - id=q.id, - title=q.title, - content_preview=q.content[:100] + "..." - if len(q.content) > 100 - else q.content, - channel=q.channel.value, - tags=[self._build_tag_info(t) for t in (q.tags or [])], - view_count=q.view_count, - answer_count=q.answer_count, - like_count=q.like_count, - collection_count=q.collection_count, - status=q.status.value, - has_accepted_answer=q.accepted_answer_id is not None, - is_liked=q.id in liked_question_ids, - is_collected=q.id in collected_question_ids, - created_at=q.created_at, - ) - ) - - return PaginatedResponse( - items=items, - total=total, - page=page, - page_size=page_size, - total_pages=(total + page_size - 1) // page_size, - ) - - async def get_user_answers( - self, - db: AsyncSession, - user_id: str, - page: int = 1, - page_size: int = 20, - ) -> PaginatedResponse[MyAnswerListItem]: - """Get answers created by user with question context.""" - query = ( - select(Answer) - .options(selectinload(Answer.question)) - .where(Answer.author_id == user_id) - .order_by(Answer.created_at.desc()) - ) - - # Count total - count_query = select(func.count()).select_from(query.subquery()) - total = (await db.execute(count_query)).scalar() or 0 - - # Paginate - query = query.offset((page - 1) * page_size).limit(page_size) - - result = await db.execute(query) - answers = result.scalars().all() - - # Get user's likes for these answers - liked_answer_ids: set[str] = set() - if answers: - answer_ids = [a.id for a in answers] - like_query = select(Like.target_id).where( - Like.user_id == user_id, - Like.target_type == "answer", - Like.target_id.in_(answer_ids), - ) - like_result = await db.execute(like_query) - liked_answer_ids = set(like_result.scalars().all()) - - items = [] - for a in answers: - items.append( - MyAnswerListItem( - id=a.id, - content_preview=a.content[:200] + "..." - if len(a.content) > 200 - else a.content, - question=QuestionBrief( - id=a.question.id, - title=a.question.title, - channel=a.question.channel.value, - ), - is_professional=a.is_professional, - is_accepted=a.is_accepted, - like_count=a.like_count, - comment_count=a.comment_count, - status=a.status.value, - is_liked=a.id in liked_answer_ids, - created_at=a.created_at, - ) - ) - - return PaginatedResponse( - items=items, - total=total, - page=page, - page_size=page_size, - total_pages=(total + page_size - 1) // page_size, - ) - - -# Singleton pattern -_community_service: CommunityService | None = None - - -def get_community_service() -> CommunityService: - """Get CommunityService singleton instance.""" - global _community_service - if _community_service is None: - _community_service = CommunityService() - return _community_service diff --git a/backend/app/services/echo/__init__.py b/backend/app/services/echo/__init__.py deleted file mode 100644 index afaab954..00000000 --- a/backend/app/services/echo/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Echo Domain service for self-reflection and partner connection.""" - -from .router import router as echo_router - -__all__ = ["echo_router"] diff --git a/backend/app/services/echo/enums.py b/backend/app/services/echo/enums.py deleted file mode 100644 index 69e4ee70..00000000 --- a/backend/app/services/echo/enums.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Echo Domain enums.""" - -from enum import Enum - - -class TagType(str, Enum): - """Identity tag types for mom's self-reflection.""" - - MUSIC = "music" # 喜欢的音乐类型 - SOUND = "sound" # 喜欢的自然声音 - LITERATURE = "literature" # 喜欢的文学/书籍类型 - MEMORY = "memory" # 青春记忆关键词 - - -class AudioType(str, Enum): - """Audio resource types.""" - - NATURE = "nature" # 自然声音 - AMBIENT = "ambient" # 环境音 - MUSIC = "music" # 背景音乐 - GUIDED = "guided" # 引导冥想 - - -class MeditationPhase(str, Enum): - """Meditation session phases.""" - - INHALE = "inhale" # 吸气 (4s) - HOLD = "hold" # 屏息 (4s) - EXHALE = "exhale" # 呼气 (6s) - - -class SceneCategory(str, Enum): - """Scene categories for matching.""" - - NATURE = "nature" # 自然风景 - COZY = "cozy" # 温馨室内 - ABSTRACT = "abstract" # 抽象艺术 - VINTAGE = "vintage" # 复古怀旧 - OCEAN = "ocean" # 海洋主题 - - -# Breathing rhythm configuration (in seconds) -BREATHING_RHYTHM = { - MeditationPhase.INHALE: 4, - MeditationPhase.HOLD: 4, - MeditationPhase.EXHALE: 6, -} - -# Total cycle duration -BREATHING_CYCLE_SECONDS = sum(BREATHING_RHYTHM.values()) # 14 seconds - -# Default meditation durations (in minutes) -DEFAULT_MEDITATION_DURATIONS = [5, 10, 15, 20, 30] diff --git a/backend/app/services/echo/matching.py b/backend/app/services/echo/matching.py deleted file mode 100644 index 69f185bf..00000000 --- a/backend/app/services/echo/matching.py +++ /dev/null @@ -1,222 +0,0 @@ -"""Scene and audio matching algorithms for Echo Domain.""" - -import json -import random -from typing import TYPE_CHECKING - -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession - -if TYPE_CHECKING: - from .models import EchoAudioLibrary, EchoIdentityTag, EchoSceneLibrary - - -async def match_scenes( - db: AsyncSession, - identity_tags: list["EchoIdentityTag"], - limit: int = 5, -) -> list[tuple["EchoSceneLibrary", float]]: - """ - Match scenes based on identity tags. - - Algorithm: - 1. Extract all tag keywords - 2. Calculate each scene's match score = keyword matches × 10 - 3. Add random factor (0.8-1.2) for diversity - 4. Return top N scenes with scores - """ - from .models import EchoSceneLibrary - - # Extract keywords from tags - tag_keywords = set() - for tag in identity_tags: - # Add the content itself as a keyword - tag_keywords.add(tag.content.lower()) - # Add words from content - words = tag.content.lower().replace(",", " ").replace(",", " ").split() - tag_keywords.update(words) - - if not tag_keywords: - # If no tags, return random scenes - result = await db.execute( - select(EchoSceneLibrary) - .where(EchoSceneLibrary.is_active.is_(True)) - .order_by(EchoSceneLibrary.sort_order) - .limit(limit) - ) - scenes = list(result.scalars().all()) - return [(scene, 0.0) for scene in scenes] - - # Get all active scenes - result = await db.execute( - select(EchoSceneLibrary).where(EchoSceneLibrary.is_active.is_(True)) - ) - all_scenes = list(result.scalars().all()) - - # Calculate match scores - scored_scenes: list[tuple[EchoSceneLibrary, float]] = [] - for scene in all_scenes: - try: - scene_keywords = set( - kw.lower() for kw in json.loads(scene.keywords) if isinstance(kw, str) - ) - except (json.JSONDecodeError, TypeError): - scene_keywords = set() - - # Count keyword matches - matches = len(tag_keywords & scene_keywords) - - # Base score = matches × 10 - base_score = matches * 10 - - # Add random factor (0.8-1.2) for diversity - random_factor = random.uniform(0.8, 1.2) - final_score = base_score * random_factor - - # Boost if category matches tag type hints - category_boosts = { - "rock": "abstract", - "classical": "nature", - "jazz": "cozy", - "pop": "abstract", - "forest": "nature", - "ocean": "ocean", - "rain": "nature", - "cafe": "cozy", - "vintage": "vintage", - } - for keyword in tag_keywords: - if keyword in category_boosts: - if scene.category.value == category_boosts[keyword]: - final_score += 5 - - scored_scenes.append((scene, final_score)) - - # Sort by score descending - scored_scenes.sort(key=lambda x: x[1], reverse=True) - - # Return top N - return scored_scenes[:limit] - - -async def match_audio( - db: AsyncSession, - identity_tags: list["EchoIdentityTag"], - limit: int = 5, -) -> list[tuple["EchoAudioLibrary", float]]: - """ - Match audio based on identity tags. - - Similar algorithm to scene matching, but considers audio-specific keywords. - """ - from .models import EchoAudioLibrary - - # Extract keywords from tags - tag_keywords = set() - for tag in identity_tags: - tag_keywords.add(tag.content.lower()) - words = tag.content.lower().replace(",", " ").replace(",", " ").split() - tag_keywords.update(words) - - if not tag_keywords: - # If no tags, return random audio - result = await db.execute( - select(EchoAudioLibrary) - .where(EchoAudioLibrary.is_active.is_(True)) - .order_by(EchoAudioLibrary.sort_order) - .limit(limit) - ) - audios = list(result.scalars().all()) - return [(audio, 0.0) for audio in audios] - - # Get all active audio - result = await db.execute( - select(EchoAudioLibrary).where(EchoAudioLibrary.is_active.is_(True)) - ) - all_audio = list(result.scalars().all()) - - # Calculate match scores - scored_audio: list[tuple[EchoAudioLibrary, float]] = [] - for audio in all_audio: - try: - audio_keywords = set( - kw.lower() for kw in json.loads(audio.keywords) if isinstance(kw, str) - ) - except (json.JSONDecodeError, TypeError): - audio_keywords = set() - - # Count keyword matches - matches = len(tag_keywords & audio_keywords) - - # Base score = matches × 10 - base_score = matches * 10 - - # Add random factor for diversity - random_factor = random.uniform(0.8, 1.2) - final_score = base_score * random_factor - - # Audio type preference based on tag content - if any(kw in tag_keywords for kw in ["rain", "thunder", "forest", "wind"]): - if audio.audio_type.value == "nature": - final_score += 8 - - if any(kw in tag_keywords for kw in ["cafe", "city", "street"]): - if audio.audio_type.value == "ambient": - final_score += 8 - - scored_audio.append((audio, final_score)) - - # Sort by score descending - scored_audio.sort(key=lambda x: x[1], reverse=True) - - return scored_audio[:limit] - - -def calculate_clarity( - tasks_confirmed: int, - tasks_completed: int, - streak_days: int, - partner_level: str, -) -> tuple[int, dict[str, int]]: - """ - Calculate window clarity level. - - Formula: - - Base: Each confirmed task = +20%, max 60% - - Additional: Each unconfirmed completed task = +10%, max 20% - - Streak bonus: streak_days × 2%, max 20% - - Level bonus: intern=0, trainee=5, regular=10, gold=15 - - Returns: - Tuple of (total_clarity, breakdown_dict) - """ - # Base from confirmed tasks - base_clarity = min(tasks_confirmed * 20, 60) - - # Additional from completed but unconfirmed - unconfirmed_completed = max(0, tasks_completed - tasks_confirmed) - task_clarity = min(unconfirmed_completed * 10, 20) - - # Streak bonus - streak_bonus = min(streak_days * 2, 20) - - # Level bonus - level_bonuses = { - "intern": 0, - "trainee": 5, - "regular": 10, - "gold": 15, - } - level_bonus = level_bonuses.get(partner_level, 0) - - # Calculate total (cap at 100) - total = min(base_clarity + task_clarity + streak_bonus + level_bonus, 100) - - breakdown = { - "base_clarity": base_clarity, - "task_clarity": task_clarity, - "streak_bonus": streak_bonus, - "level_bonus": level_bonus, - } - - return total, breakdown diff --git a/backend/app/services/echo/models.py b/backend/app/services/echo/models.py deleted file mode 100644 index b97cbd49..00000000 --- a/backend/app/services/echo/models.py +++ /dev/null @@ -1,260 +0,0 @@ -"""Echo Domain SQLAlchemy models.""" - -from datetime import datetime -from uuid import uuid4 - -from sqlalchemy import ( - Boolean, - DateTime, - Float, - ForeignKey, - Integer, - String, - Text, - UniqueConstraint, -) -from sqlalchemy import Enum as SAEnum -from sqlalchemy.orm import Mapped, mapped_column, relationship - -from app.core.database import Base - -from .enums import AudioType, SceneCategory, TagType - - -def generate_uuid() -> str: - """Generate a UUID string.""" - return str(uuid4()) - - -# ============================================================ -# Identity Tags (Mom's self-reflection labels) -# ============================================================ - - -class EchoIdentityTag(Base): - """Mom's identity tags for scene/audio matching.""" - - __tablename__ = "echo_identity_tags" - __table_args__ = ( - UniqueConstraint("user_id", "tag_type", "content", name="uq_echo_identity_tag"), - ) - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - user_id: Mapped[str] = mapped_column( - String(36), ForeignKey("users.id", ondelete="CASCADE"), index=True - ) - - tag_type: Mapped[TagType] = mapped_column(SAEnum(TagType)) - content: Mapped[str] = mapped_column(String(100)) - - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - - # Relationships - user: Mapped["User"] = relationship("User") - - -# ============================================================ -# Scene Library (Preset scenes for meditation) -# ============================================================ - - -class EchoSceneLibrary(Base): - """Preset scene library for meditation visuals.""" - - __tablename__ = "echo_scene_library" - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - - # Scene info - title: Mapped[str] = mapped_column(String(100)) - description: Mapped[str | None] = mapped_column(Text, nullable=True) - image_url: Mapped[str] = mapped_column(String(500)) - thumbnail_url: Mapped[str | None] = mapped_column(String(500), nullable=True) - - # Categorization - category: Mapped[SceneCategory] = mapped_column(SAEnum(SceneCategory)) - keywords: Mapped[str] = mapped_column(Text) # JSON array of keywords - - # Metadata - is_active: Mapped[bool] = mapped_column(Boolean, default=True) - sort_order: Mapped[int] = mapped_column(Integer, default=0) - - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - - -# ============================================================ -# Audio Library (Preset audio for meditation) -# ============================================================ - - -class EchoAudioLibrary(Base): - """Preset audio library for meditation sounds.""" - - __tablename__ = "echo_audio_library" - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - - # Audio info - title: Mapped[str] = mapped_column(String(100)) - description: Mapped[str | None] = mapped_column(Text, nullable=True) - audio_url: Mapped[str] = mapped_column(String(500)) - duration_seconds: Mapped[int | None] = mapped_column(Integer, nullable=True) - - # Categorization - audio_type: Mapped[AudioType] = mapped_column(SAEnum(AudioType)) - keywords: Mapped[str] = mapped_column(Text) # JSON array of keywords - - # External source (e.g., freesound.org) - source: Mapped[str | None] = mapped_column(String(100), nullable=True) - source_id: Mapped[str | None] = mapped_column(String(50), nullable=True) - - # Metadata - is_active: Mapped[bool] = mapped_column(Boolean, default=True) - sort_order: Mapped[int] = mapped_column(Integer, default=0) - - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - - -# ============================================================ -# Meditation Sessions (Mom's meditation records) -# ============================================================ - - -class EchoMeditationSession(Base): - """Mom's meditation session records.""" - - __tablename__ = "echo_meditation_sessions" - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - user_id: Mapped[str] = mapped_column( - String(36), ForeignKey("users.id", ondelete="CASCADE"), index=True - ) - - # Session settings - target_duration_minutes: Mapped[int] = mapped_column(Integer) - scene_id: Mapped[str | None] = mapped_column( - String(36), ForeignKey("echo_scene_library.id"), nullable=True - ) - audio_id: Mapped[str | None] = mapped_column( - String(36), ForeignKey("echo_audio_library.id"), nullable=True - ) - - # Session results - actual_duration_seconds: Mapped[int | None] = mapped_column(Integer, nullable=True) - completed: Mapped[bool] = mapped_column(Boolean, default=False) - - # Timestamps - started_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - ended_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - - # Relationships - user: Mapped["User"] = relationship("User") - scene: Mapped["EchoSceneLibrary | None"] = relationship("EchoSceneLibrary") - audio: Mapped["EchoAudioLibrary | None"] = relationship("EchoAudioLibrary") - - -# ============================================================ -# Partner Memories (Injected by partner for mom) -# ============================================================ - - -class EchoPartnerMemory(Base): - """Memories injected by partner for mom to discover.""" - - __tablename__ = "echo_partner_memories" - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - binding_id: Mapped[str] = mapped_column( - String(36), ForeignKey("partner_bindings.id", ondelete="CASCADE"), index=True - ) - - # Memory content - title: Mapped[str] = mapped_column(String(200)) - content: Mapped[str] = mapped_column(Text) - image_url: Mapped[str | None] = mapped_column(String(500), nullable=True) - - # Reveal condition (clarity level threshold) - reveal_at_clarity: Mapped[int] = mapped_column(Integer, default=50) # 0-100 - - # Status - is_revealed: Mapped[bool] = mapped_column(Boolean, default=False) - revealed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - - # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - - # Relationships - binding: Mapped["PartnerBinding"] = relationship("PartnerBinding") - - -# ============================================================ -# Window Clarity (Partner's progress visualization) -# ============================================================ - - -class EchoWindowClarity(Base): - """Window clarity state for partner mode.""" - - __tablename__ = "echo_window_clarity" - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - binding_id: Mapped[str] = mapped_column( - String(36), - ForeignKey("partner_bindings.id", ondelete="CASCADE"), - unique=True, - index=True, - ) - - # Current clarity state (cached, recalculated on request) - clarity_level: Mapped[int] = mapped_column(Integer, default=0) # 0-100 - last_calculated_at: Mapped[datetime] = mapped_column( - DateTime, default=datetime.utcnow - ) - - # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - updated_at: Mapped[datetime] = mapped_column( - DateTime, default=datetime.utcnow, onupdate=datetime.utcnow - ) - - # Relationships - binding: Mapped["PartnerBinding"] = relationship("PartnerBinding") - - -# ============================================================ -# Youth Memoirs (AI-generated memoirs for mom) -# ============================================================ - - -class EchoYouthMemoir(Base): - """AI-generated youth memoirs based on identity tags.""" - - __tablename__ = "echo_youth_memoirs" - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - user_id: Mapped[str] = mapped_column( - String(36), ForeignKey("users.id", ondelete="CASCADE"), index=True - ) - - # Memoir content - title: Mapped[str] = mapped_column(String(200)) - content: Mapped[str] = mapped_column(Text) - cover_image_url: Mapped[str | None] = mapped_column(String(500), nullable=True) - - # Generation metadata - generation_prompt: Mapped[str | None] = mapped_column(Text, nullable=True) - tags_used: Mapped[str] = mapped_column(Text) # JSON array of tag IDs used - - # Rating - user_rating: Mapped[float | None] = mapped_column(Float, nullable=True) # 1-5 - - # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - - # Relationships - user: Mapped["User"] = relationship("User") - - -# Import for relationship type hints -from app.services.community.models import User # noqa: E402, F401 -from app.services.guardian.models import PartnerBinding # noqa: E402, F401 diff --git a/backend/app/services/echo/router.py b/backend/app/services/echo/router.py deleted file mode 100644 index 716ea39e..00000000 --- a/backend/app/services/echo/router.py +++ /dev/null @@ -1,293 +0,0 @@ -"""Echo Domain API router.""" - -from typing import Annotated - -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.ext.asyncio import AsyncSession - -from app.core.database import get_db -from app.services.auth.dependencies import CurrentUserJWT - -from .schemas import ( - AudioResponse, - EchoStatusResponse, - IdentityTagCreate, - IdentityTagListResponse, - IdentityTagResponse, - MeditationEndRequest, - MeditationEndResponse, - MeditationStartRequest, - MeditationStartResponse, - MeditationStatsResponse, - MemoirGenerateRequest, - MemoirListResponse, - MemoirRatingRequest, - MemoirResponse, - MemoryInjectRequest, - PartnerMemoryResponse, - RevealedMemoriesResponse, - SceneResponse, - WindowClarityResponse, -) -from .service import EchoService - -router = APIRouter(prefix="/echo", tags=["echo"]) - - -# ============================================================ -# Echo Status -# ============================================================ - - -@router.get("/status", response_model=EchoStatusResponse) -async def get_echo_status( - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], -) -> EchoStatusResponse: - """Get user's Echo status including role and statistics.""" - service = EchoService(db) - return await service.get_echo_status(current_user.id) - - -# ============================================================ -# Identity Tags (Mom Mode) -# ============================================================ - - -@router.get("/identity-tags", response_model=IdentityTagListResponse) -async def get_identity_tags( - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], -) -> IdentityTagListResponse: - """Get user's identity tags grouped by type.""" - service = EchoService(db) - return await service.get_identity_tags(current_user.id) - - -@router.post("/identity-tags", response_model=IdentityTagResponse) -async def create_identity_tag( - data: IdentityTagCreate, - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], -) -> IdentityTagResponse: - """Create a new identity tag.""" - service = EchoService(db) - try: - tag = await service.create_identity_tag( - current_user.id, data.tag_type, data.content - ) - await db.commit() - return tag - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) from None - - -@router.delete("/identity-tags/{tag_id}") -async def delete_identity_tag( - tag_id: str, - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], -) -> dict: - """Delete an identity tag.""" - service = EchoService(db) - try: - await service.delete_identity_tag(tag_id, current_user.id) - await db.commit() - return {"message": "标签已删除"} - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) from None - - -# ============================================================ -# Scenes & Audio Matching -# ============================================================ - - -@router.get("/scenes/match", response_model=list[SceneResponse]) -async def match_scenes( - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], - limit: int = Query(5, ge=1, le=20), # noqa: B008 -) -> list[SceneResponse]: - """Match scenes based on user's identity tags.""" - service = EchoService(db) - return await service.match_scenes_for_user(current_user.id, limit) - - -@router.get("/audio/match", response_model=list[AudioResponse]) -async def match_audio( - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], - limit: int = Query(5, ge=1, le=20), # noqa: B008 -) -> list[AudioResponse]: - """Match audio based on user's identity tags.""" - service = EchoService(db) - return await service.match_audio_for_user(current_user.id, limit) - - -# ============================================================ -# Meditation (Mom Mode) -# ============================================================ - - -@router.post("/meditation/start", response_model=MeditationStartResponse) -async def start_meditation( - data: MeditationStartRequest, - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], -) -> MeditationStartResponse: - """Start a new meditation session.""" - service = EchoService(db) - result = await service.start_meditation( - current_user.id, - data.target_duration_minutes, - data.scene_id, - data.audio_id, - ) - await db.commit() - return result - - -@router.post("/meditation/end", response_model=MeditationEndResponse) -async def end_meditation( - data: MeditationEndRequest, - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], -) -> MeditationEndResponse: - """End a meditation session.""" - service = EchoService(db) - try: - result = await service.end_meditation( - data.session_id, - current_user.id, - data.actual_duration_seconds, - ) - await db.commit() - return result - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) from None - - -@router.get("/meditation/stats", response_model=MeditationStatsResponse) -async def get_meditation_stats( - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], -) -> MeditationStatsResponse: - """Get meditation statistics for the user.""" - service = EchoService(db) - return await service.get_meditation_stats(current_user.id) - - -# ============================================================ -# Window Clarity (Partner Mode) -# ============================================================ - - -@router.get("/window/clarity", response_model=WindowClarityResponse) -async def get_window_clarity( - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], -) -> WindowClarityResponse: - """Get window clarity status for partner mode.""" - service = EchoService(db) - try: - result = await service.get_window_clarity(current_user.id) - await db.commit() - return result - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) from None - - -# ============================================================ -# Partner Memories -# ============================================================ - - -@router.post("/memories", response_model=PartnerMemoryResponse) -async def inject_memory( - data: MemoryInjectRequest, - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], -) -> PartnerMemoryResponse: - """Partner injects a memory for mom to discover.""" - service = EchoService(db) - try: - result = await service.inject_memory( - current_user.id, - data.title, - data.content, - data.image_url, - data.reveal_at_clarity, - ) - await db.commit() - return result - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) from None - - -@router.get("/memories/revealed", response_model=RevealedMemoriesResponse) -async def get_revealed_memories( - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], -) -> RevealedMemoriesResponse: - """Get revealed memories for mom.""" - service = EchoService(db) - try: - result = await service.get_revealed_memories(current_user.id) - await db.commit() - return result - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) from None - - -# ============================================================ -# Youth Memoirs -# ============================================================ - - -@router.get("/memoirs", response_model=MemoirListResponse) -async def get_memoirs( - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], - limit: int = Query(10, ge=1, le=50), # noqa: B008 - offset: int = Query(0, ge=0), # noqa: B008 -) -> MemoirListResponse: - """Get user's youth memoirs.""" - service = EchoService(db) - return await service.get_memoirs(current_user.id, limit, offset) - - -@router.post("/memoirs/generate", response_model=MemoirResponse) -async def generate_memoir( - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], - data: MemoirGenerateRequest | None = None, -) -> MemoirResponse: - """Generate a youth memoir based on identity tags.""" - service = EchoService(db) - try: - result = await service.generate_memoir( - current_user.id, - data.theme if data else None, - ) - await db.commit() - return result - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) from None - - -@router.post("/memoirs/{memoir_id}/rate", response_model=MemoirResponse) -async def rate_memoir( - memoir_id: str, - data: MemoirRatingRequest, - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], -) -> MemoirResponse: - """Rate a memoir.""" - service = EchoService(db) - try: - result = await service.rate_memoir(memoir_id, current_user.id, data.rating) - await db.commit() - return result - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) from None diff --git a/backend/app/services/echo/schemas.py b/backend/app/services/echo/schemas.py deleted file mode 100644 index 481777a3..00000000 --- a/backend/app/services/echo/schemas.py +++ /dev/null @@ -1,271 +0,0 @@ -"""Echo Domain Pydantic schemas.""" - -from datetime import datetime - -from pydantic import BaseModel, Field - -from .enums import AudioType, MeditationPhase, SceneCategory, TagType - -# ============================================================ -# Echo Status -# ============================================================ - - -class EchoStatusResponse(BaseModel): - """Response for user's Echo status.""" - - role: str | None # "mom" | "partner" | None - has_binding: bool - binding_id: str | None - identity_tags_count: int - meditation_sessions_count: int - total_meditation_minutes: int - - -# ============================================================ -# Identity Tags -# ============================================================ - - -class IdentityTagCreate(BaseModel): - """Request to create an identity tag.""" - - tag_type: TagType - content: str = Field(..., min_length=1, max_length=100) - - -class IdentityTagResponse(BaseModel): - """Response for an identity tag.""" - - id: str - tag_type: TagType - content: str - created_at: datetime - - class Config: - from_attributes = True - - -class IdentityTagListResponse(BaseModel): - """Response for identity tag list grouped by type.""" - - music: list[IdentityTagResponse] - sound: list[IdentityTagResponse] - literature: list[IdentityTagResponse] - memory: list[IdentityTagResponse] - - -# ============================================================ -# Scenes -# ============================================================ - - -class SceneResponse(BaseModel): - """Response for a scene.""" - - id: str - title: str - description: str | None - image_url: str - thumbnail_url: str | None - category: SceneCategory - keywords: list[str] - match_score: float | None = None # Used when matching - - class Config: - from_attributes = True - - -class SceneMatchRequest(BaseModel): - """Request for scene matching.""" - - limit: int = Field(default=5, ge=1, le=20) - - -# ============================================================ -# Audio -# ============================================================ - - -class AudioResponse(BaseModel): - """Response for an audio resource.""" - - id: str - title: str - description: str | None - audio_url: str - duration_seconds: int | None - audio_type: AudioType - keywords: list[str] - match_score: float | None = None - - class Config: - from_attributes = True - - -class AudioMatchRequest(BaseModel): - """Request for audio matching.""" - - limit: int = Field(default=5, ge=1, le=20) - - -# ============================================================ -# Meditation -# ============================================================ - - -class MeditationStartRequest(BaseModel): - """Request to start a meditation session.""" - - target_duration_minutes: int = Field(default=10, ge=1, le=60) - scene_id: str | None = None - audio_id: str | None = None - - -class MeditationStartResponse(BaseModel): - """Response for starting meditation.""" - - session_id: str - target_duration_minutes: int - scene: SceneResponse | None - audio: AudioResponse | None - breathing_rhythm: dict[str, int] - - -class MeditationEndRequest(BaseModel): - """Request to end a meditation session.""" - - session_id: str - actual_duration_seconds: int - - -class MeditationEndResponse(BaseModel): - """Response for ending meditation.""" - - session_id: str - completed: bool - actual_duration_seconds: int - target_duration_minutes: int - completion_rate: float # 0-1 - - -class MeditationStatsResponse(BaseModel): - """Response for meditation statistics.""" - - total_sessions: int - completed_sessions: int - total_minutes: int - average_duration_minutes: float - current_streak: int - longest_streak: int - last_session_date: datetime | None - - -class BreathingGuide(BaseModel): - """Breathing guide for meditation.""" - - phase: MeditationPhase - duration_seconds: int - instruction: str - - -# ============================================================ -# Window Clarity (Partner Mode) -# ============================================================ - - -class WindowClarityResponse(BaseModel): - """Response for window clarity status.""" - - clarity_level: int # 0-100 - tasks_completed_today: int - tasks_confirmed_today: int - streak_bonus: int - level_bonus: int - breakdown: dict[str, int] # Detailed breakdown of clarity sources - - -class ClarityCalculation(BaseModel): - """Detailed clarity calculation breakdown.""" - - base_clarity: int # From confirmed tasks - task_clarity: int # From completed but unconfirmed - streak_bonus: int # From streak days - level_bonus: int # From partner level - total: int - - -# ============================================================ -# Partner Memories -# ============================================================ - - -class MemoryInjectRequest(BaseModel): - """Request to inject a memory for partner.""" - - title: str = Field(..., min_length=1, max_length=200) - content: str = Field(..., min_length=1, max_length=2000) - image_url: str | None = None - reveal_at_clarity: int = Field(default=50, ge=0, le=100) - - -class PartnerMemoryResponse(BaseModel): - """Response for a partner memory.""" - - id: str - title: str - content: str - image_url: str | None - reveal_at_clarity: int - is_revealed: bool - revealed_at: datetime | None - created_at: datetime - - class Config: - from_attributes = True - - -class RevealedMemoriesResponse(BaseModel): - """Response for revealed memories.""" - - memories: list[PartnerMemoryResponse] - current_clarity: int - next_memory_at: int | None # Clarity level needed for next reveal - - -# ============================================================ -# Youth Memoirs -# ============================================================ - - -class MemoirGenerateRequest(BaseModel): - """Request to generate a youth memoir.""" - - theme: str | None = None # Optional theme hint - - -class MemoirResponse(BaseModel): - """Response for a youth memoir.""" - - id: str - title: str - content: str - cover_image_url: str | None - user_rating: float | None - created_at: datetime - - class Config: - from_attributes = True - - -class MemoirRatingRequest(BaseModel): - """Request to rate a memoir.""" - - rating: float = Field(..., ge=1, le=5) - - -class MemoirListResponse(BaseModel): - """Response for memoir list.""" - - memoirs: list[MemoirResponse] - total: int diff --git a/backend/app/services/echo/seed_data.py b/backend/app/services/echo/seed_data.py deleted file mode 100644 index 327c6ada..00000000 --- a/backend/app/services/echo/seed_data.py +++ /dev/null @@ -1,374 +0,0 @@ -"""Seed data for Echo Domain - preset scenes and audio.""" - -import json - -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession - -from .enums import AudioType, SceneCategory -from .models import EchoAudioLibrary, EchoSceneLibrary - -# ============================================================ -# Preset Scenes -# ============================================================ - -PRESET_SCENES = [ - # Nature scenes - { - "title": "静谧森林", - "description": "阳光透过树叶洒下斑驳光影,一条小径通向未知", - "image_url": "/static/echo/scenes/forest.jpg", - "thumbnail_url": "/static/echo/scenes/forest_thumb.jpg", - "category": SceneCategory.NATURE, - "keywords": [ - "森林", - "树木", - "阳光", - "安静", - "自然", - "绿色", - "forest", - "nature", - ], - }, - { - "title": "山间云海", - "description": "站在山巅俯瞰云海翻涌,世界在脚下静静流淌", - "image_url": "/static/echo/scenes/mountains.jpg", - "thumbnail_url": "/static/echo/scenes/mountains_thumb.jpg", - "category": SceneCategory.NATURE, - "keywords": [ - "山", - "云海", - "高处", - "壮观", - "自然", - "宁静", - "mountains", - "clouds", - ], - }, - { - "title": "雨后花园", - "description": "雨后的花园空气清新,水珠在花瓣上闪烁", - "image_url": "/static/echo/scenes/garden.jpg", - "thumbnail_url": "/static/echo/scenes/garden_thumb.jpg", - "category": SceneCategory.NATURE, - "keywords": ["花园", "雨后", "清新", "花朵", "自然", "春天", "garden", "rain"], - }, - # Ocean scenes - { - "title": "日落海滩", - "description": "金色的阳光洒在海面上,浪花轻轻拍打沙滩", - "image_url": "/static/echo/scenes/beach_sunset.jpg", - "thumbnail_url": "/static/echo/scenes/beach_sunset_thumb.jpg", - "category": SceneCategory.OCEAN, - "keywords": [ - "海滩", - "日落", - "海浪", - "沙滩", - "金色", - "浪漫", - "beach", - "sunset", - "ocean", - ], - }, - { - "title": "深海静谧", - "description": "深蓝色的海水中,光线柔和地穿透水面", - "image_url": "/static/echo/scenes/deep_ocean.jpg", - "thumbnail_url": "/static/echo/scenes/deep_ocean_thumb.jpg", - "category": SceneCategory.OCEAN, - "keywords": ["海洋", "深海", "蓝色", "安静", "神秘", "ocean", "deep", "blue"], - }, - # Cozy scenes - { - "title": "温暖书房", - "description": "壁炉边的舒适角落,一杯茶,一本书", - "image_url": "/static/echo/scenes/cozy_study.jpg", - "thumbnail_url": "/static/echo/scenes/cozy_study_thumb.jpg", - "category": SceneCategory.COZY, - "keywords": ["书房", "温暖", "壁炉", "阅读", "舒适", "室内", "cozy", "reading"], - }, - { - "title": "雨天咖啡馆", - "description": "窗外雨声淅沥,咖啡香气弥漫的午后", - "image_url": "/static/echo/scenes/rainy_cafe.jpg", - "thumbnail_url": "/static/echo/scenes/rainy_cafe_thumb.jpg", - "category": SceneCategory.COZY, - "keywords": ["咖啡馆", "下雨", "温暖", "午后", "咖啡", "cafe", "rain", "cozy"], - }, - { - "title": "星光露台", - "description": "夜晚的露台,繁星点点,微风轻拂", - "image_url": "/static/echo/scenes/starry_terrace.jpg", - "thumbnail_url": "/static/echo/scenes/starry_terrace_thumb.jpg", - "category": SceneCategory.COZY, - "keywords": [ - "露台", - "星空", - "夜晚", - "浪漫", - "微风", - "stars", - "night", - "terrace", - ], - }, - # Abstract scenes - { - "title": "流动色彩", - "description": "柔和的色彩相互流动融合,如梦似幻", - "image_url": "/static/echo/scenes/abstract_colors.jpg", - "thumbnail_url": "/static/echo/scenes/abstract_colors_thumb.jpg", - "category": SceneCategory.ABSTRACT, - "keywords": [ - "抽象", - "色彩", - "艺术", - "流动", - "梦幻", - "abstract", - "colors", - "art", - ], - }, - { - "title": "光与影", - "description": "简约的光影交织,创造宁静的视觉空间", - "image_url": "/static/echo/scenes/light_shadow.jpg", - "thumbnail_url": "/static/echo/scenes/light_shadow_thumb.jpg", - "category": SceneCategory.ABSTRACT, - "keywords": [ - "光影", - "简约", - "现代", - "艺术", - "冥想", - "light", - "shadow", - "minimal", - ], - }, - # Vintage scenes - { - "title": "老唱片店", - "description": "复古的唱片店,黑胶唱片旋转,音乐流淌", - "image_url": "/static/echo/scenes/record_store.jpg", - "thumbnail_url": "/static/echo/scenes/record_store_thumb.jpg", - "category": SceneCategory.VINTAGE, - "keywords": [ - "唱片", - "复古", - "音乐", - "怀旧", - "黑胶", - "vinyl", - "vintage", - "music", - ], - }, - { - "title": "旧时光影院", - "description": "老式电影院的红色座椅,银幕上光影闪烁", - "image_url": "/static/echo/scenes/vintage_cinema.jpg", - "thumbnail_url": "/static/echo/scenes/vintage_cinema_thumb.jpg", - "category": SceneCategory.VINTAGE, - "keywords": [ - "电影院", - "复古", - "怀旧", - "红色", - "电影", - "cinema", - "vintage", - "retro", - ], - }, -] - -# ============================================================ -# Preset Audio -# ============================================================ - -PRESET_AUDIO = [ - # Nature sounds - { - "title": "森林鸟鸣", - "description": "清晨森林中各种鸟类的歌声", - "audio_url": "/static/echo/audio/forest_birds.mp3", - "duration_seconds": 600, - "audio_type": AudioType.NATURE, - "keywords": ["森林", "鸟", "自然", "清晨", "宁静", "forest", "birds", "nature"], - "source": "freesound", - }, - { - "title": "温柔雨声", - "description": "轻柔的雨滴落在窗户和树叶上", - "audio_url": "/static/echo/audio/gentle_rain.mp3", - "duration_seconds": 600, - "audio_type": AudioType.NATURE, - "keywords": ["雨", "下雨", "安静", "放松", "rain", "gentle", "relaxing"], - "source": "freesound", - }, - { - "title": "海浪轻拍", - "description": "海浪有节奏地轻轻拍打沙滩", - "audio_url": "/static/echo/audio/ocean_waves.mp3", - "duration_seconds": 600, - "audio_type": AudioType.NATURE, - "keywords": ["海浪", "海洋", "沙滩", "放松", "ocean", "waves", "beach"], - "source": "freesound", - }, - { - "title": "潺潺溪流", - "description": "山间小溪流过石头的声音", - "audio_url": "/static/echo/audio/stream.mp3", - "duration_seconds": 600, - "audio_type": AudioType.NATURE, - "keywords": ["溪流", "水声", "山间", "自然", "stream", "water", "nature"], - "source": "freesound", - }, - { - "title": "夏日虫鸣", - "description": "夏夜的蝉鸣和蟋蟀声", - "audio_url": "/static/echo/audio/summer_insects.mp3", - "duration_seconds": 600, - "audio_type": AudioType.NATURE, - "keywords": ["夏天", "虫鸣", "蝉", "夜晚", "summer", "insects", "night"], - "source": "freesound", - }, - # Ambient sounds - { - "title": "咖啡馆低语", - "description": "舒适咖啡馆的背景人声和咖啡机声", - "audio_url": "/static/echo/audio/cafe_ambient.mp3", - "duration_seconds": 600, - "audio_type": AudioType.AMBIENT, - "keywords": ["咖啡馆", "背景", "人声", "咖啡", "cafe", "ambient", "coffee"], - "source": "freesound", - }, - { - "title": "壁炉噼啪", - "description": "温暖壁炉的木柴燃烧声", - "audio_url": "/static/echo/audio/fireplace.mp3", - "duration_seconds": 600, - "audio_type": AudioType.AMBIENT, - "keywords": ["壁炉", "火焰", "温暖", "冬天", "fireplace", "fire", "warm"], - "source": "freesound", - }, - { - "title": "图书馆静谧", - "description": "图书馆中轻微的翻书声和脚步声", - "audio_url": "/static/echo/audio/library.mp3", - "duration_seconds": 600, - "audio_type": AudioType.AMBIENT, - "keywords": ["图书馆", "安静", "翻书", "学习", "library", "quiet", "study"], - "source": "freesound", - }, - # Music - { - "title": "钢琴冥想曲", - "description": "舒缓的钢琴旋律,适合深度放松", - "audio_url": "/static/echo/audio/piano_meditation.mp3", - "duration_seconds": 600, - "audio_type": AudioType.MUSIC, - "keywords": [ - "钢琴", - "冥想", - "放松", - "古典", - "piano", - "meditation", - "classical", - ], - "source": "freesound", - }, - { - "title": "环境音乐", - "description": "空灵的电子环境音乐", - "audio_url": "/static/echo/audio/ambient_electronic.mp3", - "duration_seconds": 600, - "audio_type": AudioType.MUSIC, - "keywords": [ - "环境", - "电子", - "空灵", - "冥想", - "ambient", - "electronic", - "meditation", - ], - "source": "freesound", - }, -] - - -async def seed_scenes(db: AsyncSession) -> int: - """Seed preset scenes into database. Returns count of new scenes added.""" - # Check existing scenes - result = await db.execute(select(EchoSceneLibrary)) - existing = result.scalars().all() - existing_titles = {s.title for s in existing} - - count = 0 - for i, scene_data in enumerate(PRESET_SCENES): - if scene_data["title"] not in existing_titles: - scene = EchoSceneLibrary( - title=scene_data["title"], - description=scene_data["description"], - image_url=scene_data["image_url"], - thumbnail_url=scene_data.get("thumbnail_url"), - category=scene_data["category"], - keywords=json.dumps(scene_data["keywords"], ensure_ascii=False), - sort_order=i, - ) - db.add(scene) - count += 1 - - if count > 0: - await db.commit() - - return count - - -async def seed_audio(db: AsyncSession) -> int: - """Seed preset audio into database. Returns count of new audio added.""" - # Check existing audio - result = await db.execute(select(EchoAudioLibrary)) - existing = result.scalars().all() - existing_titles = {a.title for a in existing} - - count = 0 - for i, audio_data in enumerate(PRESET_AUDIO): - if audio_data["title"] not in existing_titles: - audio = EchoAudioLibrary( - title=audio_data["title"], - description=audio_data["description"], - audio_url=audio_data["audio_url"], - duration_seconds=audio_data.get("duration_seconds"), - audio_type=audio_data["audio_type"], - keywords=json.dumps(audio_data["keywords"], ensure_ascii=False), - source=audio_data.get("source"), - sort_order=i, - ) - db.add(audio) - count += 1 - - if count > 0: - await db.commit() - - return count - - -async def seed_echo_data(db: AsyncSession) -> dict[str, int]: - """Seed all Echo preset data. Returns counts of new items added.""" - scenes_added = await seed_scenes(db) - audio_added = await seed_audio(db) - - return { - "scenes": scenes_added, - "audio": audio_added, - } diff --git a/backend/app/services/echo/service.py b/backend/app/services/echo/service.py deleted file mode 100644 index 4de6f8a4..00000000 --- a/backend/app/services/echo/service.py +++ /dev/null @@ -1,778 +0,0 @@ -"""Echo Domain service layer.""" - -import json -from datetime import date, datetime, timedelta - -from sqlalchemy import func, select -from sqlalchemy.ext.asyncio import AsyncSession - -from app.services.guardian.enums import BindingStatus, PartnerLevel, TaskStatus -from app.services.guardian.models import ( - PartnerBinding, - PartnerDailyTask, - PartnerProgress, -) - -from .enums import BREATHING_RHYTHM, TagType -from .matching import calculate_clarity, match_audio, match_scenes -from .models import ( - EchoAudioLibrary, - EchoIdentityTag, - EchoMeditationSession, - EchoPartnerMemory, - EchoSceneLibrary, - EchoWindowClarity, - EchoYouthMemoir, -) -from .schemas import ( - AudioResponse, - EchoStatusResponse, - IdentityTagListResponse, - IdentityTagResponse, - MeditationEndResponse, - MeditationStartResponse, - MeditationStatsResponse, - MemoirListResponse, - MemoirResponse, - PartnerMemoryResponse, - RevealedMemoriesResponse, - SceneResponse, - WindowClarityResponse, -) - - -class EchoService: - """Service for Echo Domain feature.""" - - def __init__(self, db: AsyncSession): - self.db = db - - # ============================================================ - # Echo Status - # ============================================================ - - async def get_echo_status(self, user_id: str) -> EchoStatusResponse: - """Get user's Echo status including role and statistics.""" - # Check binding - binding = await self._get_binding_for_user(user_id) - - role = None - binding_id = None - if binding: - if binding.mom_id == user_id: - role = "mom" - elif binding.partner_id == user_id: - role = "partner" - binding_id = binding.id - - # Count identity tags - tags_result = await self.db.execute( - select(func.count(EchoIdentityTag.id)).where( - EchoIdentityTag.user_id == user_id - ) - ) - tags_count = tags_result.scalar() or 0 - - # Count meditation sessions - sessions_result = await self.db.execute( - select(func.count(EchoMeditationSession.id)).where( - EchoMeditationSession.user_id == user_id - ) - ) - sessions_count = sessions_result.scalar() or 0 - - # Total meditation minutes - minutes_result = await self.db.execute( - select(func.sum(EchoMeditationSession.actual_duration_seconds)).where( - EchoMeditationSession.user_id == user_id, - EchoMeditationSession.completed.is_(True), - ) - ) - total_seconds = minutes_result.scalar() or 0 - total_minutes = total_seconds // 60 - - return EchoStatusResponse( - role=role, - has_binding=binding is not None and binding.status == BindingStatus.ACTIVE, - binding_id=binding_id, - identity_tags_count=tags_count, - meditation_sessions_count=sessions_count, - total_meditation_minutes=total_minutes, - ) - - # ============================================================ - # Identity Tags - # ============================================================ - - async def get_identity_tags(self, user_id: str) -> IdentityTagListResponse: - """Get user's identity tags grouped by type.""" - result = await self.db.execute( - select(EchoIdentityTag) - .where(EchoIdentityTag.user_id == user_id) - .order_by(EchoIdentityTag.created_at.desc()) - ) - tags = list(result.scalars().all()) - - # Group by type - grouped: dict[str, list[IdentityTagResponse]] = { - "music": [], - "sound": [], - "literature": [], - "memory": [], - } - - for tag in tags: - tag_response = IdentityTagResponse( - id=tag.id, - tag_type=tag.tag_type, - content=tag.content, - created_at=tag.created_at, - ) - grouped[tag.tag_type.value].append(tag_response) - - return IdentityTagListResponse(**grouped) - - async def create_identity_tag( - self, user_id: str, tag_type: TagType, content: str - ) -> IdentityTagResponse: - """Create a new identity tag.""" - # Check for duplicates - existing = await self.db.execute( - select(EchoIdentityTag).where( - EchoIdentityTag.user_id == user_id, - EchoIdentityTag.tag_type == tag_type, - EchoIdentityTag.content == content, - ) - ) - if existing.scalar_one_or_none(): - raise ValueError("该标签已存在") - - tag = EchoIdentityTag( - user_id=user_id, - tag_type=tag_type, - content=content, - ) - self.db.add(tag) - await self.db.flush() - - return IdentityTagResponse( - id=tag.id, - tag_type=tag.tag_type, - content=tag.content, - created_at=tag.created_at, - ) - - async def delete_identity_tag(self, tag_id: str, user_id: str) -> None: - """Delete an identity tag.""" - result = await self.db.execute( - select(EchoIdentityTag).where( - EchoIdentityTag.id == tag_id, - EchoIdentityTag.user_id == user_id, - ) - ) - tag = result.scalar_one_or_none() - - if not tag: - raise ValueError("标签不存在") - - await self.db.delete(tag) - await self.db.flush() - - # ============================================================ - # Scenes & Audio Matching - # ============================================================ - - async def match_scenes_for_user( - self, user_id: str, limit: int = 5 - ) -> list[SceneResponse]: - """Match scenes based on user's identity tags.""" - # Get user's tags - result = await self.db.execute( - select(EchoIdentityTag).where(EchoIdentityTag.user_id == user_id) - ) - tags = list(result.scalars().all()) - - # Match scenes - matched = await match_scenes(self.db, tags, limit) - - return [self._scene_to_response(scene, score) for scene, score in matched] - - async def match_audio_for_user( - self, user_id: str, limit: int = 5 - ) -> list[AudioResponse]: - """Match audio based on user's identity tags.""" - # Get user's tags - result = await self.db.execute( - select(EchoIdentityTag).where(EchoIdentityTag.user_id == user_id) - ) - tags = list(result.scalars().all()) - - # Match audio - matched = await match_audio(self.db, tags, limit) - - return [self._audio_to_response(audio, score) for audio, score in matched] - - def _scene_to_response( - self, scene: EchoSceneLibrary, score: float | None = None - ) -> SceneResponse: - """Convert scene model to response.""" - try: - keywords = json.loads(scene.keywords) - except (json.JSONDecodeError, TypeError): - keywords = [] - - return SceneResponse( - id=scene.id, - title=scene.title, - description=scene.description, - image_url=scene.image_url, - thumbnail_url=scene.thumbnail_url, - category=scene.category, - keywords=keywords, - match_score=score, - ) - - def _audio_to_response( - self, audio: EchoAudioLibrary, score: float | None = None - ) -> AudioResponse: - """Convert audio model to response.""" - try: - keywords = json.loads(audio.keywords) - except (json.JSONDecodeError, TypeError): - keywords = [] - - return AudioResponse( - id=audio.id, - title=audio.title, - description=audio.description, - audio_url=audio.audio_url, - duration_seconds=audio.duration_seconds, - audio_type=audio.audio_type, - keywords=keywords, - match_score=score, - ) - - # ============================================================ - # Meditation - # ============================================================ - - async def start_meditation( - self, - user_id: str, - target_duration_minutes: int, - scene_id: str | None = None, - audio_id: str | None = None, - ) -> MeditationStartResponse: - """Start a new meditation session.""" - # Validate scene - scene = None - if scene_id: - result = await self.db.execute( - select(EchoSceneLibrary).where(EchoSceneLibrary.id == scene_id) - ) - scene = result.scalar_one_or_none() - - # Validate audio - audio: EchoAudioLibrary | None = None - if audio_id: - audio_result = await self.db.execute( - select(EchoAudioLibrary).where(EchoAudioLibrary.id == audio_id) - ) - audio = audio_result.scalar_one_or_none() - - # Create session - session = EchoMeditationSession( - user_id=user_id, - target_duration_minutes=target_duration_minutes, - scene_id=scene_id if scene else None, - audio_id=audio_id if audio else None, - ) - self.db.add(session) - await self.db.flush() - - return MeditationStartResponse( - session_id=session.id, - target_duration_minutes=target_duration_minutes, - scene=self._scene_to_response(scene) if scene else None, - audio=self._audio_to_response(audio) if audio else None, - breathing_rhythm={ - phase.value: secs for phase, secs in BREATHING_RHYTHM.items() - }, - ) - - async def end_meditation( - self, - session_id: str, - user_id: str, - actual_duration_seconds: int, - ) -> MeditationEndResponse: - """End a meditation session.""" - result = await self.db.execute( - select(EchoMeditationSession).where( - EchoMeditationSession.id == session_id, - EchoMeditationSession.user_id == user_id, - ) - ) - session = result.scalar_one_or_none() - - if not session: - raise ValueError("会话不存在") - - if session.ended_at: - raise ValueError("会话已结束") - - # Update session - target_seconds = session.target_duration_minutes * 60 - completed = actual_duration_seconds >= target_seconds * 0.8 # 80% completion - - session.actual_duration_seconds = actual_duration_seconds - session.completed = completed - session.ended_at = datetime.utcnow() - - await self.db.flush() - - return MeditationEndResponse( - session_id=session.id, - completed=completed, - actual_duration_seconds=actual_duration_seconds, - target_duration_minutes=session.target_duration_minutes, - completion_rate=min(actual_duration_seconds / target_seconds, 1.0), - ) - - async def get_meditation_stats(self, user_id: str) -> MeditationStatsResponse: - """Get meditation statistics for a user.""" - # Total sessions - total_result = await self.db.execute( - select(func.count(EchoMeditationSession.id)).where( - EchoMeditationSession.user_id == user_id - ) - ) - total_sessions = total_result.scalar() or 0 - - # Completed sessions - completed_result = await self.db.execute( - select(func.count(EchoMeditationSession.id)).where( - EchoMeditationSession.user_id == user_id, - EchoMeditationSession.completed.is_(True), - ) - ) - completed_sessions = completed_result.scalar() or 0 - - # Total minutes - minutes_result = await self.db.execute( - select(func.sum(EchoMeditationSession.actual_duration_seconds)).where( - EchoMeditationSession.user_id == user_id, - EchoMeditationSession.completed.is_(True), - ) - ) - total_seconds = minutes_result.scalar() or 0 - total_minutes = total_seconds // 60 - - # Average duration - avg_duration = ( - total_minutes / completed_sessions if completed_sessions > 0 else 0 - ) - - # Calculate streaks - sessions_result = await self.db.execute( - select(EchoMeditationSession) - .where( - EchoMeditationSession.user_id == user_id, - EchoMeditationSession.completed.is_(True), - ) - .order_by(EchoMeditationSession.started_at.desc()) - ) - sessions = list(sessions_result.scalars().all()) - - current_streak = 0 - longest_streak = 0 - last_session_date = None - - if sessions: - last_session_date = sessions[0].started_at - - # Calculate streaks by unique dates - session_dates = sorted( - set(s.started_at.date() for s in sessions), reverse=True - ) - - if session_dates: - today = date.today() - # Check current streak - streak = 0 - expected_date = today - - for session_date in session_dates: - if session_date == expected_date: - streak += 1 - expected_date = expected_date - timedelta(days=1) - elif session_date == expected_date - timedelta(days=1): - # Allow for yesterday to count as current streak - streak += 1 - expected_date = session_date - timedelta(days=1) - else: - break - - current_streak = streak - - # Calculate longest streak - temp_streak = 1 - for i in range(1, len(session_dates)): - if session_dates[i] == session_dates[i - 1] - timedelta(days=1): - temp_streak += 1 - else: - longest_streak = max(longest_streak, temp_streak) - temp_streak = 1 - longest_streak = max(longest_streak, temp_streak) - - return MeditationStatsResponse( - total_sessions=total_sessions, - completed_sessions=completed_sessions, - total_minutes=total_minutes, - average_duration_minutes=round(avg_duration, 1), - current_streak=current_streak, - longest_streak=longest_streak, - last_session_date=last_session_date, - ) - - # ============================================================ - # Window Clarity (Partner Mode) - # ============================================================ - - async def get_window_clarity(self, user_id: str) -> WindowClarityResponse: - """Get window clarity status for partner.""" - binding = await self._get_binding_for_user(user_id) - - if not binding: - raise ValueError("未找到绑定关系") - - if binding.partner_id != user_id: - raise ValueError("只有伴侣可以查看窗户清晰度") - - # Get today's tasks - today = date.today() - tasks_result = await self.db.execute( - select(PartnerDailyTask).where( - PartnerDailyTask.binding_id == binding.id, - PartnerDailyTask.date == today, - ) - ) - tasks = list(tasks_result.scalars().all()) - - tasks_completed = sum( - 1 for t in tasks if t.status in [TaskStatus.COMPLETED, TaskStatus.CONFIRMED] - ) - tasks_confirmed = sum(1 for t in tasks if t.status == TaskStatus.CONFIRMED) - - # Get progress for streak and level - progress_result = await self.db.execute( - select(PartnerProgress).where(PartnerProgress.binding_id == binding.id) - ) - progress = progress_result.scalar_one_or_none() - - streak_days = progress.current_streak if progress else 0 - partner_level = ( - progress.current_level.value if progress else PartnerLevel.INTERN.value - ) - - # Calculate clarity - clarity_level, breakdown = calculate_clarity( - tasks_confirmed=tasks_confirmed, - tasks_completed=tasks_completed, - streak_days=streak_days, - partner_level=partner_level, - ) - - # Update cached clarity - await self._update_clarity_cache(binding.id, clarity_level) - - return WindowClarityResponse( - clarity_level=clarity_level, - tasks_completed_today=tasks_completed, - tasks_confirmed_today=tasks_confirmed, - streak_bonus=breakdown["streak_bonus"], - level_bonus=breakdown["level_bonus"], - breakdown=breakdown, - ) - - async def _update_clarity_cache(self, binding_id: str, clarity_level: int) -> None: - """Update cached clarity level.""" - result = await self.db.execute( - select(EchoWindowClarity).where(EchoWindowClarity.binding_id == binding_id) - ) - clarity = result.scalar_one_or_none() - - if clarity: - clarity.clarity_level = clarity_level - clarity.last_calculated_at = datetime.utcnow() - else: - clarity = EchoWindowClarity( - binding_id=binding_id, - clarity_level=clarity_level, - ) - self.db.add(clarity) - - await self.db.flush() - - # ============================================================ - # Partner Memories - # ============================================================ - - async def inject_memory( - self, - user_id: str, - title: str, - content: str, - image_url: str | None = None, - reveal_at_clarity: int = 50, - ) -> PartnerMemoryResponse: - """Partner injects a memory for mom to discover.""" - binding = await self._get_binding_for_user(user_id) - - if not binding: - raise ValueError("未找到绑定关系") - - if binding.partner_id != user_id: - raise ValueError("只有伴侣可以注入记忆") - - memory = EchoPartnerMemory( - binding_id=binding.id, - title=title, - content=content, - image_url=image_url, - reveal_at_clarity=reveal_at_clarity, - ) - self.db.add(memory) - await self.db.flush() - - return self._memory_to_response(memory) - - async def get_revealed_memories(self, user_id: str) -> RevealedMemoriesResponse: - """Get revealed memories for mom.""" - binding = await self._get_binding_for_user(user_id) - - if not binding: - raise ValueError("未找到绑定关系") - - if binding.mom_id != user_id: - raise ValueError("只有妈妈可以查看已揭示的记忆") - - # Get current clarity - clarity_result = await self.db.execute( - select(EchoWindowClarity).where(EchoWindowClarity.binding_id == binding.id) - ) - clarity = clarity_result.scalar_one_or_none() - current_clarity = clarity.clarity_level if clarity else 0 - - # Get all memories - memories_result = await self.db.execute( - select(EchoPartnerMemory) - .where(EchoPartnerMemory.binding_id == binding.id) - .order_by(EchoPartnerMemory.reveal_at_clarity.asc()) - ) - all_memories = list(memories_result.scalars().all()) - - # Check and reveal memories based on clarity - revealed_memories = [] - next_memory_at = None - - for memory in all_memories: - if memory.reveal_at_clarity <= current_clarity: - if not memory.is_revealed: - memory.is_revealed = True - memory.revealed_at = datetime.utcnow() - revealed_memories.append(self._memory_to_response(memory)) - elif next_memory_at is None: - next_memory_at = memory.reveal_at_clarity - - await self.db.flush() - - return RevealedMemoriesResponse( - memories=revealed_memories, - current_clarity=current_clarity, - next_memory_at=next_memory_at, - ) - - def _memory_to_response(self, memory: EchoPartnerMemory) -> PartnerMemoryResponse: - """Convert memory model to response.""" - return PartnerMemoryResponse( - id=memory.id, - title=memory.title, - content=memory.content, - image_url=memory.image_url, - reveal_at_clarity=memory.reveal_at_clarity, - is_revealed=memory.is_revealed, - revealed_at=memory.revealed_at, - created_at=memory.created_at, - ) - - # ============================================================ - # Youth Memoirs - # ============================================================ - - async def get_memoirs( - self, user_id: str, limit: int = 10, offset: int = 0 - ) -> MemoirListResponse: - """Get user's youth memoirs.""" - # Count total - count_result = await self.db.execute( - select(func.count(EchoYouthMemoir.id)).where( - EchoYouthMemoir.user_id == user_id - ) - ) - total = count_result.scalar() or 0 - - # Get memoirs - result = await self.db.execute( - select(EchoYouthMemoir) - .where(EchoYouthMemoir.user_id == user_id) - .order_by(EchoYouthMemoir.created_at.desc()) - .limit(limit) - .offset(offset) - ) - memoirs = list(result.scalars().all()) - - return MemoirListResponse( - memoirs=[self._memoir_to_response(m) for m in memoirs], - total=total, - ) - - async def generate_memoir( - self, user_id: str, theme: str | None = None - ) -> MemoirResponse: - """Generate a youth memoir based on identity tags.""" - # Get user's tags - result = await self.db.execute( - select(EchoIdentityTag).where(EchoIdentityTag.user_id == user_id) - ) - tags = list(result.scalars().all()) - - if not tags: - raise ValueError("请先添加身份标签再生成回忆录") - - # Build generation prompt - tag_contents = [f"{t.tag_type.value}: {t.content}" for t in tags] - tag_ids = [t.id for t in tags] - - # For now, generate a simple memoir (can be enhanced with LLM later) - title = self._generate_memoir_title(tags, theme) - content = self._generate_memoir_content(tags, theme) - - memoir = EchoYouthMemoir( - user_id=user_id, - title=title, - content=content, - generation_prompt=f"Theme: {theme}\nTags: {', '.join(tag_contents)}", - tags_used=json.dumps(tag_ids), - ) - self.db.add(memoir) - await self.db.flush() - - return self._memoir_to_response(memoir) - - def _generate_memoir_title( - self, tags: list[EchoIdentityTag], theme: str | None - ) -> str: - """Generate a memoir title based on tags.""" - if theme: - return f"关于{theme}的青春回忆" - - # Find a memory tag if available - memory_tags = [t for t in tags if t.tag_type == TagType.MEMORY] - if memory_tags: - return f"那年的{memory_tags[0].content}" - - music_tags = [t for t in tags if t.tag_type == TagType.MUSIC] - if music_tags: - return f"在{music_tags[0].content}的旋律中" - - return "青春的回声" - - def _generate_memoir_content( - self, tags: list[EchoIdentityTag], theme: str | None - ) -> str: - """Generate memoir content based on tags.""" - # Simple template-based generation - # Can be enhanced with LLM integration later - music_tags = [t.content for t in tags if t.tag_type == TagType.MUSIC] - sound_tags = [t.content for t in tags if t.tag_type == TagType.SOUND] - lit_tags = [t.content for t in tags if t.tag_type == TagType.LITERATURE] - memory_tags = [t.content for t in tags if t.tag_type == TagType.MEMORY] - - paragraphs = [] - - if memory_tags: - paragraphs.append( - f"回忆起那些关于{memory_tags[0]}的日子,心中涌起一阵温暖。" - f"那时的自己,年轻而充满梦想。" - ) - - if music_tags: - paragraphs.append( - f"耳边似乎还回响着{music_tags[0]}的旋律," - f"每一个音符都承载着那个年代的情感和记忆。" - ) - - if sound_tags: - paragraphs.append( - f"闭上眼睛,仿佛能听到{sound_tags[0]}的声音,那是属于青春的独特背景音。" - ) - - if lit_tags: - paragraphs.append( - f"那时候最爱读的是{lit_tags[0]},书中的故事仿佛就是自己的人生写照。" - ) - - paragraphs.append( - "时光流转,那些美好的记忆依然鲜活。" - "现在的我,虽然角色变了,但内心深处," - "那个热爱生活的自己从未离开。" - ) - - return "\n\n".join(paragraphs) - - async def rate_memoir( - self, memoir_id: str, user_id: str, rating: float - ) -> MemoirResponse: - """Rate a memoir.""" - result = await self.db.execute( - select(EchoYouthMemoir).where( - EchoYouthMemoir.id == memoir_id, - EchoYouthMemoir.user_id == user_id, - ) - ) - memoir = result.scalar_one_or_none() - - if not memoir: - raise ValueError("回忆录不存在") - - memoir.user_rating = rating - await self.db.flush() - - return self._memoir_to_response(memoir) - - def _memoir_to_response(self, memoir: EchoYouthMemoir) -> MemoirResponse: - """Convert memoir model to response.""" - return MemoirResponse( - id=memoir.id, - title=memoir.title, - content=memoir.content, - cover_image_url=memoir.cover_image_url, - user_rating=memoir.user_rating, - created_at=memoir.created_at, - ) - - # ============================================================ - # Helpers - # ============================================================ - - async def _get_binding_for_user(self, user_id: str) -> PartnerBinding | None: - """Get active binding for a user.""" - result = await self.db.execute( - select(PartnerBinding).where( - PartnerBinding.status == BindingStatus.ACTIVE, - (PartnerBinding.mom_id == user_id) - | (PartnerBinding.partner_id == user_id), - ) - ) - return result.scalar_one_or_none() diff --git a/backend/app/services/guardian/__init__.py b/backend/app/services/guardian/__init__.py deleted file mode 100644 index c4537447..00000000 --- a/backend/app/services/guardian/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Guardian Partner service for supporting partners of postpartum mothers.""" - -from .router import router as guardian_router - -__all__ = ["guardian_router"] diff --git a/backend/app/services/guardian/enums.py b/backend/app/services/guardian/enums.py deleted file mode 100644 index 45d190a2..00000000 --- a/backend/app/services/guardian/enums.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Guardian module enums.""" - -from enum import Enum - - -class BindingStatus(str, Enum): - """Partner binding status.""" - - PENDING = "pending" # Invitation sent, waiting for acceptance - ACTIVE = "active" # Binding active - UNBOUND = "unbound" # Previously bound, now unbound - - -class MoodLevel(str, Enum): - """Mood level enumeration.""" - - VERY_LOW = "very_low" # 1 - Very low - LOW = "low" # 2 - Low - NEUTRAL = "neutral" # 3 - Neutral - GOOD = "good" # 4 - Good - GREAT = "great" # 5 - Great - - -class HealthCondition(str, Enum): - """Health condition tags.""" - - WOUND_PAIN = "wound_pain" # 伤口疼痛 - HAIR_LOSS = "hair_loss" # 脱发期 - INSOMNIA = "insomnia" # 失眠 - BREAST_PAIN = "breast_pain" # 涨奶/乳房疼痛 - BACK_PAIN = "back_pain" # 腰背痛 - FATIGUE = "fatigue" # 疲惫 - EMOTIONAL = "emotional" # 情绪波动 - CONSTIPATION = "constipation" # 便秘 - SWEATING = "sweating" # 盗汗 - - -class TaskDifficulty(str, Enum): - """Task difficulty level.""" - - EASY = "easy" # 简易型 - 10分 - MEDIUM = "medium" # 进阶型 - 30分 - HARD = "hard" # 挑战型 - 50分 - - -class TaskStatus(str, Enum): - """Task completion status.""" - - AVAILABLE = "available" # 可选 - COMPLETED = "completed" # 伴侣已完成 - CONFIRMED = "confirmed" # 妈妈已确认 - EXPIRED = "expired" # 已过期 - - -class PartnerLevel(str, Enum): - """Partner level progression.""" - - INTERN = "intern" # 实习爸爸 (0-99分) - TRAINEE = "trainee" # 见习守护者 (100-299分) - REGULAR = "regular" # 正式守护者 (300-599分) - GOLD = "gold" # 金牌守护者 (600+分) - - -# Level thresholds -LEVEL_THRESHOLDS = { - PartnerLevel.INTERN: 0, - PartnerLevel.TRAINEE: 100, - PartnerLevel.REGULAR: 300, - PartnerLevel.GOLD: 600, -} - -# Task points by difficulty -TASK_POINTS = { - TaskDifficulty.EASY: 10, - TaskDifficulty.MEDIUM: 30, - TaskDifficulty.HARD: 50, -} diff --git a/backend/app/services/guardian/models.py b/backend/app/services/guardian/models.py deleted file mode 100644 index 1ab100c0..00000000 --- a/backend/app/services/guardian/models.py +++ /dev/null @@ -1,304 +0,0 @@ -"""Guardian module SQLAlchemy models.""" - -from datetime import date, datetime -from uuid import uuid4 - -from sqlalchemy import ( - Boolean, - Date, - DateTime, - ForeignKey, - Integer, - String, - Text, - UniqueConstraint, -) -from sqlalchemy import Enum as SAEnum -from sqlalchemy.orm import Mapped, mapped_column, relationship - -from app.core.database import Base - -from .enums import ( - BindingStatus, - MoodLevel, - PartnerLevel, - TaskDifficulty, - TaskStatus, -) - - -def generate_uuid() -> str: - """Generate a UUID string.""" - return str(uuid4()) - - -def generate_invite_code() -> str: - """Generate a short invite code.""" - import secrets - - return secrets.token_urlsafe(8) - - -# ============================================================ -# Partner Binding -# ============================================================ - - -class PartnerBinding(Base): - """Partner binding relationship between mom and partner.""" - - __tablename__ = "partner_bindings" - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - - # Mom and Partner IDs (reference users table) - mom_id: Mapped[str] = mapped_column( - String(36), ForeignKey("users.id", ondelete="CASCADE"), index=True - ) - partner_id: Mapped[str | None] = mapped_column( - String(36), - ForeignKey("users.id", ondelete="SET NULL"), - nullable=True, - index=True, - ) - - # Invitation - invite_code: Mapped[str] = mapped_column( - String(20), unique=True, index=True, default=generate_invite_code - ) - invite_expires_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - - # Status - status: Mapped[BindingStatus] = mapped_column( - SAEnum(BindingStatus), default=BindingStatus.PENDING - ) - - # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - bound_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - unbound_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - - # Relationships - mom: Mapped["User"] = relationship("User", foreign_keys=[mom_id]) - partner: Mapped["User | None"] = relationship("User", foreign_keys=[partner_id]) - - -# ============================================================ -# Mom Daily Status -# ============================================================ - - -class MomDailyStatus(Base): - """Mom's daily status record.""" - - __tablename__ = "mom_daily_status" - __table_args__ = (UniqueConstraint("mom_id", "date", name="uq_mom_daily_status"),) - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - mom_id: Mapped[str] = mapped_column( - String(36), ForeignKey("users.id", ondelete="CASCADE"), index=True - ) - - # Date (only one record per day) - date: Mapped[datetime] = mapped_column(Date, index=True) - - # Status data - mood: Mapped[MoodLevel] = mapped_column( - SAEnum(MoodLevel), default=MoodLevel.NEUTRAL - ) - energy_level: Mapped[int] = mapped_column(Integer, default=50) # 0-100 - health_conditions: Mapped[str | None] = mapped_column( - Text, nullable=True - ) # JSON array - feeding_count: Mapped[int] = mapped_column( - Integer, default=0 - ) # Night feeding count - sleep_hours: Mapped[float | None] = mapped_column(Integer, nullable=True) - - # Notes - notes: Mapped[str | None] = mapped_column(Text, nullable=True) - - # Partner notification - notified_partner: Mapped[bool] = mapped_column(Boolean, default=False) - notified_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - - # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - updated_at: Mapped[datetime] = mapped_column( - DateTime, default=datetime.utcnow, onupdate=datetime.utcnow - ) - - # Relationships - mom: Mapped["User"] = relationship("User") - - -# ============================================================ -# Partner Tasks -# ============================================================ - - -class TaskTemplate(Base): - """Predefined task templates.""" - - __tablename__ = "task_templates" - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - - # Task info - title: Mapped[str] = mapped_column(String(100)) - description: Mapped[str] = mapped_column(Text) - difficulty: Mapped[TaskDifficulty] = mapped_column(SAEnum(TaskDifficulty)) - points: Mapped[int] = mapped_column(Integer) - - # Categorization - category: Mapped[str | None] = mapped_column(String(50), nullable=True) - tags: Mapped[str | None] = mapped_column(Text, nullable=True) # JSON array - - # Status - is_active: Mapped[bool] = mapped_column(Boolean, default=True) - - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - - -class PartnerDailyTask(Base): - """Daily tasks assigned to a partner.""" - - __tablename__ = "partner_daily_tasks" - __table_args__ = ( - UniqueConstraint( - "binding_id", "date", "template_id", name="uq_partner_daily_task" - ), - ) - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - binding_id: Mapped[str] = mapped_column( - String(36), ForeignKey("partner_bindings.id", ondelete="CASCADE"), index=True - ) - template_id: Mapped[str] = mapped_column( - String(36), ForeignKey("task_templates.id", ondelete="CASCADE") - ) - - # Date - date: Mapped[datetime] = mapped_column(Date, index=True) - - # Status - status: Mapped[TaskStatus] = mapped_column( - SAEnum(TaskStatus), default=TaskStatus.AVAILABLE - ) - - # Completion - completed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - confirmed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - mom_feedback: Mapped[str | None] = mapped_column( - String(20), nullable=True - ) # emoji or text - - # Points awarded (may differ from template if bonus applied) - points_awarded: Mapped[int | None] = mapped_column(Integer, nullable=True) - - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - - # Relationships - binding: Mapped["PartnerBinding"] = relationship("PartnerBinding") - template: Mapped["TaskTemplate"] = relationship("TaskTemplate") - - -# ============================================================ -# Partner Progress -# ============================================================ - - -class PartnerProgress(Base): - """Partner's progress tracking.""" - - __tablename__ = "partner_progress" - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - binding_id: Mapped[str] = mapped_column( - String(36), ForeignKey("partner_bindings.id", ondelete="CASCADE"), unique=True - ) - - # Points and level - total_points: Mapped[int] = mapped_column(Integer, default=0) - current_level: Mapped[PartnerLevel] = mapped_column( - SAEnum(PartnerLevel), default=PartnerLevel.INTERN - ) - - # Stats - tasks_completed: Mapped[int] = mapped_column(Integer, default=0) - tasks_confirmed: Mapped[int] = mapped_column(Integer, default=0) - current_streak: Mapped[int] = mapped_column(Integer, default=0) - longest_streak: Mapped[int] = mapped_column(Integer, default=0) - last_task_date: Mapped[date | None] = mapped_column(Date, nullable=True) - - # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - updated_at: Mapped[datetime] = mapped_column( - DateTime, default=datetime.utcnow, onupdate=datetime.utcnow - ) - - # Relationships - binding: Mapped["PartnerBinding"] = relationship("PartnerBinding") - - -class PartnerBadge(Base): - """Badge awarded to partner.""" - - __tablename__ = "partner_badges" - __table_args__ = ( - UniqueConstraint("binding_id", "badge_type", name="uq_partner_badge"), - ) - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - binding_id: Mapped[str] = mapped_column( - String(36), ForeignKey("partner_bindings.id", ondelete="CASCADE"), index=True - ) - - # Badge info - badge_type: Mapped[str] = mapped_column(String(50), index=True) - badge_name: Mapped[str] = mapped_column(String(100)) - badge_icon: Mapped[str] = mapped_column(String(20)) # Emoji - description: Mapped[str | None] = mapped_column(Text, nullable=True) - - # Awarded - awarded_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - - # Relationships - binding: Mapped["PartnerBinding"] = relationship("PartnerBinding") - - -# ============================================================ -# Memory (Time Recorder) -# ============================================================ - - -class Memory(Base): - """Memory photo and note for time recorder.""" - - __tablename__ = "memories" - - id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) - binding_id: Mapped[str] = mapped_column( - String(36), ForeignKey("partner_bindings.id", ondelete="CASCADE"), index=True - ) - - # Content - photo_url: Mapped[str] = mapped_column(String(500)) - caption: Mapped[str | None] = mapped_column(Text, nullable=True) - - # Date - date: Mapped[datetime] = mapped_column(Date, index=True) - - # Baby milestone (for album generation) - milestone: Mapped[str | None] = mapped_column( - String(50), nullable=True - ) # e.g., "满月", "百天" - - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - - # Relationships - binding: Mapped["PartnerBinding"] = relationship("PartnerBinding") - - -# Import User for relationship type hints -from app.services.community.models import User # noqa: E402, F401 diff --git a/backend/app/services/guardian/router.py b/backend/app/services/guardian/router.py deleted file mode 100644 index c3cdf7c8..00000000 --- a/backend/app/services/guardian/router.py +++ /dev/null @@ -1,383 +0,0 @@ -"""Guardian Partner API router.""" - -from datetime import date -from typing import Annotated, Any - -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.ext.asyncio import AsyncSession - -from app.core.database import get_db -from app.services.auth.dependencies import CurrentUserJWT - -from .schemas import ( - AlbumResponse, - BadgeResponse, - BindingResponse, - BindRequest, - DailyStatusCreate, - DailyStatusResponse, - DailyTaskResponse, - InviteResponse, - MemoryCreate, - MemoryResponse, - ProgressResponse, - StatusNotification, - TaskCompleteRequest, - TaskConfirmRequest, -) -from .service import GuardianService - -router = APIRouter(prefix="/guardian", tags=["guardian"]) - - -# ============================================================ -# Partner Binding -# ============================================================ - - -@router.post("/invite", response_model=InviteResponse) -async def create_invite( - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], -) -> InviteResponse: - """Generate an invitation link for partner binding. (Mom only)""" - service = GuardianService(db) - try: - result = await service.create_invite(current_user.id) - await db.commit() - return result - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) from None - - -@router.post("/bind", response_model=BindingResponse) -async def accept_bind( - request: BindRequest, - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], -) -> BindingResponse: - """Accept an invitation and bind as partner.""" - service = GuardianService(db) - try: - binding = await service.accept_invite(request.invite_code, current_user.id) - await db.commit() - return BindingResponse( - id=binding.id, - mom_id=binding.mom_id, - partner_id=binding.partner_id, - status=binding.status, - created_at=binding.created_at, - bound_at=binding.bound_at, - ) - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) from None - - -@router.delete("/unbind") -async def unbind( - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], -) -> dict[str, Any]: - """Unbind partner relationship.""" - service = GuardianService(db) - binding = await service.get_binding_for_user(current_user.id) - if not binding: - raise HTTPException(status_code=404, detail="未找到绑定关系") - - try: - await service.unbind(binding.id, current_user.id) - await db.commit() - return {"message": "解绑成功"} - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) from None - - -@router.get("/status") -async def get_binding_status( - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], -) -> dict[str, Any]: - """Get current binding status with partner/mom info.""" - service = GuardianService(db) - binding = await service.get_binding_for_user(current_user.id) - - if not binding: - return { - "has_binding": False, - "role": None, - "binding": None, - "partner_info": None, - "mom_info": None, - } - - # Determine if current user is mom or partner - is_mom = binding.mom_id == current_user.id - - result: dict[str, Any] = { - "has_binding": True, - "role": "mom" if is_mom else "partner", - "binding": BindingResponse( - id=binding.id, - mom_id=binding.mom_id, - partner_id=binding.partner_id, - status=binding.status, - created_at=binding.created_at, - bound_at=binding.bound_at, - ), - "partner_info": None, - "mom_info": None, - } - - if is_mom: - result["partner_info"] = await service.get_partner_info(binding) - else: - result["mom_info"] = await service.get_mom_info(binding) - - return result - - -# ============================================================ -# Daily Status (Mom records, Partner views) -# ============================================================ - - -@router.post("/daily-status", response_model=DailyStatusResponse) -async def record_daily_status( - data: DailyStatusCreate, - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], -) -> DailyStatusResponse: - """Record mom's daily status. (Mom only)""" - service = GuardianService(db) - status = await service.record_daily_status(current_user.id, data) - await db.commit() - - return DailyStatusResponse( - id=status.id, - date=status.date, - mood=status.mood, - energy_level=status.energy_level, - health_conditions=( - __import__("json").loads(status.health_conditions) - if status.health_conditions - else [] - ), - feeding_count=status.feeding_count, - sleep_hours=status.sleep_hours, - notes=status.notes, - created_at=status.created_at, - updated_at=status.updated_at, - ) - - -@router.get("/daily-status", response_model=StatusNotification | None) -async def get_daily_status( - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], - target_date: date | None = Query( # noqa: B008 - None, description="Date to query (default: today)" - ), -) -> StatusNotification | None: - """Get mom's daily status with notification message. (Partner only)""" - service = GuardianService(db) - binding = await service.get_binding_for_user(current_user.id) - - if not binding: - raise HTTPException(status_code=404, detail="未找到绑定关系") - - # Partner viewing mom's status - if binding.partner_id == current_user.id: - status = await service.get_daily_status(binding.mom_id, target_date) - if not status: - return None - return service.generate_status_notification(status) - - # Mom viewing own status - status = await service.get_daily_status(current_user.id, target_date) - if not status: - return None - return service.generate_status_notification(status) - - -# ============================================================ -# Tasks (Partner completes, Mom confirms) -# ============================================================ - - -@router.get("/tasks", response_model=list[DailyTaskResponse]) -async def get_daily_tasks( - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], -) -> list[DailyTaskResponse]: - """Get today's tasks for partner.""" - service = GuardianService(db) - binding = await service.get_binding_for_user(current_user.id) - - if not binding: - raise HTTPException(status_code=404, detail="未找到绑定关系") - - # Only partner should get tasks, but mom can also view - tasks = await service.get_or_generate_daily_tasks(binding.id) - await db.commit() # Commit in case new tasks were generated - return tasks - - -@router.post("/tasks/{task_id}/complete", response_model=DailyTaskResponse) -async def complete_task( - task_id: str, - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], - request: TaskCompleteRequest | None = None, -) -> DailyTaskResponse: - """Mark a task as completed. (Partner only)""" - service = GuardianService(db) - try: - task = await service.complete_task(task_id, current_user.id) - await db.commit() - return service._task_to_response(task) - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) from None - - -@router.post("/tasks/{task_id}/confirm") -async def confirm_task( - task_id: str, - request: TaskConfirmRequest, - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], -) -> dict[str, Any]: - """Confirm task completion and award points. (Mom only)""" - service = GuardianService(db) - try: - task, points = await service.confirm_task( - task_id, current_user.id, request.feedback - ) - await db.commit() - return { - "task": service._task_to_response(task), - "points_awarded": points, - "message": f"已奖励 {points} 积分", - } - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) from None - - -@router.post("/tasks/{task_id}/reject", response_model=DailyTaskResponse) -async def reject_task( - task_id: str, - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], -) -> DailyTaskResponse: - """Reject task - reset to available. (Mom only)""" - service = GuardianService(db) - try: - task = await service.reject_task(task_id, current_user.id) - await db.commit() - return service._task_to_response(task) - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) from None - - -# ============================================================ -# Progress & Badges (Partner views) -# ============================================================ - - -@router.get("/progress", response_model=ProgressResponse) -async def get_progress( - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], -) -> ProgressResponse: - """Get partner's progress and level info.""" - service = GuardianService(db) - binding = await service.get_binding_for_user(current_user.id) - - if not binding: - raise HTTPException(status_code=404, detail="未找到绑定关系") - - return await service.get_progress(binding.id) - - -@router.get("/badges", response_model=list[BadgeResponse]) -async def get_badges( - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], -) -> list[BadgeResponse]: - """Get partner's badges.""" - service = GuardianService(db) - binding = await service.get_binding_for_user(current_user.id) - - if not binding: - raise HTTPException(status_code=404, detail="未找到绑定关系") - - return await service.get_badges(binding.id) - - -# ============================================================ -# Memories (Time Recorder) -# ============================================================ - - -@router.post("/memories", response_model=MemoryResponse) -async def add_memory( - data: MemoryCreate, - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], -) -> MemoryResponse: - """Add a memory photo. (Partner only)""" - service = GuardianService(db) - binding = await service.get_binding_for_user(current_user.id) - - if not binding: - raise HTTPException(status_code=404, detail="未找到绑定关系") - - # Only partner can add memories - if binding.partner_id != current_user.id: - raise HTTPException(status_code=403, detail="只有伴侣可以添加时光记录") - - memory = await service.add_memory(binding.id, data) - await db.commit() - - return MemoryResponse( - id=memory.id, - photo_url=memory.photo_url, - caption=memory.caption, - date=memory.date, - milestone=memory.milestone, - created_at=memory.created_at, - ) - - -@router.get("/memories", response_model=list[MemoryResponse]) -async def get_memories( - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], - limit: int = Query(30, ge=1, le=100), # noqa: B008 - offset: int = Query(0, ge=0), # noqa: B008 -) -> list[MemoryResponse]: - """Get memories for the binding.""" - service = GuardianService(db) - binding = await service.get_binding_for_user(current_user.id) - - if not binding: - raise HTTPException(status_code=404, detail="未找到绑定关系") - - return await service.get_memories(binding.id, limit, offset) - - -@router.get("/memories/album", response_model=AlbumResponse) -async def generate_album( - current_user: CurrentUserJWT, - db: Annotated[AsyncSession, Depends(get_db)], - milestone: str | None = Query( # noqa: B008 - None, description="Filter by milestone (e.g., '满月', '百天')" - ), -) -> AlbumResponse: - """Generate a memory album.""" - service = GuardianService(db) - binding = await service.get_binding_for_user(current_user.id) - - if not binding: - raise HTTPException(status_code=404, detail="未找到绑定关系") - - return await service.generate_album(binding.id, milestone) diff --git a/backend/app/services/guardian/schemas.py b/backend/app/services/guardian/schemas.py deleted file mode 100644 index 0edf2891..00000000 --- a/backend/app/services/guardian/schemas.py +++ /dev/null @@ -1,275 +0,0 @@ -"""Guardian module Pydantic schemas.""" - -from datetime import date, datetime - -from pydantic import BaseModel, Field - -from .enums import ( - BindingStatus, - HealthCondition, - MoodLevel, - PartnerLevel, - TaskDifficulty, - TaskStatus, -) - -# ============================================================ -# Partner Binding Schemas -# ============================================================ - - -class InviteResponse(BaseModel): - """Response containing invite information.""" - - invite_code: str - invite_url: str - expires_at: datetime | None - - -class BindRequest(BaseModel): - """Request to bind as partner.""" - - invite_code: str - - -class BindingResponse(BaseModel): - """Response containing binding information.""" - - id: str - mom_id: str - partner_id: str | None - status: BindingStatus - created_at: datetime - bound_at: datetime | None - - class Config: - from_attributes = True - - -class PartnerInfo(BaseModel): - """Partner information for display.""" - - id: str - nickname: str - avatar_url: str | None - level: PartnerLevel - total_points: int - current_streak: int - - -class MomInfo(BaseModel): - """Mom information for partner view.""" - - id: str - nickname: str - avatar_url: str | None - baby_birth_date: datetime | None - postpartum_weeks: int | None - - -# ============================================================ -# Daily Status Schemas -# ============================================================ - - -class DailyStatusCreate(BaseModel): - """Request to create/update daily status.""" - - mood: MoodLevel = MoodLevel.NEUTRAL - energy_level: int = Field(default=50, ge=0, le=100) - health_conditions: list[HealthCondition] = [] - feeding_count: int = Field(default=0, ge=0) - sleep_hours: float | None = Field(default=None, ge=0, le=24) - notes: str | None = None - - -class DailyStatusResponse(BaseModel): - """Response containing daily status.""" - - id: str - status_date: date = Field(alias="date") - mood: MoodLevel - energy_level: int - health_conditions: list[str] - feeding_count: int - sleep_hours: float | None - notes: str | None - created_at: datetime - updated_at: datetime - - class Config: - from_attributes = True - populate_by_name = True - - -class StatusNotification(BaseModel): - """Notification to partner about mom's status.""" - - status: DailyStatusResponse - message: str - suggestions: list[str] - - -# ============================================================ -# Task Schemas -# ============================================================ - - -class TaskTemplateResponse(BaseModel): - """Task template information.""" - - id: str - title: str - description: str - difficulty: TaskDifficulty - points: int - category: str | None - - class Config: - from_attributes = True - - -class DailyTaskResponse(BaseModel): - """Daily task for partner.""" - - id: str - template: TaskTemplateResponse - task_date: date = Field(alias="date") - status: TaskStatus - completed_at: datetime | None - confirmed_at: datetime | None - mom_feedback: str | None - points_awarded: int | None - - class Config: - from_attributes = True - populate_by_name = True - - -class TaskCompleteRequest(BaseModel): - """Request to mark task as completed.""" - - notes: str | None = None - - -class TaskConfirmRequest(BaseModel): - """Request from mom to confirm task completion.""" - - feedback: str = Field(..., max_length=20) # Emoji or short text - - -# ============================================================ -# Progress Schemas -# ============================================================ - - -class ProgressResponse(BaseModel): - """Partner progress information.""" - - total_points: int - current_level: PartnerLevel - next_level: PartnerLevel | None - points_to_next_level: int | None - tasks_completed: int - tasks_confirmed: int - current_streak: int - longest_streak: int - - class Config: - from_attributes = True - - -class BadgeResponse(BaseModel): - """Partner badge information.""" - - id: str - badge_type: str - badge_name: str - badge_icon: str - description: str | None - awarded_at: datetime - - class Config: - from_attributes = True - - -class LevelUpNotification(BaseModel): - """Notification when partner levels up.""" - - old_level: PartnerLevel - new_level: PartnerLevel - message: str - - -# ============================================================ -# Memory Schemas -# ============================================================ - - -class MemoryCreate(BaseModel): - """Request to create a memory.""" - - photo_url: str - caption: str | None = None - memory_date: date | None = Field(default=None, alias="date") - milestone: str | None = None - - class Config: - populate_by_name = True - - -class MemoryResponse(BaseModel): - """Memory information.""" - - id: str - photo_url: str - caption: str | None - memory_date: date = Field(alias="date") - milestone: str | None - created_at: datetime - - class Config: - from_attributes = True - populate_by_name = True - - -class AlbumResponse(BaseModel): - """Generated album/memory book.""" - - title: str - subtitle: str - cover_photo_url: str | None - memories: list[MemoryResponse] - total_days: int - milestones: list[str] - - -# ============================================================ -# WebSocket Schemas -# ============================================================ - - -class WSMessage(BaseModel): - """WebSocket message.""" - - type: str - data: dict - - -class StatusUpdateMessage(BaseModel): - """Status update notification for partner.""" - - type: str = "status_update" - status: DailyStatusResponse - message: str - suggestions: list[str] - - -class TaskConfirmationMessage(BaseModel): - """Task confirmation notification for partner.""" - - type: str = "task_confirmed" - task_id: str - feedback: str - points_awarded: int - new_total_points: int diff --git a/backend/app/services/guardian/seed_data.py b/backend/app/services/guardian/seed_data.py deleted file mode 100644 index 8ab30ff3..00000000 --- a/backend/app/services/guardian/seed_data.py +++ /dev/null @@ -1,121 +0,0 @@ -"""Seed data for Guardian Partner feature.""" - -from typing import cast - -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession - -from .enums import TASK_POINTS, TaskDifficulty -from .models import TaskTemplate - -# Default task templates -DEFAULT_TASKS = [ - # Easy tasks (10 points) - { - "title": "给太太倒一杯50度的温水", - "description": "用温度计或手感测试,确保水温适合饮用。产后妈妈需要多喝温水。", - "difficulty": TaskDifficulty.EASY, - "category": "日常关怀", - }, - { - "title": "帮太太准备一份营养加餐", - "description": "可以是水果、坚果、酸奶等健康小食,注意避开回奶食物。", - "difficulty": TaskDifficulty.EASY, - "category": "日常关怀", - }, - { - "title": "主动清洗宝宝的奶瓶和餐具", - "description": "将所有奶瓶、奶嘴清洗干净并消毒,减轻太太的家务负担。", - "difficulty": TaskDifficulty.EASY, - "category": "家务分担", - }, - { - "title": "给太太一个拥抱并说'辛苦了'", - "description": "真诚地拥抱太太,告诉她今天辛苦了。情感支持是最好的礼物。", - "difficulty": TaskDifficulty.EASY, - "category": "情感支持", - }, - { - "title": "帮宝宝换一次尿布", - "description": "独立完成一次换尿布,注意清洁和涂抹护臀霜。", - "difficulty": TaskDifficulty.EASY, - "category": "育儿参与", - }, - # Medium tasks (30 points) - { - "title": "学习并操作一套5分钟肩颈按摩", - "description": "观看教学视频学习基础肩颈按摩手法,帮太太缓解肩颈疲劳。", - "difficulty": TaskDifficulty.MEDIUM, - "category": "身体关怀", - }, - { - "title": "准备一顿月子餐", - "description": "搜索月子餐食谱,准备一顿营养均衡的正餐。注意清淡、高蛋白。", - "difficulty": TaskDifficulty.MEDIUM, - "category": "日常关怀", - }, - { - "title": "陪太太散步20分钟", - "description": "产后适量运动有助于恢复,陪太太在小区或公园慢走散步。", - "difficulty": TaskDifficulty.MEDIUM, - "category": "身体关怀", - }, - { - "title": "独立给宝宝洗一次澡", - "description": "准备好温水和洗浴用品,独立完成宝宝洗澡全流程。", - "difficulty": TaskDifficulty.MEDIUM, - "category": "育儿参与", - }, - { - "title": "整理一次家务(扫地拖地+收拾客厅)", - "description": "完成基础家务清洁,保持家里整洁,让太太安心休息。", - "difficulty": TaskDifficulty.MEDIUM, - "category": "家务分担", - }, - # Hard tasks (50 points) - { - "title": "独立带娃2小时,让太太睡个午觉", - "description": "全程独立照看宝宝至少2小时,包括喂奶(如有存奶)、换尿布、哄睡。", - "difficulty": TaskDifficulty.HARD, - "category": "育儿参与", - }, - { - "title": "策划一个小惊喜(手写信或小礼物)", - "description": "手写一封信或准备一个小礼物,表达你的爱意和感谢。", - "difficulty": TaskDifficulty.HARD, - "category": "情感支持", - }, - { - "title": "完成一次深度家务(厨房/卫生间大扫除)", - "description": "选择厨房或卫生间进行一次彻底清洁,包括擦洗台面、整理收纳。", - "difficulty": TaskDifficulty.HARD, - "category": "家务分担", - }, - { - "title": "学习一项育儿新技能并实践", - "description": "学习抚触按摩、辅食制作、婴儿游泳等新技能,并当天实践一次。", - "difficulty": TaskDifficulty.HARD, - "category": "育儿参与", - }, -] - - -async def seed_task_templates(db: AsyncSession) -> None: - """Seed default task templates if they don't exist.""" - result = await db.execute(select(TaskTemplate).limit(1)) - if result.scalar_one_or_none(): - return # Already seeded - - for task_data in DEFAULT_TASKS: - difficulty = cast(TaskDifficulty, task_data["difficulty"]) - template = TaskTemplate( - title=task_data["title"], - description=task_data["description"], - difficulty=difficulty, - points=TASK_POINTS[difficulty], - category=task_data["category"], - ) - db.add(template) - - await db.commit() - print(f"[Startup] Seeded {len(DEFAULT_TASKS)} task templates") diff --git a/backend/app/services/guardian/service.py b/backend/app/services/guardian/service.py deleted file mode 100644 index 38ba5317..00000000 --- a/backend/app/services/guardian/service.py +++ /dev/null @@ -1,702 +0,0 @@ -"""Guardian Partner service layer.""" - -import json -import random -from datetime import date, datetime, timedelta - -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.orm import selectinload - -from app.services.community.models import User - -from .enums import ( - LEVEL_THRESHOLDS, - TASK_POINTS, - BindingStatus, - HealthCondition, - MoodLevel, - PartnerLevel, - TaskDifficulty, - TaskStatus, -) -from .models import ( - Memory, - MomDailyStatus, - PartnerBadge, - PartnerBinding, - PartnerDailyTask, - PartnerProgress, - TaskTemplate, -) -from .schemas import ( - AlbumResponse, - BadgeResponse, - DailyStatusCreate, - DailyStatusResponse, - DailyTaskResponse, - InviteResponse, - MemoryCreate, - MemoryResponse, - MomInfo, - PartnerInfo, - ProgressResponse, - StatusNotification, -) - - -class GuardianService: - """Service for Guardian Partner feature.""" - - def __init__(self, db: AsyncSession): - self.db = db - - # ============================================================ - # Partner Binding - # ============================================================ - - async def create_invite( - self, mom_id: str, expires_in_hours: int = 48 - ) -> InviteResponse: - """Create an invitation for partner binding.""" - # Check if there's already an active binding - existing = await self.db.execute( - select(PartnerBinding).where( - PartnerBinding.mom_id == mom_id, - PartnerBinding.status == BindingStatus.ACTIVE, - ) - ) - if existing.scalar_one_or_none(): - raise ValueError("已有绑定的伴侣") - - # Check for pending invite - pending = await self.db.execute( - select(PartnerBinding).where( - PartnerBinding.mom_id == mom_id, - PartnerBinding.status == BindingStatus.PENDING, - ) - ) - binding = pending.scalar_one_or_none() - - if binding: - # Update existing invite - from .models import generate_invite_code - - binding.invite_code = generate_invite_code() - binding.invite_expires_at = datetime.utcnow() + timedelta( - hours=expires_in_hours - ) - else: - # Create new binding - binding = PartnerBinding( - mom_id=mom_id, - invite_expires_at=datetime.utcnow() + timedelta(hours=expires_in_hours), - ) - self.db.add(binding) - - await self.db.flush() - - return InviteResponse( - invite_code=binding.invite_code, - invite_url=f"/guardian/invite/{binding.invite_code}", - expires_at=binding.invite_expires_at, - ) - - async def accept_invite(self, invite_code: str, partner_id: str) -> PartnerBinding: - """Accept an invitation and bind as partner.""" - # Find the binding - result = await self.db.execute( - select(PartnerBinding).where( - PartnerBinding.invite_code == invite_code, - PartnerBinding.status == BindingStatus.PENDING, - ) - ) - binding = result.scalar_one_or_none() - - if not binding: - raise ValueError("邀请码无效或已过期") - - # Check expiration - if binding.invite_expires_at and binding.invite_expires_at < datetime.utcnow(): - raise ValueError("邀请码已过期") - - # Check if partner is the same as mom - if binding.mom_id == partner_id: - raise ValueError("不能绑定自己") - - # Update binding - binding.partner_id = partner_id - binding.status = BindingStatus.ACTIVE - binding.bound_at = datetime.utcnow() - - # Create progress record - progress = PartnerProgress(binding_id=binding.id) - self.db.add(progress) - - await self.db.flush() - return binding - - async def unbind(self, binding_id: str, user_id: str) -> None: - """Unbind partner relationship.""" - result = await self.db.execute( - select(PartnerBinding).where( - PartnerBinding.id == binding_id, - PartnerBinding.status == BindingStatus.ACTIVE, - ) - ) - binding = result.scalar_one_or_none() - - if not binding: - raise ValueError("绑定不存在") - - # Check permission (mom or partner can unbind) - if binding.mom_id != user_id and binding.partner_id != user_id: - raise ValueError("无权解绑") - - binding.status = BindingStatus.UNBOUND - binding.unbound_at = datetime.utcnow() - await self.db.flush() - - async def get_binding_for_user(self, user_id: str) -> PartnerBinding | None: - """Get active binding for a user (as mom or partner).""" - result = await self.db.execute( - select(PartnerBinding) - .where( - PartnerBinding.status == BindingStatus.ACTIVE, - (PartnerBinding.mom_id == user_id) - | (PartnerBinding.partner_id == user_id), - ) - .options( - selectinload(PartnerBinding.mom), selectinload(PartnerBinding.partner) - ) - ) - return result.scalar_one_or_none() - - async def get_partner_info(self, binding: PartnerBinding) -> PartnerInfo | None: - """Get partner info for display.""" - if not binding.partner_id: - return None - - result = await self.db.execute( - select(User).where(User.id == binding.partner_id) - ) - partner = result.scalar_one_or_none() - if not partner: - return None - - # Get progress - progress_result = await self.db.execute( - select(PartnerProgress).where(PartnerProgress.binding_id == binding.id) - ) - progress = progress_result.scalar_one_or_none() - - return PartnerInfo( - id=partner.id, - nickname=partner.nickname, - avatar_url=partner.avatar_url, - level=progress.current_level if progress else PartnerLevel.INTERN, - total_points=progress.total_points if progress else 0, - current_streak=progress.current_streak if progress else 0, - ) - - async def get_mom_info(self, binding: PartnerBinding) -> MomInfo: - """Get mom info for partner view.""" - result = await self.db.execute(select(User).where(User.id == binding.mom_id)) - mom = result.scalar_one_or_none() - if not mom: - raise ValueError("妈妈信息不存在") - - return MomInfo( - id=mom.id, - nickname=mom.nickname, - avatar_url=mom.avatar_url, - baby_birth_date=mom.baby_birth_date, - postpartum_weeks=mom.postpartum_weeks, - ) - - # ============================================================ - # Daily Status - # ============================================================ - - async def record_daily_status( - self, mom_id: str, data: DailyStatusCreate - ) -> MomDailyStatus: - """Record or update mom's daily status.""" - today = date.today() - - # Check for existing record - result = await self.db.execute( - select(MomDailyStatus).where( - MomDailyStatus.mom_id == mom_id, - MomDailyStatus.date == today, - ) - ) - status = result.scalar_one_or_none() - - health_conditions_json = json.dumps([c.value for c in data.health_conditions]) - - if status: - # Update existing - status.mood = data.mood - status.energy_level = data.energy_level - status.health_conditions = health_conditions_json - status.feeding_count = data.feeding_count - status.sleep_hours = data.sleep_hours - status.notes = data.notes - status.notified_partner = False # Reset notification flag - else: - # Create new - status = MomDailyStatus( - mom_id=mom_id, - date=today, - mood=data.mood, - energy_level=data.energy_level, - health_conditions=health_conditions_json, - feeding_count=data.feeding_count, - sleep_hours=data.sleep_hours, - notes=data.notes, - ) - self.db.add(status) - - await self.db.flush() - return status - - async def get_daily_status( - self, mom_id: str, target_date: date | None = None - ) -> MomDailyStatus | None: - """Get mom's daily status.""" - target_date = target_date or date.today() - result = await self.db.execute( - select(MomDailyStatus).where( - MomDailyStatus.mom_id == mom_id, - MomDailyStatus.date == target_date, - ) - ) - return result.scalar_one_or_none() - - def generate_status_notification( - self, status: MomDailyStatus - ) -> StatusNotification: - """Generate notification message for partner based on mom's status.""" - conditions = ( - json.loads(status.health_conditions) if status.health_conditions else [] - ) - suggestions = [] - messages = [] - - # Analyze mood and energy - if status.mood in [MoodLevel.VERY_LOW, MoodLevel.LOW]: - messages.append("太太今天心情不太好") - if status.energy_level < 30: - messages.append(f"能量值只有{status.energy_level}%") - - # Analyze health conditions - condition_messages = { - HealthCondition.WOUND_PAIN.value: "伤口有些疼痛", - HealthCondition.HAIR_LOSS.value: "处于脱发期", - HealthCondition.INSOMNIA.value: "昨晚没睡好", - HealthCondition.BREAST_PAIN.value: "涨奶不适", - HealthCondition.BACK_PAIN.value: "腰背酸痛", - HealthCondition.FATIGUE.value: "感到疲惫", - HealthCondition.EMOTIONAL.value: "情绪有些波动", - } - for cond in conditions: - if cond in condition_messages: - messages.append(condition_messages[cond]) - - # Analyze feeding - if status.feeding_count >= 3: - messages.append(f"深夜喂奶{status.feeding_count}次") - - # Generate suggestions - suggestion_map = { - HealthCondition.WOUND_PAIN.value: "帮忙分担家务,让她多休息", - HealthCondition.HAIR_LOSS.value: "今天千万不要评论家里地板上的头发,请默默清理掉", - HealthCondition.INSOMNIA.value: "今晚早点帮忙带娃,让她补觉", - HealthCondition.BREAST_PAIN.value: "准备热毛巾帮她热敷", - HealthCondition.BACK_PAIN.value: "可以学习肩颈按摩帮她放松", - HealthCondition.FATIGUE.value: "今晚推掉社交,早点回家陪她", - HealthCondition.EMOTIONAL.value: "多陪陪她聊天,不要讲道理,只需要倾听", - } - for cond in conditions: - if cond in suggestion_map: - suggestions.append(suggestion_map[cond]) - - if status.energy_level < 30: - suggestions.append("带一份她爱吃的低糖甜品回家") - if status.feeding_count >= 3: - suggestions.append("今晚试着帮忙喂一次奶(如果有存奶)") - - # Build final message - if messages: - message = "太太今天:" + ",".join(messages) + "。" - else: - message = "太太今天状态还不错~" - - status_response = DailyStatusResponse( - id=status.id, - date=status.date, - mood=status.mood, - energy_level=status.energy_level, - health_conditions=conditions, - feeding_count=status.feeding_count, - sleep_hours=status.sleep_hours, - notes=status.notes, - created_at=status.created_at, - updated_at=status.updated_at, - ) - - return StatusNotification( - status=status_response, - message=message, - suggestions=suggestions or ["继续保持关心~"], - ) - - # ============================================================ - # Tasks - # ============================================================ - - async def get_or_generate_daily_tasks( - self, binding_id: str - ) -> list[DailyTaskResponse]: - """Get today's tasks or generate new ones.""" - today = date.today() - - # Check for existing tasks - result = await self.db.execute( - select(PartnerDailyTask) - .where( - PartnerDailyTask.binding_id == binding_id, - PartnerDailyTask.date == today, - ) - .options(selectinload(PartnerDailyTask.template)) - ) - existing_tasks = list(result.scalars().all()) - - if existing_tasks: - return [self._task_to_response(t) for t in existing_tasks] - - # Generate new tasks (1 easy, 1 medium, 1 hard) - tasks = [] - for difficulty in [ - TaskDifficulty.EASY, - TaskDifficulty.MEDIUM, - TaskDifficulty.HARD, - ]: - template_result = await self.db.execute( - select(TaskTemplate).where( - TaskTemplate.difficulty == difficulty, - TaskTemplate.is_active.is_(True), - ) - ) - templates = list(template_result.scalars().all()) - if templates: - template = random.choice(templates) - task = PartnerDailyTask( - binding_id=binding_id, - template_id=template.id, - date=today, - ) - self.db.add(task) - tasks.append((task, template)) - - await self.db.flush() - - # Refresh to get IDs - result_tasks = [] - for task, template in tasks: - task.template = template - result_tasks.append(self._task_to_response(task)) - - return result_tasks - - async def complete_task(self, task_id: str, partner_id: str) -> PartnerDailyTask: - """Mark a task as completed by partner.""" - result = await self.db.execute( - select(PartnerDailyTask) - .where(PartnerDailyTask.id == task_id) - .options( - selectinload(PartnerDailyTask.binding), - selectinload(PartnerDailyTask.template), - ) - ) - task = result.scalar_one_or_none() - - if not task: - raise ValueError("任务不存在") - if task.binding.partner_id != partner_id: - raise ValueError("无权操作此任务") - if task.status != TaskStatus.AVAILABLE: - raise ValueError("任务状态不允许完成") - - task.status = TaskStatus.COMPLETED - task.completed_at = datetime.utcnow() - await self.db.flush() - - return task - - async def reject_task(self, task_id: str, mom_id: str) -> PartnerDailyTask: - """Mom rejects task - reset to available status.""" - result = await self.db.execute( - select(PartnerDailyTask) - .where(PartnerDailyTask.id == task_id) - .options( - selectinload(PartnerDailyTask.binding), - selectinload(PartnerDailyTask.template), - ) - ) - task = result.scalar_one_or_none() - - if not task: - raise ValueError("任务不存在") - if task.binding.mom_id != mom_id: - raise ValueError("无权操作此任务") - if task.status != TaskStatus.COMPLETED: - raise ValueError("任务状态不是已完成") - - # Reset task to available - task.status = TaskStatus.AVAILABLE - task.completed_at = None - await self.db.flush() - - return task - - async def confirm_task( - self, task_id: str, mom_id: str, feedback: str - ) -> tuple[PartnerDailyTask, int]: - """Mom confirms task completion and awards points.""" - result = await self.db.execute( - select(PartnerDailyTask) - .where(PartnerDailyTask.id == task_id) - .options( - selectinload(PartnerDailyTask.binding), - selectinload(PartnerDailyTask.template), - ) - ) - task = result.scalar_one_or_none() - - if not task: - raise ValueError("任务不存在") - if task.binding.mom_id != mom_id: - raise ValueError("无权确认此任务") - if task.status != TaskStatus.COMPLETED: - raise ValueError("任务尚未完成") - - # Award points - points = TASK_POINTS.get(task.template.difficulty, 10) - - task.status = TaskStatus.CONFIRMED - task.confirmed_at = datetime.utcnow() - task.mom_feedback = feedback - task.points_awarded = points - - # Update progress - progress = await self._get_or_create_progress(task.binding.id) - progress.total_points += points - progress.tasks_completed += 1 - progress.tasks_confirmed += 1 - - # Update streak - today = date.today() - if progress.last_task_date: - if progress.last_task_date == today - timedelta(days=1): - progress.current_streak += 1 - elif progress.last_task_date != today: - progress.current_streak = 1 - else: - progress.current_streak = 1 - - progress.last_task_date = today - if progress.current_streak > progress.longest_streak: - progress.longest_streak = progress.current_streak - - # Check for level up - new_level = self._calculate_level(progress.total_points) - if new_level != progress.current_level: - progress.current_level = new_level - # Could award badge for level up here - - await self.db.flush() - - return task, points - - def _task_to_response(self, task: PartnerDailyTask) -> DailyTaskResponse: - """Convert task model to response.""" - from .schemas import TaskTemplateResponse - - return DailyTaskResponse( - id=task.id, - template=TaskTemplateResponse( - id=task.template.id, - title=task.template.title, - description=task.template.description, - difficulty=task.template.difficulty, - points=task.template.points, - category=task.template.category, - ), - date=task.date, - status=task.status, - completed_at=task.completed_at, - confirmed_at=task.confirmed_at, - mom_feedback=task.mom_feedback, - points_awarded=task.points_awarded, - ) - - # ============================================================ - # Progress - # ============================================================ - - async def get_progress(self, binding_id: str) -> ProgressResponse: - """Get partner's progress.""" - progress = await self._get_or_create_progress(binding_id) - - next_level = None - points_to_next = None - - # Calculate next level - levels = list(PartnerLevel) - current_idx = levels.index(progress.current_level) - if current_idx < len(levels) - 1: - next_level = levels[current_idx + 1] - points_to_next = LEVEL_THRESHOLDS[next_level] - progress.total_points - - return ProgressResponse( - total_points=progress.total_points, - current_level=progress.current_level, - next_level=next_level, - points_to_next_level=max(0, points_to_next) if points_to_next else None, - tasks_completed=progress.tasks_completed, - tasks_confirmed=progress.tasks_confirmed, - current_streak=progress.current_streak, - longest_streak=progress.longest_streak, - ) - - async def get_badges(self, binding_id: str) -> list[BadgeResponse]: - """Get partner's badges.""" - result = await self.db.execute( - select(PartnerBadge).where(PartnerBadge.binding_id == binding_id) - ) - badges = result.scalars().all() - return [ - BadgeResponse( - id=b.id, - badge_type=b.badge_type, - badge_name=b.badge_name, - badge_icon=b.badge_icon, - description=b.description, - awarded_at=b.awarded_at, - ) - for b in badges - ] - - async def _get_or_create_progress(self, binding_id: str) -> PartnerProgress: - """Get or create progress record.""" - result = await self.db.execute( - select(PartnerProgress).where(PartnerProgress.binding_id == binding_id) - ) - progress = result.scalar_one_or_none() - - if not progress: - progress = PartnerProgress(binding_id=binding_id) - self.db.add(progress) - await self.db.flush() - - return progress - - def _calculate_level(self, points: int) -> PartnerLevel: - """Calculate level based on points.""" - for level in reversed(list(PartnerLevel)): - if points >= LEVEL_THRESHOLDS[level]: - return level - return PartnerLevel.INTERN - - # ============================================================ - # Memories - # ============================================================ - - async def add_memory(self, binding_id: str, data: MemoryCreate) -> Memory: - """Add a memory photo.""" - memory = Memory( - binding_id=binding_id, - photo_url=data.photo_url, - caption=data.caption, - date=data.memory_date or date.today(), - milestone=data.milestone, - ) - self.db.add(memory) - await self.db.flush() - return memory - - async def get_memories( - self, binding_id: str, limit: int = 30, offset: int = 0 - ) -> list[MemoryResponse]: - """Get memories for a binding.""" - result = await self.db.execute( - select(Memory) - .where(Memory.binding_id == binding_id) - .order_by(Memory.date.desc()) - .limit(limit) - .offset(offset) - ) - memories = result.scalars().all() - return [ - MemoryResponse( - id=m.id, - photo_url=m.photo_url, - caption=m.caption, - date=m.date, - milestone=m.milestone, - created_at=m.created_at, - ) - for m in memories - ] - - async def generate_album( - self, binding_id: str, milestone: str | None = None - ) -> AlbumResponse: - """Generate an album/memory book.""" - # Get memories - query = select(Memory).where(Memory.binding_id == binding_id) - if milestone: - query = query.where(Memory.milestone == milestone) - query = query.order_by(Memory.date.asc()) - - result = await self.db.execute(query) - memories = result.scalars().all() - - memory_responses = [ - MemoryResponse( - id=m.id, - photo_url=m.photo_url, - caption=m.caption, - date=m.date, - milestone=m.milestone, - created_at=m.created_at, - ) - for m in memories - ] - - # Calculate stats - total_days = len(set(m.date for m in memories)) - milestones = list(set(m.milestone for m in memories if m.milestone)) - - # Generate title - if milestone: - title = f"宝宝{milestone}回忆录" - else: - title = "我们的时光记录" - - return AlbumResponse( - title=title, - subtitle="每一天,都是爱的见证", - cover_photo_url=memory_responses[0].photo_url if memory_responses else None, - memories=memory_responses, - total_days=total_days, - milestones=milestones, - ) - - -# Dependency injection helper -async def get_guardian_service(db: AsyncSession) -> GuardianService: - """Get guardian service instance.""" - return GuardianService(db) diff --git a/backend/app/services/verification.py b/backend/app/services/verification.py deleted file mode 100644 index 06f27660..00000000 --- a/backend/app/services/verification.py +++ /dev/null @@ -1,169 +0,0 @@ -"""Chain-of-Verification (CoVe) service for reducing AI hallucinations. - -This service implements an optimized CoVe approach in a single LLM call: -1. Extract factual claims from response -2. Verify claims against search results -3. Generate corrected response if needed - -Reference: https://arxiv.org/abs/2309.11495 -""" - -import json -import logging -from typing import Any - -from openai import OpenAI - -from app.core.config import get_settings - -logger = logging.getLogger(__name__) - -# Combined prompt for verification and correction in ONE call -VERIFY_AND_CORRECT_PROMPT = """你是一个事实核查助手。请分析以下AI回复,验证其中的事实性陈述,并在必要时进行修正。 - -## AI回复 -{response} - -## 参考资料(来自网络搜索) -{context} - -## 任务 -1. 识别回复中的事实性陈述(不包括情感表达) -2. 对照参考资料验证这些陈述 -3. 如果发现错误或无法验证的事实,生成修正后的回复 - -## 返回格式(JSON) -{{ - "has_factual_claims": true/false, - "claims_found": ["陈述1", "陈述2"], - "verification": {{ - "verified": ["已验证的陈述"], - "unverified": ["无法验证的陈述"], - "incorrect": ["错误的陈述"] - }}, - "needs_correction": true/false, - "corrected_response": "修正后的回复(保持温暖语气,100-200字)或null" -}} - -只返回JSON,不要其他内容。""" - - -class ChainOfVerification: - """Chain-of-Verification service for fact-checking LLM responses.""" - - def __init__(self) -> None: - settings = get_settings() - self._api_key = settings.modelscope_key - self._base_url = settings.modelscope_base_url - self._model = settings.modelscope_model - self._client: OpenAI | None = None - - @property - def client(self) -> OpenAI: - """Lazy load OpenAI client.""" - if self._client is None: - if not self._api_key: - raise ValueError("MODELSCOPE_KEY not configured") - self._client = OpenAI( - api_key=self._api_key, - base_url=self._base_url, - ) - return self._client - - def verify_and_correct( - self, - response: str, - search_context: str | None, - ) -> tuple[str, dict[str, Any]]: - """ - Verify a response and correct if needed in ONE LLM call. - - Args: - response: The initial LLM response - search_context: The web search context used for generation - - Returns: - Tuple of (corrected_response, verification_metadata) - """ - # If no search context, skip verification - if not search_context: - return response, {"skipped": True, "reason": "no_search_context"} - - try: - result = self.client.chat.completions.create( - model=self._model, - messages=[ - { - "role": "user", - "content": VERIFY_AND_CORRECT_PROMPT.format( - response=response, - context=search_context, - ), - } - ], - temperature=0.3, - max_tokens=1024, - ) - content = result.choices[0].message.content or "{}" - - # Parse JSON response - parsed = json.loads(content.strip()) - - # Check if there are factual claims - if not parsed.get("has_factual_claims", False): - return response, {"skipped": True, "reason": "no_claims"} - - claims = parsed.get("claims_found", []) - verification = parsed.get("verification", {}) - - logger.info( - f"CoVe: {len(verification.get('verified', []))} verified, " - f"{len(verification.get('unverified', []))} unverified, " - f"{len(verification.get('incorrect', []))} incorrect" - ) - - # If correction is needed and provided, use it - if parsed.get("needs_correction") and parsed.get("corrected_response"): - return parsed["corrected_response"], { - "corrected": True, - "claims": claims, - "verification": verification, - } - - # If only unverified claims, add disclaimer - if ( - verification.get("unverified") - and not verification.get("verified") - and not verification.get("incorrect") - ): - disclaimer = "\n\n(以上信息仅供参考,建议咨询专业医生获取准确建议 💗)" - return response + disclaimer, { - "disclaimer_added": True, - "claims": claims, - "verification": verification, - } - - return response, { - "verified": True, - "claims": claims, - "verification": verification, - } - - except json.JSONDecodeError as e: - logger.warning(f"CoVe JSON parse failed: {e}") - return response, {"error": "json_parse_failed"} - except Exception as e: - logger.warning(f"CoVe failed: {e}") - return response, {"error": str(e)} - - -# Global service instance -_cove_service: ChainOfVerification | None = None - - -def get_cove_service() -> ChainOfVerification: - """Get the Chain-of-Verification service singleton.""" - global _cove_service - if _cove_service is None: - _cove_service = ChainOfVerification() - return _cove_service diff --git a/backend/app/services/web_search.py b/backend/app/services/web_search.py deleted file mode 100644 index 659c9a8a..00000000 --- a/backend/app/services/web_search.py +++ /dev/null @@ -1,336 +0,0 @@ -"""Web search service for grounding AI responses with real information. - -This service integrates with Firecrawl API to provide web search capabilities -for reducing AI hallucinations in medical/factual questions. -""" - -import logging -import re -from typing import Any - -import httpx - -from app.core.config import get_settings - -logger = logging.getLogger(__name__) - - -def analyze_search_intent(text: str) -> dict[str, Any]: - """ - Use AI to analyze user message and extract search parameters in ONE call. - - Returns dict with: - - needs_search: bool - whether web search is needed - - location: str | None - English location for Firecrawl - - time_filter: str | None - tbs parameter value - """ - from openai import OpenAI - - from app.core.config import get_settings - - try: - settings = get_settings() - if not settings.modelscope_key: - return {"needs_search": False, "location": None, "time_filter": None} - - client = OpenAI( - api_key=settings.modelscope_key, - base_url=settings.modelscope_base_url, - ) - - response = client.chat.completions.create( - model=settings.modelscope_model, - messages=[ - { - "role": "user", - "content": f"""分析用户消息,返回JSON格式结果。 - -## 判断规则 - -needs_search = true 的情况: -- 询问具体地点、机构、服务(如月子中心、医院) -- 询问最新信息、新闻、活动 -- 询问医学知识、症状、治疗方法 -- 询问"怎么办"、"如何"等需要专业知识的问题 - -needs_search = false 的情况: -- 情感倾诉(如"我好累") -- 日常闲聊、打招呼 - -location:如果提到中国地名,翻译成英文格式如 "Yangpu,Shanghai,China",否则为 null - -time_filter: -- 提到"今天" → "qdr:d" -- 提到"本周/最近几天" → "qdr:w" -- 提到"最近/近期/本月" → "qdr:m" -- 提到"最新" → "qdr:w" -- 否则为 null - -## 返回格式(只返回JSON,不要其他内容) -{{"needs_search": true/false, "location": "..." or null, "time_filter": "..." or null}} - -用户消息:{text}""", - } - ], - temperature=0, - max_tokens=100, - ) - result = response.choices[0].message.content or "{}" - - # Parse JSON response - import json - - try: - parsed = json.loads(result.strip()) - return { - "needs_search": parsed.get("needs_search", False), - "location": parsed.get("location"), - "time_filter": parsed.get("time_filter"), - } - except json.JSONDecodeError: - logger.warning(f"Failed to parse search intent JSON: {result}") - return {"needs_search": False, "location": None, "time_filter": None} - - except Exception as e: - logger.warning(f"AI analyze_search_intent failed: {e}") - return {"needs_search": False, "location": None, "time_filter": None} - - -def strip_markdown(text: str) -> str: - """ - Remove markdown formatting from text to prevent AI from mimicking markdown style. - - Strips: headers, bold, italic, links, images, code blocks, lists, etc. - """ - if not text: - return text - - # Remove code blocks (```...```) - text = re.sub(r"```[\s\S]*?```", "", text) - - # Remove inline code (`...`) - text = re.sub(r"`[^`]+`", "", text) - - # Remove images ![alt](url) - text = re.sub(r"!\[[^\]]*\]\([^)]+\)", "", text) - - # Remove links [text](url) -> keep text - text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text) - - # Remove headers (# ## ### etc) - text = re.sub(r"^#{1,6}\s+", "", text, flags=re.MULTILINE) - - # Remove bold **text** or __text__ - text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text) - text = re.sub(r"__([^_]+)__", r"\1", text) - - # Remove italic *text* or _text_ - text = re.sub(r"\*([^*]+)\*", r"\1", text) - text = re.sub(r"_([^_]+)_", r"\1", text) - - # Remove strikethrough ~~text~~ - text = re.sub(r"~~([^~]+)~~", r"\1", text) - - # Remove blockquotes (> at start of line) - text = re.sub(r"^>\s*", "", text, flags=re.MULTILINE) - - # Remove horizontal rules (---, ***, ___) - text = re.sub(r"^[-*_]{3,}\s*$", "", text, flags=re.MULTILINE) - - # Remove list markers (- * + and numbered lists) - text = re.sub(r"^\s*[-*+]\s+", "", text, flags=re.MULTILINE) - text = re.sub(r"^\s*\d+\.\s+", "", text, flags=re.MULTILINE) - - # Remove HTML tags - text = re.sub(r"<[^>]+>", "", text) - - # Clean up extra whitespace - text = re.sub(r"\n{3,}", "\n\n", text) - text = text.strip() - - return text - - -class WebSearchService: - """Service for performing web searches using Firecrawl API.""" - - def __init__(self) -> None: - settings = get_settings() - self._api_key = settings.firecrawl_api_key - self._base_url = "https://api.firecrawl.dev/v1" - self._client: httpx.AsyncClient | None = None - - @property - def is_configured(self) -> bool: - """Check if Firecrawl API is configured.""" - return bool(self._api_key) - - async def _get_client(self) -> httpx.AsyncClient: - """Get or create HTTP client.""" - if self._client is None: - self._client = httpx.AsyncClient(timeout=30.0) - return self._client - - async def search( - self, - query: str, - max_results: int = 3, - location: str | None = None, - tbs: str | None = None, - ) -> dict[str, Any]: - """ - Perform a web search using Firecrawl API. - - Args: - query: Search query string - max_results: Maximum number of results (1-10) - location: Geographic location for search (e.g., "Shanghai,China") - tbs: Time-based filter (e.g., "qdr:d" for past day) - - Returns: - dict with 'results' key containing search results - """ - if not self.is_configured: - logger.warning("Firecrawl API not configured, skipping web search") - return {"results": []} - - client = await self._get_client() - - # Build request payload - payload: dict[str, Any] = { - "query": query, - "limit": max_results, - "scrapeOptions": { - "formats": ["markdown"], - "onlyMainContent": True, # Exclude nav, footer, etc. - }, - "country": "CN", # Default to China for Chinese queries - } - - # Add optional location parameter - if location: - payload["location"] = location - logger.debug(f"Using location filter: {location}") - - # Add optional time-based filter - if tbs: - payload["tbs"] = tbs - logger.debug(f"Using time filter: {tbs}") - - try: - response = await client.post( - f"{self._base_url}/search", - headers={ - "Authorization": f"Bearer {self._api_key}", - "Content-Type": "application/json", - }, - json=payload, - ) - response.raise_for_status() - data = response.json() - - if not data.get("success"): - logger.error(f"Firecrawl search failed: {data}") - return {"results": []} - - # Filter out PDFs and extract only needed fields - results = [] - for r in data.get("data", []): - url = r.get("url", "") - # Skip PDF files - if url.lower().endswith(".pdf"): - continue - results.append( - { - "title": r.get("title", ""), - "url": url, - "content": strip_markdown( - (r.get("description") or r.get("markdown", ""))[:500] - ), - } - ) - if len(results) >= max_results: - break - - return {"results": results} - except httpx.HTTPStatusError as e: - logger.error( - f"Firecrawl API error: {e.response.status_code} - {e.response.text}" - ) - return {"results": []} - except Exception as e: - logger.error(f"Web search failed: {e}") - return {"results": []} - - async def search_for_context( - self, question: str - ) -> tuple[str, list[dict[str, str]]] | None: - """ - Search web and format results as context for LLM. - - Returns: - Tuple of (context_string, source_list) or None if no results. - source_list contains dicts with 'title' and 'url' keys. - """ - if not self.is_configured: - return None - - # Use ONE LLM call to analyze search intent, location, and time - intent = analyze_search_intent(question) - - if not intent["needs_search"]: - logger.debug( - f"Skipping search for non-factual question: {question[:50]}..." - ) - return None - - location = intent["location"] - tbs = intent["time_filter"] - - if location: - logger.info(f"Extracted location from query: {location}") - if tbs: - logger.info(f"Extracted time filter from query: {tbs}") - - # Use original question as search query (trimmed to reasonable length) - search_query = question[:100].strip() - - results = await self.search( - search_query, max_results=10, location=location, tbs=tbs - ) - - if not results["results"]: - return None - - # Format context for LLM - context_parts = ["【参考来源(来自网络搜索)】"] - sources = [] - for i, r in enumerate(results["results"], 1): - context_parts.append(f"{i}. {r['title']}") - if r["content"]: - content_preview = r["content"][:500].strip() - if len(r["content"]) > 500: - content_preview += "..." - context_parts.append(f" {content_preview}") - # Collect sources - sources.append({"title": r["title"], "url": r["url"]}) - - return "\n".join(context_parts), sources - - async def close(self) -> None: - """Close the HTTP client.""" - if self._client: - await self._client.aclose() - self._client = None - - -# Global service instance -_web_search_service: WebSearchService | None = None - - -def get_web_search_service() -> WebSearchService: - """Get the web search service singleton.""" - global _web_search_service - if _web_search_service is None: - _web_search_service = WebSearchService() - return _web_search_service diff --git a/backend/app/static/css/style.css b/backend/app/static/css/style.css deleted file mode 100644 index 1b62d9d8..00000000 --- a/backend/app/static/css/style.css +++ /dev/null @@ -1,896 +0,0 @@ -/* MomShell - Simplified Styles */ - -:root { - --primary: #d88a9f; - --primary-light: #f5e6e8; - --success: #5a9a6a; - --warning: #c9a055; - --danger: #c66; - --text: #333; - --text-light: #666; - --background: #faf9f7; - --card-bg: #fff; - --border: #e5e5e5; - --spacing-xs: 0.25rem; - --spacing-sm: 0.5rem; - --spacing-md: 1rem; - --spacing-lg: 1.5rem; - --spacing-xl: 2rem; - --radius: 6px; -} - -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans SC', sans-serif; - background: var(--background); - color: var(--text); - line-height: 1.6; - min-height: 100vh; -} - -.app-container { - max-width: 1200px; - margin: 0 auto; - min-height: 100vh; - display: flex; - flex-direction: column; -} - -/* Header */ -.header { - display: flex; - justify-content: space-between; - align-items: center; - padding: var(--spacing-md) var(--spacing-lg); - background: var(--card-bg); - border-bottom: 1px solid var(--border); -} - -.logo { - text-decoration: none; -} - -.logo-text { - font-size: 1.25rem; - font-weight: 600; - color: var(--primary); -} - -.nav { - display: flex; - gap: var(--spacing-xs); -} - -.nav-btn { - padding: var(--spacing-sm) var(--spacing-md); - border: none; - background: transparent; - color: var(--text-light); - font-size: 0.9rem; - cursor: pointer; - border-radius: var(--radius); -} - -.nav-btn:hover { - background: var(--primary-light); -} - -.nav-btn.active { - background: var(--primary); - color: #fff; -} - -/* Main Content */ -.main-content { - flex: 1; - padding: var(--spacing-lg); -} - -h2 { - margin-bottom: var(--spacing-lg); - font-weight: 600; - color: var(--text); -} - -/* Views */ -.view { - display: none; -} - -.view.active { - display: block; -} - -/* Home Page */ -.home-hero { - text-align: center; - padding: var(--spacing-xl) 0; -} - -.home-hero h1 { - font-size: 2.5rem; - font-weight: 600; - color: var(--primary); - margin-bottom: var(--spacing-sm); -} - -.home-subtitle { - color: var(--text-light); - font-size: 1.1rem; -} - -.feature-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); - gap: var(--spacing-lg); - max-width: 900px; - margin: var(--spacing-xl) auto; -} - -.feature-card { - display: block; - text-decoration: none; - background: var(--card-bg); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: var(--spacing-xl) var(--spacing-lg); - text-align: center; - color: var(--text); -} - -.feature-card:hover { - border-color: var(--primary); -} - -.feature-icon { - font-size: 2rem; - margin-bottom: var(--spacing-md); -} - -.feature-card h3 { - font-size: 1.1rem; - margin-bottom: var(--spacing-sm); -} - -.feature-card p { - color: var(--text-light); - font-size: 0.9rem; -} - -/* Chat / Companion */ -.chat-container { - max-width: 700px; - margin: 0 auto; - background: var(--card-bg); - border: 1px solid var(--border); - border-radius: var(--radius); - overflow: hidden; -} - -.chat-messages { - height: 400px; - overflow-y: auto; - padding: var(--spacing-lg); -} - -.chat-welcome { - text-align: center; - color: var(--text-light); - padding: var(--spacing-xl); -} - -.chat-welcome p { - margin-bottom: var(--spacing-sm); -} - -.chat-message { - margin-bottom: var(--spacing-md); - padding: var(--spacing-sm) var(--spacing-md); - border-radius: var(--radius); - max-width: 80%; -} - -.chat-message.user { - background: var(--primary); - color: #fff; - margin-left: auto; -} - -.chat-message.assistant { - background: var(--primary-light); -} - -.chat-input-area { - display: flex; - gap: var(--spacing-sm); - padding: var(--spacing-md); - border-top: 1px solid var(--border); -} - -.chat-input-area textarea { - flex: 1; - padding: var(--spacing-sm); - border: 1px solid var(--border); - border-radius: var(--radius); - resize: none; - font-family: inherit; - font-size: 0.95rem; -} - -.chat-input-area textarea:focus { - outline: none; - border-color: var(--primary); -} - -/* Community */ -.community-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: var(--spacing-lg); -} - -.community-header h2 { - margin-bottom: 0; -} - -.community-channels { - display: flex; - gap: var(--spacing-sm); - margin-bottom: var(--spacing-lg); -} - -.channel-btn { - padding: var(--spacing-sm) var(--spacing-md); - border: 1px solid var(--border); - background: var(--card-bg); - border-radius: var(--radius); - cursor: pointer; - font-size: 0.9rem; -} - -.channel-btn:hover { - border-color: var(--primary); -} - -.channel-btn.active { - background: var(--primary); - border-color: var(--primary); - color: #fff; -} - -.community-posts { - background: var(--card-bg); - border: 1px solid var(--border); - border-radius: var(--radius); - min-height: 300px; -} - -.empty-state { - text-align: center; - padding: var(--spacing-xl); - color: var(--text-light); -} - -.empty-state p { - margin-bottom: var(--spacing-md); -} - -.post-card { - padding: var(--spacing-lg); - border-bottom: 1px solid var(--border); -} - -.post-card:last-child { - border-bottom: none; -} - -.post-author { - display: flex; - align-items: center; - gap: var(--spacing-sm); - margin-bottom: var(--spacing-sm); - font-size: 0.9rem; -} - -.post-role { - background: var(--primary-light); - padding: 2px 6px; - border-radius: var(--radius); - font-size: 0.75rem; -} - -.post-content { - margin-bottom: var(--spacing-sm); -} - -.post-title { - font-size: 1rem; - font-weight: 500; - margin-bottom: var(--spacing-sm); -} - -.post-meta { - font-size: 0.85rem; - color: var(--text-light); - display: flex; - gap: var(--spacing-md); -} - -/* Buttons */ -.btn { - padding: var(--spacing-sm) var(--spacing-lg); - border: none; - border-radius: var(--radius); - font-size: 0.95rem; - cursor: pointer; - font-weight: 500; -} - -.btn-primary { - background: var(--primary); - color: #fff; -} - -.btn-primary:hover { - opacity: 0.9; -} - -.btn-secondary { - background: var(--primary-light); - color: var(--text); -} - -.btn-secondary:hover { - background: #ecd9db; -} - -.btn-danger { - background: var(--danger); - color: #fff; -} - -.btn-large { - padding: var(--spacing-md) var(--spacing-xl); - font-size: 1rem; -} - -.hidden { - display: none !important; -} - -/* Exercise Selection */ -.exercise-categories { - display: flex; - gap: var(--spacing-sm); - margin-bottom: var(--spacing-lg); - flex-wrap: wrap; -} - -.category-btn { - padding: var(--spacing-sm) var(--spacing-md); - border: 1px solid var(--border); - background: var(--card-bg); - border-radius: var(--radius); - cursor: pointer; - font-size: 0.9rem; -} - -.category-btn:hover { - border-color: var(--primary); -} - -.category-btn.active { - background: var(--primary); - border-color: var(--primary); - color: #fff; -} - -/* Exercise List */ -.exercise-list { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: var(--spacing-lg); -} - -.exercise-card { - background: var(--card-bg); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: var(--spacing-lg); - cursor: pointer; -} - -.exercise-card:hover { - border-color: var(--primary); -} - -.exercise-card-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: var(--spacing-sm); -} - -.exercise-card h3 { - font-size: 1rem; - font-weight: 600; -} - -.exercise-card .difficulty { - font-size: 0.75rem; - padding: 2px 6px; - border-radius: var(--radius); - background: var(--primary-light); -} - -.exercise-card .difficulty.beginner { - background: #d4edda; - color: #155724; -} - -.exercise-card .difficulty.intermediate { - background: #fff3cd; - color: #856404; -} - -.exercise-card .difficulty.advanced { - background: #f8d7da; - color: #721c24; -} - -.exercise-card p { - color: var(--text-light); - font-size: 0.9rem; - margin-bottom: var(--spacing-md); -} - -.exercise-card-footer { - display: flex; - justify-content: space-between; - font-size: 0.85rem; - color: var(--text-light); -} - -.exercise-card .category-tag { - background: var(--primary-light); - padding: 2px 6px; - border-radius: var(--radius); - font-size: 0.75rem; -} - -/* Progress View */ -.progress-summary { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: var(--spacing-lg); - margin-bottom: var(--spacing-xl); -} - -.stat-card { - background: var(--card-bg); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: var(--spacing-lg); - text-align: center; -} - -.stat-value { - display: block; - font-size: 2rem; - font-weight: 700; - color: var(--primary); -} - -.stat-label { - color: var(--text-light); - font-size: 0.9rem; -} - -/* Strength Metrics */ -.strength-metrics { - background: var(--card-bg); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: var(--spacing-lg); - margin-bottom: var(--spacing-lg); -} - -.strength-metrics h3 { - margin-bottom: var(--spacing-md); - font-size: 1rem; -} - -.metric-item { - margin-bottom: var(--spacing-md); -} - -.metric-header { - display: flex; - justify-content: space-between; - margin-bottom: var(--spacing-xs); - font-size: 0.9rem; -} - -.metric-bar { - height: 6px; - background: var(--border); - border-radius: 3px; - overflow: hidden; -} - -.metric-fill { - height: 100%; - background: var(--primary); - border-radius: 3px; -} - -/* Achievements */ -.achievements { - background: var(--card-bg); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: var(--spacing-lg); -} - -.achievements h3 { - margin-bottom: var(--spacing-md); - font-size: 1rem; -} - -.achievements-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); - gap: var(--spacing-md); -} - -.achievement-badge { - text-align: center; - padding: var(--spacing-md); - border-radius: var(--radius); - background: var(--primary-light); - opacity: 0.5; -} - -.achievement-badge.earned { - opacity: 1; - background: #fff3cd; -} - -.achievement-icon { - font-size: 1.5rem; - margin-bottom: var(--spacing-xs); -} - -.achievement-name { - font-size: 0.8rem; - font-weight: 500; -} - -/* Session View */ -.session-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: var(--spacing-lg); -} - -.session-content { - display: grid; - grid-template-columns: 2fr 1fr; - gap: var(--spacing-lg); -} - -/* Video Area */ -.video-area { - background: #000; - border-radius: var(--radius); - overflow: hidden; -} - -.video-container { - position: relative; - width: 100%; - padding-bottom: 75%; -} - -.video-container video, -.video-container canvas { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - object-fit: cover; -} - -.video-container canvas { - pointer-events: none; -} - -.video-overlay { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - pointer-events: none; - display: flex; - justify-content: space-between; - padding: var(--spacing-md); -} - -.phase-indicator { - background: rgba(0, 0, 0, 0.6); - color: #fff; - padding: var(--spacing-sm) var(--spacing-md); - border-radius: var(--radius); - font-size: 0.9rem; - height: fit-content; -} - -.score-display { - background: rgba(0, 0, 0, 0.6); - color: #fff; - padding: var(--spacing-sm) var(--spacing-md); - border-radius: var(--radius); - text-align: center; - height: fit-content; -} - -.score-value { - display: block; - font-size: 1.25rem; - font-weight: 700; -} - -.score-label { - font-size: 0.75rem; - opacity: 0.8; -} - -/* Info Panel */ -.info-panel { - display: flex; - flex-direction: column; - gap: var(--spacing-md); -} - -.session-progress { - background: var(--card-bg); - padding: var(--spacing-md); - border: 1px solid var(--border); - border-radius: var(--radius); -} - -.progress-bar { - height: 8px; - background: var(--border); - border-radius: 4px; - overflow: hidden; - margin-bottom: var(--spacing-sm); -} - -.progress-fill { - height: 100%; - background: var(--primary); - border-radius: 4px; - width: 0%; -} - -.progress-text { - display: flex; - justify-content: space-between; - font-size: 0.85rem; - color: var(--text-light); -} - -.current-phase { - background: var(--card-bg); - padding: var(--spacing-md); - border: 1px solid var(--border); - border-radius: var(--radius); -} - -.current-phase h4 { - margin-bottom: var(--spacing-sm); - color: var(--primary); - font-size: 0.95rem; -} - -.phase-cues { - list-style: none; - margin-top: var(--spacing-sm); -} - -.phase-cues li { - padding: var(--spacing-xs) 0; - color: var(--text-light); - font-size: 0.85rem; -} - -.phase-cues li::before { - content: "- "; - color: var(--success); -} - -.feedback-area { - background: var(--card-bg); - padding: var(--spacing-md); - border: 1px solid var(--border); - border-radius: var(--radius); - min-height: 60px; -} - -.feedback-message { - font-size: 0.95rem; - color: var(--text); -} - -.feedback-message.correction { - color: var(--warning); -} - -.feedback-message.warning { - color: var(--danger); -} - -.feedback-message.encouragement { - color: var(--success); -} - -.session-controls { - display: flex; - gap: var(--spacing-sm); - flex-wrap: wrap; -} - -/* Modal */ -.modal { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.4); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; -} - -.modal-content { - background: var(--card-bg); - padding: var(--spacing-xl); - border-radius: var(--radius); - text-align: center; - max-width: 450px; - width: 90%; -} - -.post-modal-content { - max-width: 550px; - text-align: left; -} - -.post-modal-content h2 { - text-align: center; - margin-bottom: var(--spacing-lg); -} - -.form-group { - margin-bottom: var(--spacing-md); -} - -.form-group label { - display: block; - margin-bottom: var(--spacing-xs); - font-weight: 500; - font-size: 0.9rem; -} - -.form-group input, -.form-group select, -.form-group textarea { - width: 100%; - padding: var(--spacing-sm); - border: 1px solid var(--border); - border-radius: var(--radius); - font-family: inherit; - font-size: 0.95rem; -} - -.form-group input:focus, -.form-group select:focus, -.form-group textarea:focus { - outline: none; - border-color: var(--primary); -} - -.form-group textarea { - resize: vertical; - min-height: 120px; -} - -.form-actions { - display: flex; - justify-content: flex-end; - gap: var(--spacing-sm); - margin-top: var(--spacing-lg); -} - -.modal-content h2 { - color: var(--primary); - margin-bottom: var(--spacing-lg); -} - -.session-summary { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: var(--spacing-md); - margin-bottom: var(--spacing-lg); -} - -.summary-stat { - padding: var(--spacing-md); - background: var(--primary-light); - border-radius: var(--radius); -} - -.summary-value { - display: block; - font-size: 1.5rem; - font-weight: 700; - color: var(--primary); -} - -.summary-label { - font-size: 0.8rem; - color: var(--text-light); -} - -.new-achievements { - margin-bottom: var(--spacing-lg); - padding: var(--spacing-md); - background: #fff3cd; - border-radius: var(--radius); -} - -.new-achievements h3 { - margin-bottom: var(--spacing-sm); - font-size: 0.95rem; -} - -/* Responsive */ -@media (max-width: 768px) { - .session-content { - grid-template-columns: 1fr; - } - - .progress-summary { - grid-template-columns: 1fr; - } - - .exercise-list { - grid-template-columns: 1fr; - } - - .header { - flex-direction: column; - gap: var(--spacing-md); - } - - .nav { - flex-wrap: wrap; - justify-content: center; - } - - .feature-grid { - grid-template-columns: 1fr; - } -} diff --git a/backend/app/static/js/app.js b/backend/app/static/js/app.js deleted file mode 100644 index c7b0041c..00000000 --- a/backend/app/static/js/app.js +++ /dev/null @@ -1,941 +0,0 @@ -/** - * MomShell - Frontend Application - */ - -// Configuration -const CONFIG = { - API_BASE: '/api', - COMPANION_API: '/api/v1/companion', - COMMUNITY_API: '/api/v1/community', - WS_BASE: `ws://${window.location.host}/api/ws/coach`, - FRAME_RATE: 8, - USER_ID: 'default_user', -}; - -// Application State -const state = { - currentView: 'home', - exercises: [], - selectedExercise: null, - sessionId: null, - ws: null, - videoStream: null, - isSessionActive: false, - frameInterval: null, - audioQueue: [], - isPlayingAudio: false, - currentAudio: null, - // Chat state - chatHistory: [], - // Community state - communityPosts: [], - currentChannel: 'all', -}; - -// ============================================================================ -// DOM Elements -// ============================================================================ - -const elements = { - // Views - homeView: document.getElementById('home-view'), - companionView: document.getElementById('companion-view'), - communityView: document.getElementById('community-view'), - exercisesView: document.getElementById('exercises-view'), - progressView: document.getElementById('progress-view'), - sessionView: document.getElementById('session-view'), - - // Exercise list - exerciseList: document.getElementById('exercise-list'), - - // Chat elements - chatMessages: document.getElementById('chat-messages'), - chatInput: document.getElementById('chat-input'), - chatSendBtn: document.getElementById('chat-send-btn'), - - // Community elements - communityPosts: document.getElementById('community-posts'), - postModal: document.getElementById('post-modal'), - postForm: document.getElementById('post-form'), - postTitle: document.getElementById('post-title'), - postContent: document.getElementById('post-content'), - postChannel: document.getElementById('post-channel'), - openPostModalBtn: document.getElementById('open-post-modal-btn'), - cancelPostBtn: document.getElementById('cancel-post-btn'), - - // Session elements - sessionExerciseName: document.getElementById('session-exercise-name'), - localVideo: document.getElementById('local-video'), - poseCanvas: document.getElementById('pose-canvas'), - phaseIndicator: document.getElementById('phase-indicator'), - scoreDisplay: document.getElementById('score-display'), - sessionProgressBar: document.getElementById('session-progress-bar'), - currentSet: document.getElementById('current-set'), - totalSets: document.getElementById('total-sets'), - currentRep: document.getElementById('current-rep'), - totalReps: document.getElementById('total-reps'), - phaseDescription: document.getElementById('phase-description'), - phaseCues: document.getElementById('phase-cues'), - feedbackMessage: document.getElementById('feedback-message'), - - // Buttons - startBtn: document.getElementById('start-btn'), - pauseBtn: document.getElementById('pause-btn'), - resumeBtn: document.getElementById('resume-btn'), - endSessionBtn: document.getElementById('end-session-btn'), - - // Modal - sessionCompleteModal: document.getElementById('session-complete-modal'), - summaryScore: document.getElementById('summary-score'), - summaryReps: document.getElementById('summary-reps'), - summaryDuration: document.getElementById('summary-duration'), - newAchievements: document.getElementById('new-achievements'), - achievementsEarned: document.getElementById('achievements-earned'), - closeModalBtn: document.getElementById('close-modal-btn'), - - // Progress view - totalSessions: document.getElementById('total-sessions'), - currentStreak: document.getElementById('current-streak'), - totalMinutes: document.getElementById('total-minutes'), - metricsContainer: document.getElementById('metrics-container'), - achievementsContainer: document.getElementById('achievements-container'), -}; - -// ============================================================================ -// API Functions - Exercises & Progress -// ============================================================================ - -async function fetchExercises() { - try { - const response = await fetch(`${CONFIG.API_BASE}/exercises/`); - const exercises = await response.json(); - state.exercises = exercises; - renderExerciseList(exercises); - } catch (error) { - console.error('Failed to fetch exercises:', error); - } -} - -async function fetchProgress() { - try { - const response = await fetch(`${CONFIG.API_BASE}/progress/${CONFIG.USER_ID}/summary`); - const summary = await response.json(); - renderProgressSummary(summary); - } catch (error) { - console.error('Failed to fetch progress:', error); - } -} - -async function fetchAchievements() { - try { - const response = await fetch(`${CONFIG.API_BASE}/progress/${CONFIG.USER_ID}/achievements`); - const achievements = await response.json(); - renderAchievements(achievements); - } catch (error) { - console.error('Failed to fetch achievements:', error); - } -} - -// ============================================================================ -// API Functions - Companion Chat -// ============================================================================ - -async function sendChatMessage(message) { - if (!message.trim()) return; - - // Add user message to UI - addChatMessage(message, 'user'); - elements.chatInput.value = ''; - - try { - const response = await fetch(`${CONFIG.COMPANION_API}/chat`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - content: message, - session_id: CONFIG.USER_ID, - }), - }); - - if (response.ok) { - const data = await response.json(); - addChatMessage(data.text || '...', 'assistant'); - } else { - addChatMessage('抱歉,暂时无法回复。请稍后再试。', 'assistant'); - } - } catch (error) { - console.error('Chat error:', error); - addChatMessage('连接出现问题,请检查网络。', 'assistant'); - } -} - -function addChatMessage(text, role) { - const welcome = elements.chatMessages.querySelector('.chat-welcome'); - if (welcome) welcome.remove(); - - const msgEl = document.createElement('div'); - msgEl.className = `chat-message ${role}`; - msgEl.textContent = text; - elements.chatMessages.appendChild(msgEl); - elements.chatMessages.scrollTop = elements.chatMessages.scrollHeight; - - state.chatHistory.push({ role, text }); -} - -// ============================================================================ -// API Functions - Community -// ============================================================================ - -async function fetchCommunityPosts(channel = 'all') { - try { - let url = `${CONFIG.COMMUNITY_API}/questions/`; - if (channel !== 'all') { - url = `${CONFIG.COMMUNITY_API}/questions/channel/${channel}`; - } - const response = await fetch(url); - if (response.ok) { - const data = await response.json(); - const posts = data.items || []; - state.communityPosts = posts; - renderCommunityPosts(posts); - } else { - renderEmptyCommunity(); - } - } catch (error) { - console.error('Failed to fetch posts:', error); - renderEmptyCommunity(); - } -} - -function renderCommunityPosts(posts) { - if (!posts || posts.length === 0) { - renderEmptyCommunity(); - return; - } - - elements.communityPosts.innerHTML = posts.map(post => ` -
- -

${escapeHtml(post.title)}

-
${escapeHtml(post.content_preview)}
- -
- `).join(''); -} - -function renderEmptyCommunity() { - elements.communityPosts.innerHTML = ` -
-

暂无帖子,来发布第一条吧

-
- `; -} - -async function createQuestion(title, content, channel) { - try { - const response = await fetch(`${CONFIG.COMMUNITY_API}/questions/`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-User-ID': CONFIG.USER_ID, - }, - body: JSON.stringify({ - title: title, - content: content, - channel: channel, - tag_ids: [], - image_urls: [], - }), - }); - - if (response.ok || response.status === 201) { - return { success: true }; - } else { - const error = await response.json(); - return { success: false, error: error.detail || '发布失败' }; - } - } catch (error) { - console.error('Create question error:', error); - return { success: false, error: '网络错误,请重试' }; - } -} - -function openPostModal() { - elements.postModal.classList.remove('hidden'); - elements.postTitle.value = ''; - elements.postContent.value = ''; - elements.postChannel.value = 'experience'; -} - -function closePostModal() { - elements.postModal.classList.add('hidden'); -} - -async function handlePostSubmit(e) { - e.preventDefault(); - - const title = elements.postTitle.value.trim(); - const content = elements.postContent.value.trim(); - const channel = elements.postChannel.value; - - if (title.length < 5) { - alert('标题至少需要5个字'); - return; - } - if (content.length < 10) { - alert('内容至少需要10个字'); - return; - } - - const result = await createQuestion(title, content, channel); - - if (result.success) { - closePostModal(); - fetchCommunityPosts(state.currentChannel); - alert('发布成功!'); - } else { - alert(result.error); - } -} - -function getRoleName(role) { - const names = { - doctor: '医生', - nurse: '护士', - mom: '妈妈', - expert: '专家', - }; - return names[role] || role; -} - -// ============================================================================ -// WebSocket Functions -// ============================================================================ - -function connectWebSocket(exerciseId) { - state.sessionId = generateSessionId(); - const wsUrl = `${CONFIG.WS_BASE}/${state.sessionId}`; - - state.ws = new WebSocket(wsUrl); - - state.ws.onopen = () => { - console.log('WebSocket connected'); - state.ws.send(JSON.stringify({ - type: 'start', - exercise_id: exerciseId, - user_id: CONFIG.USER_ID, - use_llm: true, - })); - }; - - state.ws.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - handleWebSocketMessage(data); - } catch (e) { - console.error('Error handling message:', e); - } - }; - - state.ws.onclose = (event) => { - console.log('WebSocket disconnected'); - stopFrameSending(); - stopAllAudio(); - }; - - state.ws.onerror = (error) => { - console.error('WebSocket error:', error); - }; -} - -function handleWebSocketMessage(data) { - switch (data.type) { - case 'ack': - console.log('Server acknowledged:', data.message); - break; - - case 'state': - updateSessionState(data.data); - if (data.annotated_frame) { - displayAnnotatedFrame(data.annotated_frame); - } - if (data.feedback) { - displayFeedback(data.feedback); - if (data.feedback.audio) { - playAudio(data.feedback.audio); - } - } - break; - - case 'feedback': - displayFeedback(data.feedback); - if (data.feedback.audio) { - playAudio(data.feedback.audio); - } - break; - - case 'session_ended': - showSessionComplete(data.summary); - break; - - case 'error': - console.error('Server error:', data.message); - showFeedback(data.message, 'warning'); - break; - } -} - -function sendControl(action) { - if (state.ws && state.ws.readyState === WebSocket.OPEN) { - state.ws.send(JSON.stringify({ - type: 'control', - action: action, - })); - } -} - -// ============================================================================ -// Video & Frame Processing -// ============================================================================ - -async function startCamera() { - try { - const stream = await navigator.mediaDevices.getUserMedia({ - video: { - width: { ideal: 480 }, - height: { ideal: 360 }, - facingMode: 'user', - }, - audio: false, - }); - - state.videoStream = stream; - elements.localVideo.srcObject = stream; - - await new Promise((resolve) => { - elements.localVideo.onloadedmetadata = resolve; - }); - - return true; - } catch (error) { - console.error('Failed to start camera:', error); - alert('无法访问摄像头,请检查权限设置。'); - return false; - } -} - -function stopCamera() { - if (state.videoStream) { - state.videoStream.getTracks().forEach(track => track.stop()); - state.videoStream = null; - } -} - -function startFrameSending() { - if (state.frameInterval) return; - - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - - state.frameInterval = setInterval(() => { - if (!state.ws || state.ws.readyState !== WebSocket.OPEN) return; - if (!elements.localVideo.videoWidth) return; - - canvas.width = elements.localVideo.videoWidth; - canvas.height = elements.localVideo.videoHeight; - ctx.drawImage(elements.localVideo, 0, 0); - - const dataUrl = canvas.toDataURL('image/jpeg', 0.5); - const base64 = dataUrl.split(',')[1]; - - state.ws.send(JSON.stringify({ - type: 'frame', - data: base64, - })); - }, 1000 / CONFIG.FRAME_RATE); -} - -function stopFrameSending() { - if (state.frameInterval) { - clearInterval(state.frameInterval); - state.frameInterval = null; - } -} - -function displayAnnotatedFrame(base64Data) { - const canvas = elements.poseCanvas; - const ctx = canvas.getContext('2d'); - - const img = new Image(); - img.onload = () => { - canvas.width = img.width; - canvas.height = img.height; - ctx.drawImage(img, 0, 0); - }; - img.src = `data:image/jpeg;base64,${base64Data}`; -} - -// ============================================================================ -// UI Rendering -// ============================================================================ - -function renderExerciseList(exercises, category = 'all') { - const filtered = category === 'all' - ? exercises - : exercises.filter(e => e.category === category); - - elements.exerciseList.innerHTML = filtered.map(exercise => ` -
-
-

${exercise.name}

- ${getDifficultyName(exercise.difficulty)} -
-

${exercise.description.substring(0, 100)}...

- -
- `).join(''); - - elements.exerciseList.querySelectorAll('.exercise-card').forEach(card => { - card.addEventListener('click', () => { - const exerciseId = card.dataset.id; - const exercise = exercises.find(e => e.id === exerciseId); - startSession(exercise); - }); - }); -} - -function renderProgressSummary(summary) { - elements.totalSessions.textContent = summary.total_sessions || 0; - elements.currentStreak.textContent = summary.current_streak || 0; - elements.totalMinutes.textContent = Math.round(summary.total_minutes || 0); - - if (summary.strength_metrics) { - elements.metricsContainer.innerHTML = Object.entries(summary.strength_metrics) - .map(([name, data]) => ` -
-
- ${getMetricName(name)} - ${Math.round(data.value)}% -
-
-
-
-
- `).join(''); - } -} - -function renderAchievements(achievements) { - elements.achievementsContainer.innerHTML = achievements.map(a => ` -
-
${getAchievementIcon(a.icon)}
-
${a.name}
-
- `).join(''); -} - -function updateSessionState(data) { - if (!data) return; - - if (data.progress) { - const progress = data.progress; - elements.sessionProgressBar.style.width = `${progress.progress}%`; - elements.currentSet.textContent = progress.current_set || 1; - elements.totalSets.textContent = progress.total_sets || 3; - elements.currentRep.textContent = progress.current_rep || 1; - elements.totalReps.textContent = progress.total_reps || 10; - - if (progress.current_phase) { - elements.phaseIndicator.textContent = getPhaseName(progress.current_phase); - } - } - - if (data.analysis) { - const score = Math.round(data.analysis.score); - elements.scoreDisplay.querySelector('.score-value').textContent = score; - } - - updateControlButtons(data.session_state); -} - -function updateControlButtons(sessionState) { - elements.startBtn.classList.toggle('hidden', sessionState !== 'preparing'); - elements.pauseBtn.classList.toggle('hidden', sessionState !== 'exercising'); - elements.resumeBtn.classList.toggle('hidden', sessionState !== 'paused'); -} - -function displayFeedback(feedback) { - const el = elements.feedbackMessage; - el.textContent = feedback.text; - el.className = 'feedback-message'; - - if (feedback.type === 'correction') { - el.classList.add('correction'); - } else if (feedback.type === 'safety_warning') { - el.classList.add('warning'); - } else if (feedback.type === 'encouragement') { - el.classList.add('encouragement'); - } -} - -function showFeedback(text, type = 'info') { - elements.feedbackMessage.textContent = text; - elements.feedbackMessage.className = `feedback-message ${type}`; -} - -function showSessionComplete(summary) { - stopFrameSending(); - state.isSessionActive = false; - - elements.summaryScore.textContent = Math.round(summary.average_score || 0); - elements.summaryReps.textContent = summary.completed_reps || 0; - elements.summaryDuration.textContent = formatDuration(summary.session_duration || 0); - - if (summary.new_achievements && summary.new_achievements.length > 0) { - elements.newAchievements.classList.remove('hidden'); - elements.achievementsEarned.innerHTML = summary.new_achievements - .map(a => `
-
${getAchievementIcon(a.icon)}
-
${a.name}
-
`) - .join(''); - } else { - elements.newAchievements.classList.add('hidden'); - } - - elements.sessionCompleteModal.classList.remove('hidden'); -} - -// ============================================================================ -// Session Management -// ============================================================================ - -async function startSession(exercise) { - state.selectedExercise = exercise; - - showView('session'); - elements.sessionExerciseName.textContent = exercise.name; - - if (exercise.phases && exercise.phases.length > 0) { - const firstPhase = exercise.phases[0]; - elements.phaseDescription.textContent = firstPhase.description; - elements.phaseCues.innerHTML = firstPhase.cues - .map(cue => `
  • ${cue}
  • `) - .join(''); - } - - const cameraStarted = await startCamera(); - if (!cameraStarted) { - showView('exercises'); - return; - } - - connectWebSocket(exercise.id); -} - -function beginExercise() { - if (state.ws && state.ws.readyState === WebSocket.OPEN) { - state.ws.send(JSON.stringify({ type: 'begin' })); - state.isSessionActive = true; - startFrameSending(); - } -} - -function endSession() { - sendControl('end'); - stopFrameSending(); - stopCamera(); - stopAllAudio(); - - if (state.ws) { - state.ws.close(); - state.ws = null; - } -} - -// ============================================================================ -// View Navigation -// ============================================================================ - -function showView(viewName) { - state.currentView = viewName; - - // Hide all views - document.querySelectorAll('.view').forEach(v => v.classList.remove('active')); - - // Show selected view - const viewEl = document.getElementById(`${viewName}-view`); - if (viewEl) viewEl.classList.add('active'); - - // Update nav buttons - document.querySelectorAll('.nav-btn').forEach(btn => { - btn.classList.toggle('active', btn.dataset.view === viewName); - }); - - // Load data for view - switch (viewName) { - case 'exercises': - if (state.exercises.length === 0) fetchExercises(); - break; - case 'progress': - fetchProgress(); - fetchAchievements(); - break; - case 'community': - fetchCommunityPosts(state.currentChannel); - break; - } -} - -// Global function for onclick handlers in HTML -window.showView = showView; - -// ============================================================================ -// Utility Functions -// ============================================================================ - -function generateSessionId() { - return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); -} - -function getCategoryName(category) { - const names = { - breathing: '呼吸训练', - pelvic_floor: '盆底肌', - diastasis_recti: '腹直肌修复', - posture: '体态矫正', - strength: '力量训练', - }; - return names[category] || category; -} - -function getDifficultyName(difficulty) { - const names = { - beginner: '初级', - intermediate: '中级', - advanced: '高级', - }; - return names[difficulty] || difficulty; -} - -function getMetricName(name) { - const names = { - core_strength: '核心力量', - pelvic_floor: '盆底肌', - posture: '体态', - flexibility: '柔韧性', - }; - return names[name] || name; -} - -function getPhaseName(phase) { - const names = { - preparation: '准备', - inhale: '吸气', - exhale: '呼气', - hold: '保持', - release: '放松', - rest: '休息', - }; - return names[phase] || phase; -} - -function getAchievementIcon(icon) { - const icons = { - footprints: '👣', - fire: '🔥', - 'calendar-check': '📅', - trophy: '🏆', - star: '⭐', - 'check-circle': '✅', - medal: '🏅', - 'trending-up': '📈', - award: '🎖️', - }; - return icons[icon] || '🌟'; -} - -function formatDuration(seconds) { - const mins = Math.floor(seconds / 60); - const secs = Math.round(seconds % 60); - return `${mins}:${secs.toString().padStart(2, '0')}`; -} - -function formatTime(timestamp) { - if (!timestamp) return ''; - // 后端返回的是 UTC 时间,需要添加 Z 标识 - const normalizedTimestamp = timestamp.endsWith('Z') ? timestamp : timestamp + 'Z'; - const date = new Date(normalizedTimestamp); - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffMins = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMs / 3600000); - const diffDays = Math.floor(diffMs / 86400000); - - if (diffMins < 1) return '刚刚'; - if (diffMins < 60) return `${diffMins}分钟前`; - if (diffHours < 24) return `${diffHours}小时前`; - if (diffDays < 7) return `${diffDays}天前`; - return date.toLocaleDateString('zh-CN'); -} - -function escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; -} - -// Audio queue management -function queueAudio(base64Data) { - if (!base64Data) return; - state.audioQueue.push(base64Data); - processAudioQueue(); -} - -function processAudioQueue() { - if (state.isPlayingAudio || state.audioQueue.length === 0) { - return; - } - - state.isPlayingAudio = true; - const base64Data = state.audioQueue.shift(); - - try { - const audio = new Audio(`data:audio/mp3;base64,${base64Data}`); - state.currentAudio = audio; - - audio.onended = () => { - state.isPlayingAudio = false; - state.currentAudio = null; - setTimeout(() => processAudioQueue(), 500); - }; - - audio.onerror = () => { - state.isPlayingAudio = false; - state.currentAudio = null; - setTimeout(() => processAudioQueue(), 100); - }; - - audio.play().catch(() => { - state.isPlayingAudio = false; - state.currentAudio = null; - processAudioQueue(); - }); - } catch (error) { - state.isPlayingAudio = false; - processAudioQueue(); - } -} - -function playAudio(base64Data) { - queueAudio(base64Data); -} - -function stopAllAudio() { - state.audioQueue = []; - if (state.currentAudio) { - state.currentAudio.pause(); - state.currentAudio = null; - } - state.isPlayingAudio = false; -} - -// ============================================================================ -// Event Listeners -// ============================================================================ - -function initEventListeners() { - // Navigation - document.querySelectorAll('.nav-btn').forEach(btn => { - btn.addEventListener('click', () => { - const view = btn.dataset.view; - if (view) showView(view); - }); - }); - - // Category filter - document.querySelectorAll('.category-btn').forEach(btn => { - btn.addEventListener('click', () => { - document.querySelectorAll('.category-btn').forEach(b => b.classList.remove('active')); - btn.classList.add('active'); - renderExerciseList(state.exercises, btn.dataset.category); - }); - }); - - // Community channel filter - document.querySelectorAll('.channel-btn').forEach(btn => { - btn.addEventListener('click', () => { - document.querySelectorAll('.channel-btn').forEach(b => b.classList.remove('active')); - btn.classList.add('active'); - state.currentChannel = btn.dataset.channel; - fetchCommunityPosts(btn.dataset.channel); - }); - }); - - // Post modal - if (elements.openPostModalBtn) { - elements.openPostModalBtn.addEventListener('click', openPostModal); - } - if (elements.cancelPostBtn) { - elements.cancelPostBtn.addEventListener('click', closePostModal); - } - if (elements.postForm) { - elements.postForm.addEventListener('submit', handlePostSubmit); - } - if (elements.postModal) { - elements.postModal.addEventListener('click', (e) => { - if (e.target === elements.postModal) closePostModal(); - }); - } - - // Chat - if (elements.chatSendBtn) { - elements.chatSendBtn.addEventListener('click', () => { - sendChatMessage(elements.chatInput.value); - }); - } - - if (elements.chatInput) { - elements.chatInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - sendChatMessage(elements.chatInput.value); - } - }); - } - - // Session controls - if (elements.startBtn) elements.startBtn.addEventListener('click', beginExercise); - if (elements.pauseBtn) elements.pauseBtn.addEventListener('click', () => sendControl('pause')); - if (elements.resumeBtn) elements.resumeBtn.addEventListener('click', () => sendControl('resume')); - if (elements.endSessionBtn) elements.endSessionBtn.addEventListener('click', endSession); - - // Modal - if (elements.closeModalBtn) { - elements.closeModalBtn.addEventListener('click', () => { - elements.sessionCompleteModal.classList.add('hidden'); - showView('exercises'); - fetchProgress(); - }); - } -} - -// ============================================================================ -// Initialization -// ============================================================================ - -document.addEventListener('DOMContentLoaded', () => { - initEventListeners(); - // Start on home view - showView('home'); -}); diff --git a/backend/app/templates/index.html b/backend/app/templates/index.html deleted file mode 100644 index ac8e8a6e..00000000 --- a/backend/app/templates/index.html +++ /dev/null @@ -1,241 +0,0 @@ - - - - - - MomShell - 产后康复助手 - - - -
    - -
    - - -
    - - -
    - -
    -
    -

    MomShell

    -

    为新妈妈打造的温暖空间

    -
    - -
    - - -
    -

    情感陪伴

    -
    -
    -
    -

    你好,我是你的情感陪伴助手。

    -

    有什么想聊的吗?无论是开心还是烦恼,我都在这里。

    -
    -
    -
    - - -
    -
    -
    - - -
    -
    -

    互助社区

    - -
    -
    - - - -
    -
    -
    -

    暂无帖子

    -
    -
    -
    - - - - - -
    -

    康复动作

    -
    - - - - - - -
    -
    - -
    -
    - - -
    -

    我的进度

    -
    -
    - 0 - 训练次数 -
    -
    - 0 - 连续天数 -
    -
    - 0 - 训练分钟 -
    -
    -
    -

    力量恢复进度

    -
    -
    -
    -

    成就勋章

    -
    -
    -
    - - -
    -
    -

    动作名称

    - -
    - -
    - -
    -
    - - -
    -
    准备中...
    -
    - -- - 得分 -
    -
    -
    -
    - - -
    -
    -
    -
    -
    -
    - 1/3 - 1/10 -
    -
    - -
    -

    当前阶段

    -

    准备开始

    -
      -
      - - - -
      - - - -
      -
      -
      -
      - - - -
      -
      - - - - diff --git a/backend/app/tts_cache/3fde956fc05b1b35b28663f449fd73df.mp3 b/backend/app/tts_cache/3fde956fc05b1b35b28663f449fd73df.mp3 deleted file mode 100644 index 3bb5cbea..00000000 --- a/backend/app/tts_cache/3fde956fc05b1b35b28663f449fd73df.mp3 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e0b4210488f633775f7d6e4b993af0753aefc9a21839b0b1bb484a14a798f0b8 -size 16128 diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go new file mode 100644 index 00000000..163c99a0 --- /dev/null +++ b/backend/cmd/server/main.go @@ -0,0 +1,144 @@ +package main + +import ( + "fmt" + "log" + + "github.com/gin-gonic/gin" + "github.com/momshell/backend/internal/config" + "github.com/momshell/backend/internal/database" + "github.com/momshell/backend/internal/handler" + "github.com/momshell/backend/internal/middleware" + "github.com/momshell/backend/internal/model" + "github.com/momshell/backend/internal/repository" + "github.com/momshell/backend/internal/router" + "github.com/momshell/backend/internal/service" + "github.com/momshell/backend/pkg/openai" + "github.com/momshell/backend/pkg/password" +) + +func main() { + // Load config + cfg := config.Load() + + // Connect to database + db, err := database.Connect(cfg) + if err != nil { + log.Fatalf("Failed to connect to database: %v", err) + } + log.Println("Connected to database") + + // Run migrations + if err := database.Migrate(db); err != nil { + log.Fatalf("Failed to run migrations: %v", err) + } + log.Println("Database migrations completed") + + // Create initial admin if configured + createInitialAdmin(cfg, repository.NewUserRepo(db)) + + // Initialize repositories + userRepo := repository.NewUserRepo(db) + questionRepo := repository.NewQuestionRepo(db) + answerRepo := repository.NewAnswerRepo(db) + commentRepo := repository.NewCommentRepo(db) + interactionRepo := repository.NewInteractionRepo(db) + tagRepo := repository.NewTagRepo(db) + chatRepo := repository.NewChatRepo(db) + echoRepo := repository.NewEchoRepo(db) + + // Initialize services + moderationService := service.NewModerationService() + authService := service.NewAuthService(cfg, userRepo) + communityService := service.NewCommunityService( + questionRepo, answerRepo, commentRepo, + interactionRepo, tagRepo, userRepo, + moderationService, + ) + + var chatClient *openai.Client + if cfg.OpenAIAPIKey != "" { + chatClient = openai.NewClient(cfg.OpenAIAPIKey, cfg.OpenAIBaseURL, cfg.OpenAIModel) + } else { + log.Println("[WARN] OPENAI_API_KEY not set, chat service will not work") + chatClient = openai.NewClient("dummy", cfg.OpenAIBaseURL, cfg.OpenAIModel) + } + chatService := service.NewChatService(chatClient, chatRepo) + echoService := service.NewEchoService(chatClient, echoRepo) + + userService := service.NewUserService( + db, userRepo, questionRepo, answerRepo, + interactionRepo, communityService, + ) + + // Initialize admin layer + adminRepo := repository.NewAdminRepo(db) + adminService := service.NewAdminService(cfg, adminRepo, userRepo) + + // Initialize handlers + authHandler := handler.NewAuthHandler(authService) + questionHandler := handler.NewQuestionHandler(communityService, authService) + answerHandler := handler.NewAnswerHandler(communityService, authService) + commentHandler := handler.NewCommentHandler(communityService, authService) + interactionHandler := handler.NewInteractionHandler(communityService) + tagHandler := handler.NewTagHandler(communityService) + chatHandler := handler.NewChatHandler(chatService) + echoHandler := handler.NewEchoHandler(echoService) + userHandler := handler.NewUserHandler(userService) + adminHandler := handler.NewAdminHandler(adminService, authService) + + // Setup Gin + r := gin.New() + r.Use(middleware.Recovery()) + r.Use(middleware.CORS()) + r.Use(gin.Logger()) + + // Register routes + router.Setup( + r, cfg, + authHandler, questionHandler, answerHandler, + commentHandler, interactionHandler, tagHandler, + chatHandler, echoHandler, userHandler, adminHandler, + ) + + // Start server + addr := fmt.Sprintf(":%s", cfg.Port) + log.Printf("Starting server on %s", addr) + if err := r.Run(addr); err != nil { + log.Fatalf("Failed to start server: %v", err) + } +} + +func createInitialAdmin(cfg *config.Config, userRepo *repository.UserRepo) { + if cfg.AdminUsername == "" || cfg.AdminEmail == "" || cfg.AdminPassword == "" { + return + } + + exists, _ := userRepo.ExistsByUsernameOrEmail(cfg.AdminUsername, cfg.AdminEmail) + if exists { + log.Println("Admin user already exists, skipping creation") + return + } + + hash, err := password.Hash(cfg.AdminPassword) + if err != nil { + log.Printf("Failed to hash admin password: %v", err) + return + } + + admin := &model.User{ + Username: cfg.AdminUsername, + Email: cfg.AdminEmail, + PasswordHash: hash, + Nickname: "Admin", + Role: model.RoleAdmin, + IsActive: true, + } + + if err := userRepo.Create(admin); err != nil { + log.Printf("Failed to create admin user: %v", err) + return + } + + log.Printf("Admin user created: %s", cfg.AdminUsername) +} diff --git a/backend/data/.gitkeep b/backend/data/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 00000000..c6355567 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,51 @@ +module github.com/momshell/backend + +go 1.23 + +require ( + github.com/gin-contrib/cors v1.7.2 + github.com/gin-gonic/gin v1.10.0 + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/google/uuid v1.6.0 + github.com/joho/godotenv v1.5.1 + github.com/sashabaranov/go-openai v1.32.5 + golang.org/x/crypto v0.31.0 + gorm.io/driver/postgres v1.5.11 + gorm.io/gorm v1.25.12 +) + +require ( + github.com/bytedance/sonic v1.12.6 // indirect + github.com/bytedance/sonic/loader v0.2.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.7 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.23.0 // indirect + github.com/goccy/go-json v0.10.4 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.2 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.12.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/protobuf v1.36.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 00000000..4c5b581b --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,121 @@ +github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk= +github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E= +github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= +github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= +github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= +github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= +github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= +github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= +github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/sashabaranov/go-openai v1.32.5 h1:/eNVa8KzlE7mJdKPZDj6886MUzZQjoVHyn0sLvIt5qA= +github.com/sashabaranov/go-openai v1.32.5/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= +golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= +gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/backend/internal/admin/admin.html b/backend/internal/admin/admin.html new file mode 100644 index 00000000..ec510ccb --- /dev/null +++ b/backend/internal/admin/admin.html @@ -0,0 +1,692 @@ + + + + + + MomShell 管理面板 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/internal/admin/embed.go b/backend/internal/admin/embed.go new file mode 100644 index 00000000..20f9765c --- /dev/null +++ b/backend/internal/admin/embed.go @@ -0,0 +1,6 @@ +package admin + +import _ "embed" + +//go:embed admin.html +var HTML []byte diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go new file mode 100644 index 00000000..4d1dfadd --- /dev/null +++ b/backend/internal/config/config.go @@ -0,0 +1,79 @@ +package config + +import ( + "fmt" + "os" + "strconv" + + "github.com/joho/godotenv" +) + +type Config struct { + // Database + DatabaseURL string + + // JWT + JWTSecretKey string + JWTAlgorithm string + JWTAccessTokenExpireMin int + JWTRefreshTokenExpireDays int + + // OpenAI compatible API + OpenAIAPIKey string + OpenAIBaseURL string + OpenAIModel string + + // Server + Port string + + // Admin + AdminUsername string + AdminEmail string + AdminPassword string +} + +func Load() *Config { + // Load .env file, overriding any existing env vars + // (ensures local .env is the source of truth for dev) + _ = godotenv.Overload() + + cfg := &Config{ + DatabaseURL: getEnv("DATABASE_URL", "postgres://user:password@localhost:5432/momshell?sslmode=disable"), + JWTSecretKey: getEnv("JWT_SECRET_KEY", "change-me-in-production"), + JWTAlgorithm: "HS256", + JWTAccessTokenExpireMin: getEnvInt("JWT_ACCESS_TOKEN_EXPIRE_MINUTES", 30), + JWTRefreshTokenExpireDays: getEnvInt("JWT_REFRESH_TOKEN_EXPIRE_DAYS", 7), + OpenAIAPIKey: getEnv("OPENAI_API_KEY", ""), + OpenAIBaseURL: getEnv("OPENAI_BASE_URL", "https://api-inference.modelscope.cn/v1"), + OpenAIModel: getEnv("OPENAI_MODEL", "Qwen/Qwen2.5-72B-Instruct"), + Port: getEnv("PORT", "8000"), + AdminUsername: getEnv("ADMIN_USERNAME", ""), + AdminEmail: getEnv("ADMIN_EMAIL", ""), + AdminPassword: getEnv("ADMIN_PASSWORD", ""), + } + + if cfg.JWTSecretKey == "change-me-in-production" { + fmt.Println("[WARN] Using default JWT secret key. Set JWT_SECRET_KEY in production!") + } + + return cfg +} + +func getEnv(key, fallback string) string { + if v := os.Getenv(key); v != "" { + return v + } + return fallback +} + +func getEnvInt(key string, fallback int) int { + v := os.Getenv(key) + if v == "" { + return fallback + } + i, err := strconv.Atoi(v) + if err != nil { + return fallback + } + return i +} diff --git a/backend/internal/database/database.go b/backend/internal/database/database.go new file mode 100644 index 00000000..6bfee090 --- /dev/null +++ b/backend/internal/database/database.go @@ -0,0 +1,29 @@ +package database + +import ( + "fmt" + + "github.com/momshell/backend/internal/config" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +func Connect(cfg *config.Config) (*gorm.DB, error) { + db, err := gorm.Open(postgres.Open(cfg.DatabaseURL), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), + }) + if err != nil { + return nil, fmt.Errorf("failed to connect to database: %w", err) + } + + sqlDB, err := db.DB() + if err != nil { + return nil, fmt.Errorf("failed to get underlying sql.DB: %w", err) + } + + sqlDB.SetMaxOpenConns(25) + sqlDB.SetMaxIdleConns(10) + + return db, nil +} diff --git a/backend/internal/database/migrate.go b/backend/internal/database/migrate.go new file mode 100644 index 00000000..7f27b6cc --- /dev/null +++ b/backend/internal/database/migrate.go @@ -0,0 +1,24 @@ +package database + +import ( + "github.com/momshell/backend/internal/model" + "gorm.io/gorm" +) + +func Migrate(db *gorm.DB) error { + return db.AutoMigrate( + &model.User{}, + &model.UserCertification{}, + &model.Tag{}, + &model.Question{}, + &model.QuestionTag{}, + &model.Answer{}, + &model.Comment{}, + &model.Like{}, + &model.Collection{}, + &model.ModerationLog{}, + &model.ChatMemory{}, + &model.IdentityTag{}, + &model.Memoir{}, + ) +} diff --git a/backend/internal/dto/admin.go b/backend/internal/dto/admin.go new file mode 100644 index 00000000..61187d8c --- /dev/null +++ b/backend/internal/dto/admin.go @@ -0,0 +1,89 @@ +package dto + +import "time" + +// AdminUserListParams holds query parameters for admin user listing +type AdminUserListParams struct { + PaginationParams + Search string `form:"search"` + Role string `form:"role"` + Status string `form:"status"` // active, banned, inactive +} + +// AdminUserListItem is a user row in admin user list +type AdminUserListItem struct { + ID string `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Nickname string `json:"nickname"` + Role string `json:"role"` + IsActive bool `json:"is_active"` + IsBanned bool `json:"is_banned"` + IsGuest bool `json:"is_guest"` + CreatedAt time.Time `json:"created_at"` +} + +// AdminUserDetail is the full user detail for admin view +type AdminUserDetail struct { + ID string `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Nickname string `json:"nickname"` + AvatarURL *string `json:"avatar_url"` + Role string `json:"role"` + ShellCode *string `json:"shell_code"` + IsGuest bool `json:"is_guest"` + IsActive bool `json:"is_active"` + IsBanned bool `json:"is_banned"` + PartnerID *string `json:"partner_id"` + BabyBirthDate *time.Time `json:"baby_birth_date"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LastActiveAt *time.Time `json:"last_active_at"` + + // Certification info + CertificationStatus *string `json:"certification_status,omitempty"` + CertificationType *string `json:"certification_type,omitempty"` +} + +// AdminCreateUser is the request body for creating a user via admin +type AdminCreateUser struct { + Username string `json:"username" binding:"required,min=3,max=50"` + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required,min=6"` + Nickname string `json:"nickname" binding:"required,min=1,max=50"` + Role string `json:"role" binding:"required"` +} + +// AdminUserUpdate is the request body for updating a user via admin +type AdminUserUpdate struct { + Role *string `json:"role"` + IsActive *bool `json:"is_active"` + IsBanned *bool `json:"is_banned"` + Nickname *string `json:"nickname"` + Email *string `json:"email"` +} + +// DashboardStats holds statistics for the admin dashboard +type DashboardStats struct { + TotalUsers int64 `json:"total_users"` + ActiveUsers int64 `json:"active_users"` + BannedUsers int64 `json:"banned_users"` + GuestUsers int64 `json:"guest_users"` + RoleDistribution map[string]int64 `json:"role_distribution"` + TotalQuestions int64 `json:"total_questions"` + TotalAnswers int64 `json:"total_answers"` + TotalCertifications int64 `json:"total_certifications"` +} + +// ConfigItem represents a single configuration item +type ConfigItem struct { + Key string `json:"key"` + Value string `json:"value"` + Editable bool `json:"editable"` +} + +// ConfigUpdateRequest is the request body for updating configuration +type ConfigUpdateRequest struct { + Items map[string]string `json:"items" binding:"required"` +} diff --git a/backend/internal/dto/answer.go b/backend/internal/dto/answer.go new file mode 100644 index 00000000..99cd6aac --- /dev/null +++ b/backend/internal/dto/answer.go @@ -0,0 +1,72 @@ +package dto + +import "time" + +// AnswerCreate is the request body for creating an answer +type AnswerCreate struct { + Content string `json:"content" binding:"required,min=1"` + ImageURLs []string `json:"image_urls"` +} + +// AnswerUpdate is the request body for updating an answer +type AnswerUpdate struct { + Content *string `json:"content" binding:"omitempty,min=1"` +} + +// AnswerListParams holds query parameters for listing answers +type AnswerListParams struct { + PaginationParams + IsProfessional *bool `form:"is_professional"` + SortBy string `form:"sort_by" binding:"omitempty,oneof=created_at like_count"` + Order string `form:"order" binding:"omitempty,oneof=asc desc"` +} + +func (p *AnswerListParams) GetSortBy() string { + if p.SortBy == "" { + return "created_at" + } + return p.SortBy +} + +func (p *AnswerListParams) GetOrder() string { + if p.Order == "" { + return "desc" + } + return p.Order +} + +// AnswerListItem is a single answer in list responses +type AnswerListItem struct { + ID string `json:"id"` + QuestionID string `json:"question_id"` + Author AuthorInfo `json:"author"` + Content string `json:"content"` + ContentPreview string `json:"content_preview"` + IsProfessional bool `json:"is_professional"` + IsAccepted bool `json:"is_accepted"` + LikeCount int `json:"like_count"` + CommentCount int `json:"comment_count"` + IsLiked bool `json:"is_liked"` + CreatedAt time.Time `json:"created_at"` +} + +// MyAnswerListItem is for the user's own answers list +type MyAnswerListItem struct { + ID string `json:"id"` + ContentPreview string `json:"content_preview"` + Question QuestionBrief `json:"question"` + IsProfessional bool `json:"is_professional"` + IsAccepted bool `json:"is_accepted"` + LikeCount int `json:"like_count"` + CommentCount int `json:"comment_count"` + Status string `json:"status"` + IsLiked bool `json:"is_liked"` + CreatedAt time.Time `json:"created_at"` +} + +// QuestionBrief is a brief question for context in answer lists +type QuestionBrief struct { + ID string `json:"id"` + Title string `json:"title"` + Channel string `json:"channel"` +} diff --git a/backend/internal/dto/auth.go b/backend/internal/dto/auth.go new file mode 100644 index 00000000..6f7d870b --- /dev/null +++ b/backend/internal/dto/auth.go @@ -0,0 +1,67 @@ +package dto + +import "time" + +// RegisterRequest is the request body for registration +type RegisterRequest struct { + Username string `json:"username" binding:"required,min=3,max=50"` + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required,min=6"` + Nickname string `json:"nickname" binding:"required,min=1,max=50"` + Role string `json:"role" binding:"omitempty,oneof=mom dad family"` +} + +// LoginRequest is the request body for login +type LoginRequest struct { + Login string `json:"login" binding:"required"` // username or email + Password string `json:"password" binding:"required"` +} + +// TokenResponse is the response body for login/refresh +type TokenResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int `json:"expires_in"` // seconds +} + +// RefreshRequest is the request body for token refresh +type RefreshRequest struct { + RefreshToken string `json:"refresh_token" binding:"required"` +} + +// UserResponse is the response body for user info +type UserResponse struct { + ID string `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Nickname string `json:"nickname"` + AvatarURL *string `json:"avatar_url"` + Role string `json:"role"` + IsCertified bool `json:"is_certified"` + CertificationTitle *string `json:"certification_title"` + BabyBirthDate *time.Time `json:"baby_birth_date"` + PostpartumWeeks *int `json:"postpartum_weeks"` + CreatedAt time.Time `json:"created_at"` +} + +// ChangePasswordRequest is the request body for changing password +type ChangePasswordRequest struct { + OldPassword string `json:"old_password" binding:"required"` + NewPassword string `json:"new_password" binding:"required,min=6"` +} + +// ForgotPasswordRequest is the request body for forgot password +type ForgotPasswordRequest struct { + Email string `json:"email" binding:"required,email"` +} + +// ResetPasswordRequest is the request body for resetting password +type ResetPasswordRequest struct { + Token string `json:"token" binding:"required"` + NewPassword string `json:"new_password" binding:"required,min=6"` +} + +// UpdateRoleRequest is the request body for updating user role +type UpdateRoleRequest struct { + Role string `json:"role" binding:"required,oneof=mom dad family"` +} diff --git a/backend/internal/dto/chat.go b/backend/internal/dto/chat.go new file mode 100644 index 00000000..123d299f --- /dev/null +++ b/backend/internal/dto/chat.go @@ -0,0 +1,33 @@ +package dto + +// UserMessage is the request body for chat +type UserMessage struct { + Content string `json:"content" binding:"required,min=1"` + SessionID *string `json:"session_id"` +} + +// VisualMetadata holds visual effect info +type VisualMetadata struct { + EffectType string `json:"effect_type"` // ripple, sunlight, calm, warm_glow, gentle_wave + Intensity float64 `json:"intensity"` // 0.0 ~ 1.0 + ColorTone string `json:"color_tone"` // soft_pink, warm_gold, gentle_blue, lavender, neutral_white, coral, sage +} + +// VisualResponse is the response body for chat +type VisualResponse struct { + Text string `json:"text"` + VisualMetadata VisualMetadata `json:"visual_metadata"` + MemoryUpdated bool `json:"memory_updated"` +} + +// ChatProfile is the response body for chat profile +type ChatProfile struct { + PreferredName *string `json:"preferred_name"` + HasPets bool `json:"has_pets"` + PetDetails *string `json:"pet_details"` + Interests []string `json:"interests"` + Concerns []string `json:"concerns"` + ImportantDates []string `json:"important_dates"` + BabyAgeWeeks *int `json:"baby_age_weeks"` + CommunityInteractions []string `json:"community_interactions"` +} diff --git a/backend/internal/dto/comment.go b/backend/internal/dto/comment.go new file mode 100644 index 00000000..ae936163 --- /dev/null +++ b/backend/internal/dto/comment.go @@ -0,0 +1,23 @@ +package dto + +import "time" + +// CommentCreate is the request body for creating a comment +type CommentCreate struct { + Content string `json:"content" binding:"required,min=1"` + ParentID *string `json:"parent_id"` +} + +// CommentListItem is a single comment (with nested replies) +type CommentListItem struct { + ID string `json:"id"` + AnswerID string `json:"answer_id"` + Author AuthorInfo `json:"author"` + Content string `json:"content"` + ParentID *string `json:"parent_id"` + ReplyToUser *AuthorInfo `json:"reply_to_user"` + LikeCount int `json:"like_count"` + IsLiked bool `json:"is_liked"` + CreatedAt time.Time `json:"created_at"` + Replies []CommentListItem `json:"replies"` +} diff --git a/backend/internal/dto/common.go b/backend/internal/dto/common.go new file mode 100644 index 00000000..9ac04c49 --- /dev/null +++ b/backend/internal/dto/common.go @@ -0,0 +1,75 @@ +package dto + +import "time" + +// PaginationParams holds pagination query parameters +type PaginationParams struct { + Page int `form:"page" binding:"omitempty,min=1"` + PageSize int `form:"page_size" binding:"omitempty,min=1,max=100"` +} + +func (p *PaginationParams) GetPage() int { + if p.Page <= 0 { + return 1 + } + return p.Page +} + +func (p *PaginationParams) GetPageSize() int { + if p.PageSize <= 0 { + return 20 + } + return p.PageSize +} + +func (p *PaginationParams) GetOffset() int { + return (p.GetPage() - 1) * p.GetPageSize() +} + +// PaginatedResponse is a generic paginated response +type PaginatedResponse struct { + Items interface{} `json:"items"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + TotalPages int64 `json:"total_pages"` +} + +func NewPaginatedResponse(items interface{}, total int64, page, pageSize int) PaginatedResponse { + totalPages := (total + int64(pageSize) - 1) / int64(pageSize) + return PaginatedResponse{ + Items: items, + Total: total, + Page: page, + PageSize: pageSize, + TotalPages: totalPages, + } +} + +// AuthorInfo holds basic author info for responses +type AuthorInfo struct { + ID string `json:"id"` + Nickname string `json:"nickname"` + AvatarURL *string `json:"avatar_url"` + Role string `json:"role"` + IsCertified bool `json:"is_certified"` + CertificationTitle *string `json:"certification_title"` +} + +// ErrorResponse is a standard error response +type ErrorResponse struct { + Error string `json:"error"` + Detail interface{} `json:"detail,omitempty"` +} + +// SuccessResponse is a standard success response +type SuccessResponse struct { + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` +} + +// TimestampResponse used for items with timestamps +type TimestampResponse struct { + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/backend/internal/dto/echo.go b/backend/internal/dto/echo.go new file mode 100644 index 00000000..e256c031 --- /dev/null +++ b/backend/internal/dto/echo.go @@ -0,0 +1,28 @@ +package dto + +import "github.com/momshell/backend/internal/model" + +type IdentityTagCreateRequest struct { + TagType string `json:"tag_type" binding:"required,oneof=music sound literature memory"` + Content string `json:"content" binding:"required,min=1,max=200"` +} + +type IdentityTagListResponse struct { + Music []model.IdentityTag `json:"music"` + Sound []model.IdentityTag `json:"sound"` + Literature []model.IdentityTag `json:"literature"` + Memory []model.IdentityTag `json:"memory"` +} + +type GenerateMemoirRequest struct { + Theme *string `json:"theme" binding:"omitempty,max=100"` +} + +type MemoirListResponse struct { + Memoirs []model.Memoir `json:"memoirs"` + Total int64 `json:"total"` +} + +type RateMemoirRequest struct { + Rating int `json:"rating" binding:"required,min=1,max=5"` +} diff --git a/backend/internal/dto/interaction.go b/backend/internal/dto/interaction.go new file mode 100644 index 00000000..229f4d14 --- /dev/null +++ b/backend/internal/dto/interaction.go @@ -0,0 +1,41 @@ +package dto + +// LikeRequest is the request body for liking content +type LikeRequest struct { + TargetType string `json:"target_type" binding:"required,oneof=question answer comment"` + TargetID string `json:"target_id" binding:"required"` +} + +// LikeResponse is the response for like operations +type LikeResponse struct { + IsLiked bool `json:"is_liked"` + NewCount int `json:"new_count"` +} + +// CollectionRequest is the request body for collecting a question +type CollectionRequest struct { + QuestionID string `json:"question_id" binding:"required"` + FolderName *string `json:"folder_name"` + Note *string `json:"note"` +} + +// CollectionResponse is the response for collection operations +type CollectionResponse struct { + IsCollected bool `json:"is_collected"` + NewCount int `json:"new_count"` +} + +// CollectionItem is a single collection item +type CollectionItem struct { + ID string `json:"id"` + Question QuestionListItem `json:"question"` + FolderName *string `json:"folder_name"` + Note *string `json:"note"` + CreatedAt interface{} `json:"created_at"` +} + +// InteractionStatus is the response for interaction status check +type InteractionStatus struct { + IsLiked bool `json:"is_liked"` + IsCollected bool `json:"is_collected"` +} diff --git a/backend/internal/dto/question.go b/backend/internal/dto/question.go new file mode 100644 index 00000000..a82e29e0 --- /dev/null +++ b/backend/internal/dto/question.go @@ -0,0 +1,108 @@ +package dto + +import "time" + +// QuestionCreate is the request body for creating a question +type QuestionCreate struct { + Title string `json:"title" binding:"required,min=1,max=200"` + Content string `json:"content" binding:"required,min=1"` + Channel string `json:"channel" binding:"required,oneof=professional experience"` + ImageURLs []string `json:"image_urls"` + TagIDs []string `json:"tag_ids"` +} + +// QuestionUpdate is the request body for updating a question +type QuestionUpdate struct { + Title *string `json:"title" binding:"omitempty,min=1,max=200"` + Content *string `json:"content" binding:"omitempty,min=1"` + TagIDs []string `json:"tag_ids"` +} + +// QuestionListParams holds query parameters for listing questions +type QuestionListParams struct { + PaginationParams + Channel string `form:"channel" binding:"omitempty,oneof=professional experience"` + TagID string `form:"tag_id"` + SortBy string `form:"sort_by" binding:"omitempty,oneof=created_at view_count answer_count like_count"` + Order string `form:"order" binding:"omitempty,oneof=asc desc"` +} + +func (p *QuestionListParams) GetSortBy() string { + if p.SortBy == "" { + return "created_at" + } + return p.SortBy +} + +func (p *QuestionListParams) GetOrder() string { + if p.Order == "" { + return "desc" + } + return p.Order +} + +// QuestionListItem is a single question in list responses +type QuestionListItem struct { + ID string `json:"id"` + Title string `json:"title"` + ContentPreview string `json:"content_preview"` + Channel string `json:"channel"` + Author AuthorInfo `json:"author"` + Tags []TagInfo `json:"tags"` + ViewCount int `json:"view_count"` + AnswerCount int `json:"answer_count"` + LikeCount int `json:"like_count"` + CollectionCount int `json:"collection_count"` + IsPinned bool `json:"is_pinned"` + IsFeatured bool `json:"is_featured"` + HasAcceptedAnswer bool `json:"has_accepted_answer"` + IsLiked bool `json:"is_liked"` + IsCollected bool `json:"is_collected"` + CreatedAt time.Time `json:"created_at"` +} + +// QuestionDetail is the full question detail +type QuestionDetail struct { + ID string `json:"id"` + Title string `json:"title"` + Content string `json:"content"` + ContentPreview string `json:"content_preview"` + Channel string `json:"channel"` + Status string `json:"status"` + Author AuthorInfo `json:"author"` + Tags []TagInfo `json:"tags"` + ImageURLs []string `json:"image_urls"` + ViewCount int `json:"view_count"` + AnswerCount int `json:"answer_count"` + LikeCount int `json:"like_count"` + CollectionCount int `json:"collection_count"` + IsPinned bool `json:"is_pinned"` + IsFeatured bool `json:"is_featured"` + HasAcceptedAnswer bool `json:"has_accepted_answer"` + AcceptedAnswerID *string `json:"accepted_answer_id"` + IsLiked bool `json:"is_liked"` + IsCollected bool `json:"is_collected"` + ProfessionalAnswerCount int `json:"professional_answer_count"` + ExperienceAnswerCount int `json:"experience_answer_count"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + PublishedAt *time.Time `json:"published_at"` +} + +// MyQuestionListItem is for the user's own questions list (includes status) +type MyQuestionListItem struct { + ID string `json:"id"` + Title string `json:"title"` + ContentPreview string `json:"content_preview"` + Channel string `json:"channel"` + Tags []TagInfo `json:"tags"` + ViewCount int `json:"view_count"` + AnswerCount int `json:"answer_count"` + LikeCount int `json:"like_count"` + CollectionCount int `json:"collection_count"` + Status string `json:"status"` + HasAcceptedAnswer bool `json:"has_accepted_answer"` + IsLiked bool `json:"is_liked"` + IsCollected bool `json:"is_collected"` + CreatedAt time.Time `json:"created_at"` +} diff --git a/backend/internal/dto/tag.go b/backend/internal/dto/tag.go new file mode 100644 index 00000000..71e3a6f5 --- /dev/null +++ b/backend/internal/dto/tag.go @@ -0,0 +1,34 @@ +package dto + +// TagCreate is the request body for creating a tag +type TagCreate struct { + Name string `json:"name" binding:"required,min=1,max=50"` + Slug string `json:"slug" binding:"required,min=1,max=50"` + Description *string `json:"description"` +} + +// TagUpdate is the request body for updating a tag +type TagUpdate struct { + Name *string `json:"name" binding:"omitempty,min=1,max=50"` + Slug *string `json:"slug" binding:"omitempty,min=1,max=50"` + Description *string `json:"description"` +} + +// TagInfo is a brief tag info for embedding in other responses +type TagInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` +} + +// TagListItem is a single tag in list responses +type TagListItem struct { + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + Description *string `json:"description"` + QuestionCount int `json:"question_count"` + FollowerCount int `json:"follower_count"` + IsActive bool `json:"is_active"` + IsFeatured bool `json:"is_featured"` +} diff --git a/backend/internal/dto/user.go b/backend/internal/dto/user.go new file mode 100644 index 00000000..154546e8 --- /dev/null +++ b/backend/internal/dto/user.go @@ -0,0 +1,32 @@ +package dto + +import "time" + +// UserProfile is the response for user profile +type UserProfile struct { + ID string `json:"id"` + Nickname string `json:"nickname"` + Email string `json:"email"` + AvatarURL *string `json:"avatar_url"` + Role string `json:"role"` + IsCertified bool `json:"is_certified"` + CertificationTitle *string `json:"certification_title"` + Stats UserStats `json:"stats"` + CreatedAt time.Time `json:"created_at"` +} + +// UserStats holds user statistics +type UserStats struct { + QuestionCount int `json:"question_count"` + AnswerCount int `json:"answer_count"` + LikeReceivedCount int `json:"like_received_count"` + CollectionCount int `json:"collection_count"` +} + +// UserProfileUpdate is the request body for updating user profile +type UserProfileUpdate struct { + Nickname *string `json:"nickname" binding:"omitempty,min=1,max=50"` + Email *string `json:"email" binding:"omitempty,email"` + AvatarURL *string `json:"avatar_url"` + Role *string `json:"role" binding:"omitempty,oneof=mom dad family"` +} diff --git a/backend/internal/handler/admin.go b/backend/internal/handler/admin.go new file mode 100644 index 00000000..23960be0 --- /dev/null +++ b/backend/internal/handler/admin.go @@ -0,0 +1,193 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/momshell/backend/internal/admin" + "github.com/momshell/backend/internal/dto" + "github.com/momshell/backend/internal/middleware" + "github.com/momshell/backend/internal/model" + "github.com/momshell/backend/internal/service" +) + +type AdminHandler struct { + adminService *service.AdminService + authService *service.AuthService +} + +func NewAdminHandler(adminService *service.AdminService, authService *service.AuthService) *AdminHandler { + return &AdminHandler{ + adminService: adminService, + authService: authService, + } +} + +// requireAdmin checks if the current user is an admin +func (h *AdminHandler) requireAdmin(c *gin.Context) (string, bool) { + userID := middleware.GetUserID(c) + if userID == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"}) + return "", false + } + + user, err := h.authService.GetUserByID(userID) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "用户不存在"}) + return "", false + } + + if user.Role != model.RoleAdmin { + c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"}) + return "", false + } + + return userID, true +} + +// ServeAdminPage returns the embedded admin HTML page +func (h *AdminHandler) ServeAdminPage(c *gin.Context) { + c.Data(http.StatusOK, "text/html; charset=utf-8", admin.HTML) +} + +// GetStats returns dashboard statistics +func (h *AdminHandler) GetStats(c *gin.Context) { + if _, ok := h.requireAdmin(c); !ok { + return + } + + stats, err := h.adminService.GetDashboardStats() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, stats) +} + +// ListUsers returns paginated user list +func (h *AdminHandler) ListUsers(c *gin.Context) { + if _, ok := h.requireAdmin(c); !ok { + return + } + + var params dto.AdminUserListParams + if err := c.ShouldBindQuery(¶ms); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) + return + } + + resp, err := h.adminService.ListUsers(params) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, resp) +} + +// GetUser returns a single user's detail +func (h *AdminHandler) GetUser(c *gin.Context) { + if _, ok := h.requireAdmin(c); !ok { + return + } + + id := c.Param("id") + detail, err := h.adminService.GetUser(id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, detail) +} + +// CreateUser creates a new user +func (h *AdminHandler) CreateUser(c *gin.Context) { + if _, ok := h.requireAdmin(c); !ok { + return + } + + var req dto.AdminCreateUser + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: " + err.Error()}) + return + } + + detail, err := h.adminService.CreateUser(req) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusCreated, detail) +} + +// UpdateUser updates user fields +func (h *AdminHandler) UpdateUser(c *gin.Context) { + adminID, ok := h.requireAdmin(c) + if !ok { + return + } + + id := c.Param("id") + var req dto.AdminUserUpdate + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: " + err.Error()}) + return + } + + detail, err := h.adminService.UpdateUser(id, adminID, req) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, detail) +} + +// DeleteUser deletes a user +func (h *AdminHandler) DeleteUser(c *gin.Context) { + adminID, ok := h.requireAdmin(c) + if !ok { + return + } + + id := c.Param("id") + if err := h.adminService.DeleteUser(id, adminID); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "用户已删除"}) +} + +// GetConfig returns configuration items +func (h *AdminHandler) GetConfig(c *gin.Context) { + if _, ok := h.requireAdmin(c); !ok { + return + } + + items := h.adminService.GetConfig() + c.JSON(http.StatusOK, gin.H{"items": items}) +} + +// UpdateConfig updates editable configuration items +func (h *AdminHandler) UpdateConfig(c *gin.Context) { + if _, ok := h.requireAdmin(c); !ok { + return + } + + var req dto.ConfigUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: " + err.Error()}) + return + } + + if err := h.adminService.UpdateConfig(req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "配置已更新"}) +} diff --git a/backend/internal/handler/answer.go b/backend/internal/handler/answer.go new file mode 100644 index 00000000..9239a24a --- /dev/null +++ b/backend/internal/handler/answer.go @@ -0,0 +1,128 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/momshell/backend/internal/dto" + "github.com/momshell/backend/internal/middleware" + "github.com/momshell/backend/internal/service" +) + +type AnswerHandler struct { + communityService *service.CommunityService + authService *service.AuthService +} + +func NewAnswerHandler(communityService *service.CommunityService, authService *service.AuthService) *AnswerHandler { + return &AnswerHandler{communityService: communityService, authService: authService} +} + +// GET /api/v1/community/questions/:id/answers +func (h *AnswerHandler) List(c *gin.Context) { + questionID := c.Param("id") + + var params dto.AnswerListParams + if err := c.ShouldBindQuery(¶ms); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + userID := middleware.GetUserID(c) + resp, err := h.communityService.GetAnswers(questionID, params, userID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, resp) +} + +// POST /api/v1/community/questions/:id/answers +func (h *AnswerHandler) Create(c *gin.Context) { + questionID := c.Param("id") + + var req dto.AnswerCreate + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + userID := middleware.GetUserID(c) + user, err := h.authService.GetUserByID(userID) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "用户不存在"}) + return + } + + answer, err := h.communityService.CreateAnswer(questionID, req, user) + if err != nil { + status := http.StatusBadRequest + if err.Error() == "问题不存在" { + status = http.StatusNotFound + } else if err.Error() == "专业频道仅限认证专业人士回答" { + status = http.StatusForbidden + } + c.JSON(status, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusCreated, gin.H{"id": answer.ID, "status": string(answer.Status)}) +} + +// PUT /api/v1/community/answers/:id +func (h *AnswerHandler) Update(c *gin.Context) { + answerID := c.Param("id") + + var req dto.AnswerUpdate + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + userID := middleware.GetUserID(c) + user, err := h.authService.GetUserByID(userID) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "用户不存在"}) + return + } + + answer, err := h.communityService.UpdateAnswer(answerID, req, user) + if err != nil { + status := http.StatusBadRequest + if err.Error() == "回答不存在" { + status = http.StatusNotFound + } else if err.Error() == "无权修改此回答" { + status = http.StatusForbidden + } + c.JSON(status, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"id": answer.ID, "status": string(answer.Status)}) +} + +// DELETE /api/v1/community/answers/:id +func (h *AnswerHandler) Delete(c *gin.Context) { + answerID := c.Param("id") + + userID := middleware.GetUserID(c) + user, err := h.authService.GetUserByID(userID) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "用户不存在"}) + return + } + + if err := h.communityService.DeleteAnswer(answerID, user); err != nil { + status := http.StatusBadRequest + if err.Error() == "回答不存在" { + status = http.StatusNotFound + } else if err.Error() == "无权删除此回答" { + status = http.StatusForbidden + } + c.JSON(status, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "删除成功"}) +} diff --git a/backend/internal/handler/auth.go b/backend/internal/handler/auth.go new file mode 100644 index 00000000..25c4538c --- /dev/null +++ b/backend/internal/handler/auth.go @@ -0,0 +1,164 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/momshell/backend/internal/dto" + "github.com/momshell/backend/internal/middleware" + "github.com/momshell/backend/internal/service" +) + +type AuthHandler struct { + authService *service.AuthService +} + +func NewAuthHandler(authService *service.AuthService) *AuthHandler { + return &AuthHandler{authService: authService} +} + +// POST /api/v1/auth/register +func (h *AuthHandler) Register(c *gin.Context) { + var req dto.RegisterRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + resp, err := h.authService.Register(req) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusCreated, resp) +} + +// POST /api/v1/auth/login +func (h *AuthHandler) Login(c *gin.Context) { + var req dto.LoginRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + resp, err := h.authService.Login(req) + if err != nil { + status := http.StatusUnauthorized + if err.Error() == "账号已禁用" || err.Error() == "账号已被封禁" { + status = http.StatusForbidden + } + c.JSON(status, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, resp) +} + +// POST /api/v1/auth/refresh +func (h *AuthHandler) Refresh(c *gin.Context) { + var req dto.RefreshRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + resp, err := h.authService.RefreshToken(req.RefreshToken) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, resp) +} + +// GET /api/v1/auth/me +func (h *AuthHandler) GetMe(c *gin.Context) { + userID := middleware.GetUserID(c) + resp, err := h.authService.GetCurrentUser(userID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, resp) +} + +// POST /api/v1/auth/change-password +func (h *AuthHandler) ChangePassword(c *gin.Context) { + userID := middleware.GetUserID(c) + + var req dto.ChangePasswordRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := h.authService.ChangePassword(userID, req.OldPassword, req.NewPassword); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "密码修改成功"}) +} + +// POST /api/v1/auth/forgot-password +func (h *AuthHandler) ForgotPassword(c *gin.Context) { + var req dto.ForgotPasswordRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + token, err := h.authService.ForgotPassword(req.Email) + if err != nil { + // Don't reveal if user doesn't exist + c.JSON(http.StatusOK, gin.H{"message": "如果该邮箱已注册,将收到重置密码邮件"}) + return + } + + // In production, send email instead of returning token + c.JSON(http.StatusOK, gin.H{ + "message": "如果该邮箱已注册,将收到重置密码邮件", + "token": token, // For development only + }) +} + +// POST /api/v1/auth/reset-password +func (h *AuthHandler) ResetPassword(c *gin.Context) { + var req dto.ResetPasswordRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := h.authService.ResetPassword(req.Token, req.NewPassword); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "密码重置成功"}) +} + +// PATCH /api/v1/auth/me/role +func (h *AuthHandler) UpdateRole(c *gin.Context) { + userID := middleware.GetUserID(c) + + var req dto.UpdateRoleRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + resp, err := h.authService.UpdateRole(userID, req.Role) + if err != nil { + status := http.StatusBadRequest + if err.Error() == "认证专业人员不能修改角色" || err.Error() == "管理员不能通过此接口修改角色" { + status = http.StatusForbidden + } + c.JSON(status, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, resp) +} diff --git a/backend/internal/handler/chat.go b/backend/internal/handler/chat.go new file mode 100644 index 00000000..93ea2a8f --- /dev/null +++ b/backend/internal/handler/chat.go @@ -0,0 +1,61 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/momshell/backend/internal/dto" + "github.com/momshell/backend/internal/middleware" + "github.com/momshell/backend/internal/service" +) + +type ChatHandler struct { + chatService *service.ChatService +} + +func NewChatHandler(chatService *service.ChatService) *ChatHandler { + return &ChatHandler{chatService: chatService} +} + +// POST /api/v1/companion/chat +func (h *ChatHandler) Chat(c *gin.Context) { + var req dto.UserMessage + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + userID := middleware.GetUserID(c) + resp, err := h.chatService.Chat(c.Request.Context(), req, userID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, resp) +} + +// GET /api/v1/companion/profile +func (h *ChatHandler) GetProfile(c *gin.Context) { + userID := middleware.GetUserID(c) + + if userID != "" { + profile, err := h.chatService.GetProfile(userID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, profile) + return + } + + // Guest - check session_id query param + sessionID := c.Query("session_id") + if sessionID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "需要 session_id 或登录"}) + return + } + + profile := h.chatService.GetGuestProfile(sessionID) + c.JSON(http.StatusOK, profile) +} diff --git a/backend/internal/handler/comment.go b/backend/internal/handler/comment.go new file mode 100644 index 00000000..02dec0cd --- /dev/null +++ b/backend/internal/handler/comment.go @@ -0,0 +1,88 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/momshell/backend/internal/dto" + "github.com/momshell/backend/internal/middleware" + "github.com/momshell/backend/internal/service" +) + +type CommentHandler struct { + communityService *service.CommunityService + authService *service.AuthService +} + +func NewCommentHandler(communityService *service.CommunityService, authService *service.AuthService) *CommentHandler { + return &CommentHandler{communityService: communityService, authService: authService} +} + +// GET /api/v1/community/answers/:id/comments +func (h *CommentHandler) List(c *gin.Context) { + answerID := c.Param("id") + userID := middleware.GetUserID(c) + + comments, err := h.communityService.GetComments(answerID, userID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, comments) +} + +// POST /api/v1/community/answers/:id/comments +func (h *CommentHandler) Create(c *gin.Context) { + answerID := c.Param("id") + + var req dto.CommentCreate + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + userID := middleware.GetUserID(c) + user, err := h.authService.GetUserByID(userID) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "用户不存在"}) + return + } + + comment, err := h.communityService.CreateComment(answerID, req, user) + if err != nil { + status := http.StatusBadRequest + if err.Error() == "回答不存在" || err.Error() == "回复目标不存在" { + status = http.StatusNotFound + } + c.JSON(status, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusCreated, comment) +} + +// DELETE /api/v1/community/comments/:id +func (h *CommentHandler) Delete(c *gin.Context) { + commentID := c.Param("id") + + userID := middleware.GetUserID(c) + user, err := h.authService.GetUserByID(userID) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "用户不存在"}) + return + } + + if err := h.communityService.DeleteComment(commentID, user); err != nil { + status := http.StatusBadRequest + if err.Error() == "评论不存在" { + status = http.StatusNotFound + } else if err.Error() == "无权删除此评论" { + status = http.StatusForbidden + } + c.JSON(status, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "删除成功"}) +} diff --git a/backend/internal/handler/echo.go b/backend/internal/handler/echo.go new file mode 100644 index 00000000..650ab62c --- /dev/null +++ b/backend/internal/handler/echo.go @@ -0,0 +1,139 @@ +package handler + +import ( + "errors" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/momshell/backend/internal/dto" + "github.com/momshell/backend/internal/middleware" + "github.com/momshell/backend/internal/service" + "gorm.io/gorm" +) + +type EchoHandler struct { + echoService *service.EchoService +} + +func NewEchoHandler(echoService *service.EchoService) *EchoHandler { + return &EchoHandler{echoService: echoService} +} + +func (h *EchoHandler) GetIdentityTags(c *gin.Context) { + userID := middleware.GetUserID(c) + + resp, err := h.echoService.GetIdentityTags(userID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, resp) +} + +func (h *EchoHandler) CreateIdentityTag(c *gin.Context) { + var req dto.IdentityTagCreateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + userID := middleware.GetUserID(c) + tag, err := h.echoService.CreateIdentityTag(userID, req) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, tag) +} + +func (h *EchoHandler) DeleteIdentityTag(c *gin.Context) { + userID := middleware.GetUserID(c) + tagID := c.Param("id") + + err := h.echoService.DeleteIdentityTag(userID, tagID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "identity tag not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.Status(http.StatusNoContent) +} + +func (h *EchoHandler) GetMemoirs(c *gin.Context) { + limit := 10 + offset := 0 + + if limitRaw := c.Query("limit"); limitRaw != "" { + parsed, err := strconv.Atoi(limitRaw) + if err != nil || parsed <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid limit"}) + return + } + limit = parsed + } + + if offsetRaw := c.Query("offset"); offsetRaw != "" { + parsed, err := strconv.Atoi(offsetRaw) + if err != nil || parsed < 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid offset"}) + return + } + offset = parsed + } + + userID := middleware.GetUserID(c) + resp, err := h.echoService.GetMemoirs(userID, limit, offset) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, resp) +} + +func (h *EchoHandler) GenerateMemoir(c *gin.Context) { + var req dto.GenerateMemoirRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + userID := middleware.GetUserID(c) + memoir, err := h.echoService.GenerateMemoir(c.Request.Context(), userID, req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, memoir) +} + +func (h *EchoHandler) RateMemoir(c *gin.Context) { + var req dto.RateMemoirRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + userID := middleware.GetUserID(c) + memoirID := c.Param("id") + + memoir, err := h.echoService.RateMemoir(userID, memoirID, req.Rating) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "memoir not found"}) + return + } + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, memoir) +} diff --git a/backend/internal/handler/interaction.go b/backend/internal/handler/interaction.go new file mode 100644 index 00000000..87eaea9c --- /dev/null +++ b/backend/internal/handler/interaction.go @@ -0,0 +1,105 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/momshell/backend/internal/dto" + "github.com/momshell/backend/internal/middleware" + "github.com/momshell/backend/internal/service" +) + +type InteractionHandler struct { + communityService *service.CommunityService +} + +func NewInteractionHandler(communityService *service.CommunityService) *InteractionHandler { + return &InteractionHandler{communityService: communityService} +} + +// POST /api/v1/community/likes +func (h *InteractionHandler) CreateLike(c *gin.Context) { + var req dto.LikeRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + userID := middleware.GetUserID(c) + isLiked, newCount, err := h.communityService.ToggleLike(userID, req.TargetType, req.TargetID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, dto.LikeResponse{IsLiked: isLiked, NewCount: newCount}) +} + +// DELETE /api/v1/community/likes +func (h *InteractionHandler) DeleteLike(c *gin.Context) { + var req dto.LikeRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + userID := middleware.GetUserID(c) + isLiked, newCount, err := h.communityService.ToggleLike(userID, req.TargetType, req.TargetID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, dto.LikeResponse{IsLiked: isLiked, NewCount: newCount}) +} + +// POST /api/v1/community/collections +func (h *InteractionHandler) CreateCollection(c *gin.Context) { + var req dto.CollectionRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + userID := middleware.GetUserID(c) + isCollected, newCount, err := h.communityService.ToggleCollection(userID, req.QuestionID, req.FolderName, req.Note) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, dto.CollectionResponse{IsCollected: isCollected, NewCount: newCount}) +} + +// DELETE /api/v1/community/collections/:id +func (h *InteractionHandler) DeleteCollection(c *gin.Context) { + questionID := c.Param("id") + userID := middleware.GetUserID(c) + + isCollected, newCount, err := h.communityService.ToggleCollection(userID, questionID, nil, nil) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, dto.CollectionResponse{IsCollected: isCollected, NewCount: newCount}) +} + +// GET /api/v1/community/collections/my +func (h *InteractionHandler) GetMyCollections(c *gin.Context) { + userID := middleware.GetUserID(c) + + var params dto.PaginationParams + if err := c.ShouldBindQuery(¶ms); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + resp, err := h.communityService.GetUserCollections(userID, params.GetPage(), params.GetPageSize()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, resp) +} diff --git a/backend/internal/handler/question.go b/backend/internal/handler/question.go new file mode 100644 index 00000000..ad396881 --- /dev/null +++ b/backend/internal/handler/question.go @@ -0,0 +1,183 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/momshell/backend/internal/dto" + "github.com/momshell/backend/internal/middleware" + "github.com/momshell/backend/internal/service" +) + +type QuestionHandler struct { + communityService *service.CommunityService + authService *service.AuthService +} + +func NewQuestionHandler(communityService *service.CommunityService, authService *service.AuthService) *QuestionHandler { + return &QuestionHandler{communityService: communityService, authService: authService} +} + +// GET /api/v1/community/questions +func (h *QuestionHandler) List(c *gin.Context) { + var params dto.QuestionListParams + if err := c.ShouldBindQuery(¶ms); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + userID := middleware.GetUserID(c) + resp, err := h.communityService.GetQuestions(params, userID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, resp) +} + +// GET /api/v1/community/questions/hot +func (h *QuestionHandler) ListHot(c *gin.Context) { + params := dto.QuestionListParams{} + params.SortBy = "view_count" + params.Order = "desc" + if err := c.ShouldBindQuery(¶ms); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + params.SortBy = "view_count" + params.Order = "desc" + + userID := middleware.GetUserID(c) + resp, err := h.communityService.GetQuestions(params, userID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, resp) +} + +// GET /api/v1/community/questions/channel/:channel +func (h *QuestionHandler) ListByChannel(c *gin.Context) { + channel := c.Param("channel") + if channel != "professional" && channel != "experience" { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的频道"}) + return + } + + var params dto.QuestionListParams + if err := c.ShouldBindQuery(¶ms); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + params.Channel = channel + + userID := middleware.GetUserID(c) + resp, err := h.communityService.GetQuestions(params, userID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, resp) +} + +// GET /api/v1/community/questions/:id +func (h *QuestionHandler) Get(c *gin.Context) { + id := c.Param("id") + userID := middleware.GetUserID(c) + + resp, err := h.communityService.GetQuestion(id, userID) + if err != nil { + if err.Error() == "问题不存在" { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, resp) +} + +// POST /api/v1/community/questions +func (h *QuestionHandler) Create(c *gin.Context) { + var req dto.QuestionCreate + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + userID := middleware.GetUserID(c) + user, err := h.authService.GetUserByID(userID) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "用户不存在"}) + return + } + + question, err := h.communityService.CreateQuestion(req, user) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusCreated, gin.H{"id": question.ID, "status": string(question.Status)}) +} + +// PUT /api/v1/community/questions/:id +func (h *QuestionHandler) Update(c *gin.Context) { + id := c.Param("id") + + var req dto.QuestionUpdate + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + userID := middleware.GetUserID(c) + user, err := h.authService.GetUserByID(userID) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "用户不存在"}) + return + } + + question, err := h.communityService.UpdateQuestion(id, req, user) + if err != nil { + status := http.StatusBadRequest + if err.Error() == "问题不存在" { + status = http.StatusNotFound + } else if err.Error() == "无权修改此问题" { + status = http.StatusForbidden + } + c.JSON(status, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"id": question.ID, "status": string(question.Status)}) +} + +// DELETE /api/v1/community/questions/:id +func (h *QuestionHandler) Delete(c *gin.Context) { + id := c.Param("id") + + userID := middleware.GetUserID(c) + user, err := h.authService.GetUserByID(userID) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "用户不存在"}) + return + } + + if err := h.communityService.DeleteQuestion(id, user); err != nil { + status := http.StatusBadRequest + if err.Error() == "问题不存在" { + status = http.StatusNotFound + } else if err.Error() == "无权删除此问题" { + status = http.StatusForbidden + } + c.JSON(status, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "删除成功"}) +} diff --git a/backend/internal/handler/tag.go b/backend/internal/handler/tag.go new file mode 100644 index 00000000..769dad37 --- /dev/null +++ b/backend/internal/handler/tag.go @@ -0,0 +1,56 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/momshell/backend/internal/dto" + "github.com/momshell/backend/internal/service" +) + +type TagHandler struct { + communityService *service.CommunityService +} + +func NewTagHandler(communityService *service.CommunityService) *TagHandler { + return &TagHandler{communityService: communityService} +} + +// GET /api/v1/community/tags +func (h *TagHandler) List(c *gin.Context) { + tags, err := h.communityService.GetTags() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, tags) +} + +// GET /api/v1/community/tags/hot +func (h *TagHandler) ListHot(c *gin.Context) { + tags, err := h.communityService.GetHotTags(20) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, tags) +} + +// POST /api/v1/community/tags (admin only) +func (h *TagHandler) Create(c *gin.Context) { + var req dto.TagCreate + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + tag, err := h.communityService.CreateTag(req) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusCreated, gin.H{"id": tag.ID, "name": tag.Name, "slug": tag.Slug}) +} diff --git a/backend/internal/handler/user.go b/backend/internal/handler/user.go new file mode 100644 index 00000000..1108af8a --- /dev/null +++ b/backend/internal/handler/user.go @@ -0,0 +1,87 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/momshell/backend/internal/dto" + "github.com/momshell/backend/internal/middleware" + "github.com/momshell/backend/internal/service" +) + +type UserHandler struct { + userService *service.UserService +} + +func NewUserHandler(userService *service.UserService) *UserHandler { + return &UserHandler{userService: userService} +} + +// GET /api/v1/community/users/me +func (h *UserHandler) GetMe(c *gin.Context) { + userID := middleware.GetUserID(c) + profile, err := h.userService.GetProfile(userID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, profile) +} + +// PUT /api/v1/community/users/me +func (h *UserHandler) UpdateMe(c *gin.Context) { + userID := middleware.GetUserID(c) + + var req dto.UserProfileUpdate + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + profile, err := h.userService.UpdateProfile(userID, req) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, profile) +} + +// GET /api/v1/community/users/me/questions +func (h *UserHandler) GetMyQuestions(c *gin.Context) { + userID := middleware.GetUserID(c) + + var params dto.PaginationParams + if err := c.ShouldBindQuery(¶ms); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + resp, err := h.userService.GetUserQuestions(userID, params.GetPage(), params.GetPageSize()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, resp) +} + +// GET /api/v1/community/users/me/answers +func (h *UserHandler) GetMyAnswers(c *gin.Context) { + userID := middleware.GetUserID(c) + + var params dto.PaginationParams + if err := c.ShouldBindQuery(¶ms); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + resp, err := h.userService.GetUserAnswers(userID, params.GetPage(), params.GetPageSize()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, resp) +} diff --git a/backend/internal/middleware/auth.go b/backend/internal/middleware/auth.go new file mode 100644 index 00000000..df9364e1 --- /dev/null +++ b/backend/internal/middleware/auth.go @@ -0,0 +1,99 @@ +package middleware + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/momshell/backend/internal/config" + pkgjwt "github.com/momshell/backend/pkg/jwt" +) + +const ( + ContextUserID = "user_id" +) + +// AuthRequired requires a valid JWT token +func AuthRequired(cfg *config.Config) gin.HandlerFunc { + return func(c *gin.Context) { + userID, err := extractUserID(c, cfg) + if err != nil || userID == "" { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未授权,请先登录"}) + return + } + c.Set(ContextUserID, userID) + c.Next() + } +} + +// AuthOptional extracts user ID if token is present, but does not require it +func AuthOptional(cfg *config.Config) gin.HandlerFunc { + return func(c *gin.Context) { + userID, _ := extractUserID(c, cfg) + if userID != "" { + c.Set(ContextUserID, userID) + } + c.Next() + } +} + +// AdminRequired requires a valid JWT token and admin role +// Note: actual role check is done in handler with user lookup +func AdminRequired(cfg *config.Config) gin.HandlerFunc { + return func(c *gin.Context) { + userID, err := extractUserID(c, cfg) + if err != nil || userID == "" { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未授权,请先登录"}) + return + } + c.Set(ContextUserID, userID) + // Admin role check will be done in the handler after fetching the user + c.Next() + } +} + +func extractUserID(c *gin.Context, cfg *config.Config) (string, error) { + tokenStr := "" + + // 1. Authorization header + auth := c.GetHeader("Authorization") + if strings.HasPrefix(auth, "Bearer ") { + tokenStr = strings.TrimPrefix(auth, "Bearer ") + } + + // 2. X-Access-Token header + if tokenStr == "" { + tokenStr = c.GetHeader("X-Access-Token") + } + + // 3. Cookie + if tokenStr == "" { + if cookie, err := c.Cookie("access_token"); err == nil { + tokenStr = cookie + } + } + + if tokenStr == "" { + return "", nil + } + + claims, err := pkgjwt.ParseToken(tokenStr, cfg.JWTSecretKey) + if err != nil { + return "", err + } + + if claims.Type != "access" { + return "", pkgjwt.ErrInvalidToken + } + + return claims.Subject, nil +} + +// GetUserID extracts user ID from context (set by auth middleware) +func GetUserID(c *gin.Context) string { + id, _ := c.Get(ContextUserID) + if id == nil { + return "" + } + return id.(string) +} diff --git a/backend/internal/middleware/cors.go b/backend/internal/middleware/cors.go new file mode 100644 index 00000000..58a0a704 --- /dev/null +++ b/backend/internal/middleware/cors.go @@ -0,0 +1,19 @@ +package middleware + +import ( + "time" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +func CORS() gin.HandlerFunc { + return cors.New(cors.Config{ + AllowAllOrigins: true, + AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, + AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Access-Token"}, + ExposeHeaders: []string{"Content-Length"}, + AllowCredentials: true, + MaxAge: 12 * time.Hour, + }) +} diff --git a/backend/internal/middleware/recovery.go b/backend/internal/middleware/recovery.go new file mode 100644 index 00000000..cbd50d04 --- /dev/null +++ b/backend/internal/middleware/recovery.go @@ -0,0 +1,22 @@ +package middleware + +import ( + "log" + "net/http" + + "github.com/gin-gonic/gin" +) + +func Recovery() gin.HandlerFunc { + return func(c *gin.Context) { + defer func() { + if err := recover(); err != nil { + log.Printf("[PANIC] %v", err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ + "error": "服务器内部错误", + }) + } + }() + c.Next() + } +} diff --git a/backend/internal/model/chat.go b/backend/internal/model/chat.go new file mode 100644 index 00000000..e59abb23 --- /dev/null +++ b/backend/internal/model/chat.go @@ -0,0 +1,60 @@ +package model + +import ( + "encoding/json" + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type ChatMemory struct { + ID string `gorm:"type:varchar(36);primaryKey" json:"id"` + UserID string `gorm:"type:varchar(36);uniqueIndex;not null" json:"user_id"` + ProfileData string `gorm:"type:text" json:"profile_data"` // JSON + ConversationTurns string `gorm:"type:text" json:"conversation_turns"` // JSON array + + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + + User User `gorm:"foreignKey:UserID" json:"-"` +} + +func (m *ChatMemory) BeforeCreate(tx *gorm.DB) error { + if m.ID == "" { + m.ID = uuid.New().String() + } + return nil +} + +func (m *ChatMemory) GetProfile() map[string]interface{} { + if m.ProfileData == "" { + return make(map[string]interface{}) + } + var result map[string]interface{} + if err := json.Unmarshal([]byte(m.ProfileData), &result); err != nil { + return make(map[string]interface{}) + } + return result +} + +func (m *ChatMemory) SetProfile(data map[string]interface{}) { + b, _ := json.Marshal(data) + m.ProfileData = string(b) +} + +func (m *ChatMemory) GetTurns() []map[string]interface{} { + if m.ConversationTurns == "" { + return nil + } + var result []map[string]interface{} + if err := json.Unmarshal([]byte(m.ConversationTurns), &result); err != nil { + return nil + } + return result +} + +func (m *ChatMemory) SetTurns(turns []map[string]interface{}) { + b, _ := json.Marshal(turns) + m.ConversationTurns = string(b) +} diff --git a/backend/internal/model/community.go b/backend/internal/model/community.go new file mode 100644 index 00000000..dbeca4d0 --- /dev/null +++ b/backend/internal/model/community.go @@ -0,0 +1,148 @@ +package model + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +// ChannelType enum +type ChannelType string + +const ( + ChannelProfessional ChannelType = "professional" + ChannelExperience ChannelType = "experience" +) + +// ContentStatus enum +type ContentStatus string + +const ( + StatusDraft ContentStatus = "draft" + StatusPendingReview ContentStatus = "pending_review" + StatusPublished ContentStatus = "published" + StatusHidden ContentStatus = "hidden" + StatusDeleted ContentStatus = "deleted" +) + +type Question struct { + ID string `gorm:"type:varchar(36);primaryKey" json:"id"` + AuthorID string `gorm:"type:varchar(36);index;not null" json:"author_id"` + + // Content + Title string `gorm:"type:varchar(200);index;not null" json:"title"` + Content string `gorm:"type:text;not null" json:"content"` + ImageURLs *string `gorm:"type:text" json:"image_urls"` // JSON array + + // Channel + Channel ChannelType `gorm:"type:varchar(20);index;default:'experience'" json:"channel"` + + // Status + Status ContentStatus `gorm:"type:varchar(20);index;default:'pending_review'" json:"status"` + + // Statistics + ViewCount int `gorm:"default:0" json:"view_count"` + AnswerCount int `gorm:"default:0" json:"answer_count"` + LikeCount int `gorm:"default:0" json:"like_count"` + CollectionCount int `gorm:"default:0" json:"collection_count"` + + // Flags + IsPinned bool `gorm:"default:false" json:"is_pinned"` + IsFeatured bool `gorm:"default:false" json:"is_featured"` + + // Accepted answer + AcceptedAnswerID *string `gorm:"type:varchar(36)" json:"accepted_answer_id"` + + // Timestamps + CreatedAt time.Time `gorm:"index" json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + PublishedAt *time.Time `json:"published_at"` + + // Relationships + Author User `gorm:"foreignKey:AuthorID" json:"author,omitempty"` + Answers []Answer `gorm:"foreignKey:QuestionID" json:"answers,omitempty"` + Tags []Tag `gorm:"many2many:question_tags" json:"tags,omitempty"` +} + +func (q *Question) BeforeCreate(tx *gorm.DB) error { + if q.ID == "" { + q.ID = uuid.New().String() + } + return nil +} + +type Answer struct { + ID string `gorm:"type:varchar(36);primaryKey" json:"id"` + QuestionID string `gorm:"type:varchar(36);index;not null" json:"question_id"` + AuthorID string `gorm:"type:varchar(36);index;not null" json:"author_id"` + + // Content + Content string `gorm:"type:text;not null" json:"content"` + ImageURLs *string `gorm:"type:text" json:"image_urls"` + + // Author role info + AuthorRole UserRole `gorm:"type:varchar(30);index" json:"author_role"` + IsProfessional bool `gorm:"index;default:false" json:"is_professional"` + + // Status + Status ContentStatus `gorm:"type:varchar(20);index;default:'pending_review'" json:"status"` + + // Statistics + LikeCount int `gorm:"default:0" json:"like_count"` + CommentCount int `gorm:"default:0" json:"comment_count"` + + // Accepted + IsAccepted bool `gorm:"default:false" json:"is_accepted"` + + // Timestamps + CreatedAt time.Time `gorm:"index" json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + + // Relationships + Author User `gorm:"foreignKey:AuthorID" json:"author,omitempty"` + Question Question `gorm:"foreignKey:QuestionID" json:"-"` + Comments []Comment `gorm:"foreignKey:AnswerID" json:"comments,omitempty"` +} + +func (a *Answer) BeforeCreate(tx *gorm.DB) error { + if a.ID == "" { + a.ID = uuid.New().String() + } + return nil +} + +type Comment struct { + ID string `gorm:"type:varchar(36);primaryKey" json:"id"` + AnswerID string `gorm:"type:varchar(36);index;not null" json:"answer_id"` + AuthorID string `gorm:"type:varchar(36);index;not null" json:"author_id"` + + // Nested reply + ParentID *string `gorm:"type:varchar(36)" json:"parent_id"` + ReplyToUserID *string `gorm:"type:varchar(36)" json:"reply_to_user_id"` + + // Content + Content string `gorm:"type:text;not null" json:"content"` + + // Status + Status ContentStatus `gorm:"type:varchar(20);default:'pending_review'" json:"status"` + + // Statistics + LikeCount int `gorm:"default:0" json:"like_count"` + + // Timestamps + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + + // Relationships + Author User `gorm:"foreignKey:AuthorID" json:"author,omitempty"` + Answer Answer `gorm:"foreignKey:AnswerID" json:"-"` + ReplyToUser *User `gorm:"foreignKey:ReplyToUserID" json:"reply_to_user,omitempty"` +} + +func (c *Comment) BeforeCreate(tx *gorm.DB) error { + if c.ID == "" { + c.ID = uuid.New().String() + } + return nil +} diff --git a/backend/internal/model/echo.go b/backend/internal/model/echo.go new file mode 100644 index 00000000..b98c7c9c --- /dev/null +++ b/backend/internal/model/echo.go @@ -0,0 +1,46 @@ +package model + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type IdentityTag struct { + ID string `gorm:"type:varchar(36);primaryKey" json:"id"` + UserID string `gorm:"type:varchar(36);index;not null" json:"-"` + TagType string `gorm:"type:varchar(20);not null" json:"tag_type"` + Content string `gorm:"type:varchar(200);not null" json:"content"` + CreatedAt time.Time `json:"created_at"` + + User User `gorm:"foreignKey:UserID" json:"-"` +} + +func (t *IdentityTag) BeforeCreate(tx *gorm.DB) error { + if t.ID == "" { + t.ID = uuid.New().String() + } + return nil +} + +type Memoir struct { + ID string `gorm:"type:varchar(36);primaryKey" json:"id"` + UserID string `gorm:"type:varchar(36);index;not null" json:"-"` + Title string `gorm:"type:varchar(200);not null" json:"title"` + Content string `gorm:"type:text;not null" json:"content"` + CoverImageURL *string `gorm:"type:text" json:"cover_image_url"` + Theme *string `gorm:"type:varchar(100)" json:"-"` + UserRating *int `gorm:"type:int" json:"user_rating"` + CreatedAt time.Time `gorm:"index" json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + + User User `gorm:"foreignKey:UserID" json:"-"` +} + +func (m *Memoir) BeforeCreate(tx *gorm.DB) error { + if m.ID == "" { + m.ID = uuid.New().String() + } + return nil +} diff --git a/backend/internal/model/interaction.go b/backend/internal/model/interaction.go new file mode 100644 index 00000000..d818caec --- /dev/null +++ b/backend/internal/model/interaction.go @@ -0,0 +1,79 @@ +package model + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type Like struct { + ID string `gorm:"type:varchar(36);primaryKey" json:"id"` + UserID string `gorm:"type:varchar(36);index;not null" json:"user_id"` + TargetType string `gorm:"type:varchar(20);index;not null" json:"target_type"` // question, answer, comment + TargetID string `gorm:"type:varchar(36);index;not null" json:"target_id"` + CreatedAt time.Time `json:"created_at"` + + // Relationships + User User `gorm:"foreignKey:UserID" json:"-"` +} + +func (l *Like) BeforeCreate(tx *gorm.DB) error { + if l.ID == "" { + l.ID = uuid.New().String() + } + return nil +} + +// TableName overrides +func (Like) TableName() string { return "likes" } + +type Collection struct { + ID string `gorm:"type:varchar(36);primaryKey" json:"id"` + UserID string `gorm:"type:varchar(36);index;not null" json:"user_id"` + QuestionID string `gorm:"type:varchar(36);index;not null" json:"question_id"` + FolderName *string `gorm:"type:varchar(50)" json:"folder_name"` + Note *string `gorm:"type:varchar(500)" json:"note"` + CreatedAt time.Time `json:"created_at"` + + // Relationships + User User `gorm:"foreignKey:UserID" json:"-"` + Question Question `gorm:"foreignKey:QuestionID" json:"question,omitempty"` +} + +func (c *Collection) BeforeCreate(tx *gorm.DB) error { + if c.ID == "" { + c.ID = uuid.New().String() + } + return nil +} + +// ModerationResult enum +type ModerationResult string + +const ( + ModerationPassed ModerationResult = "passed" + ModerationRejected ModerationResult = "rejected" + ModerationNeedManualReview ModerationResult = "need_manual_review" +) + +type ModerationLog struct { + ID string `gorm:"type:varchar(36);primaryKey" json:"id"` + TargetType string `gorm:"type:varchar(20);index;not null" json:"target_type"` + TargetID string `gorm:"type:varchar(36);index;not null" json:"target_id"` + ModerationType string `gorm:"type:varchar(20);not null" json:"moderation_type"` // auto, manual + Result ModerationResult `gorm:"type:varchar(30);not null" json:"result"` + SensitiveCategories *string `gorm:"type:text" json:"sensitive_categories"` // JSON array + ConfidenceScore *float64 `json:"confidence_score"` + Reason *string `gorm:"type:text" json:"reason"` + OriginalContent *string `gorm:"type:text" json:"original_content"` + ReviewerID *string `gorm:"type:varchar(36)" json:"reviewer_id"` + CreatedAt time.Time `json:"created_at"` +} + +func (m *ModerationLog) BeforeCreate(tx *gorm.DB) error { + if m.ID == "" { + m.ID = uuid.New().String() + } + return nil +} diff --git a/backend/internal/model/tag.go b/backend/internal/model/tag.go new file mode 100644 index 00000000..053a7ef0 --- /dev/null +++ b/backend/internal/model/tag.go @@ -0,0 +1,43 @@ +package model + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type Tag struct { + ID string `gorm:"type:varchar(36);primaryKey" json:"id"` + Name string `gorm:"type:varchar(50);uniqueIndex;not null" json:"name"` + Slug string `gorm:"type:varchar(50);uniqueIndex;not null" json:"slug"` + Description *string `gorm:"type:varchar(200)" json:"description"` + + // Statistics + QuestionCount int `gorm:"default:0" json:"question_count"` + FollowerCount int `gorm:"default:0" json:"follower_count"` + + // Status + IsActive bool `gorm:"default:true" json:"is_active"` + IsFeatured bool `gorm:"default:false" json:"is_featured"` + + CreatedAt time.Time `json:"created_at"` + + // Relationships + Questions []Question `gorm:"many2many:question_tags" json:"questions,omitempty"` +} + +func (t *Tag) BeforeCreate(tx *gorm.DB) error { + if t.ID == "" { + t.ID = uuid.New().String() + } + return nil +} + +type QuestionTag struct { + QuestionID string `gorm:"type:varchar(36);primaryKey" json:"question_id"` + TagID string `gorm:"type:varchar(36);primaryKey" json:"tag_id"` + CreatedAt time.Time `json:"created_at"` +} + +func (QuestionTag) TableName() string { return "question_tags" } diff --git a/backend/internal/model/user.go b/backend/internal/model/user.go new file mode 100644 index 00000000..c9595944 --- /dev/null +++ b/backend/internal/model/user.go @@ -0,0 +1,125 @@ +package model + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +// UserRole enum +type UserRole string + +const ( + RoleGuest UserRole = "guest" + RoleMom UserRole = "mom" + RoleDad UserRole = "dad" + RoleFamily UserRole = "family" + RoleCertifiedDoctor UserRole = "certified_doctor" + RoleCertifiedTherapist UserRole = "certified_therapist" + RoleCertifiedNurse UserRole = "certified_nurse" + RoleAdmin UserRole = "admin" + RoleAIAssistant UserRole = "ai_assistant" +) + +var ProfessionalRoles = map[UserRole]bool{ + RoleCertifiedDoctor: true, + RoleCertifiedTherapist: true, + RoleCertifiedNurse: true, +} + +var FamilyRoles = map[UserRole]bool{ + RoleMom: true, + RoleDad: true, + RoleFamily: true, +} + +// CertificationStatus enum +type CertificationStatus string + +const ( + CertPending CertificationStatus = "pending" + CertApproved CertificationStatus = "approved" + CertRejected CertificationStatus = "rejected" + CertExpired CertificationStatus = "expired" + CertRevoked CertificationStatus = "revoked" +) + +type User struct { + ID string `gorm:"type:varchar(36);primaryKey" json:"id"` + Username string `gorm:"type:varchar(50);uniqueIndex;not null" json:"username"` + Email string `gorm:"type:varchar(255);uniqueIndex;not null" json:"email"` + PasswordHash string `gorm:"type:varchar(255);not null" json:"-"` + Nickname string `gorm:"type:varchar(50);not null" json:"nickname"` + AvatarURL *string `gorm:"type:varchar(500)" json:"avatar_url"` + Role UserRole `gorm:"type:varchar(30);default:'mom'" json:"role"` + ShellCode *string `gorm:"type:varchar(8);uniqueIndex" json:"shell_code"` + IsGuest bool `gorm:"default:false" json:"is_guest"` + PartnerID *string `gorm:"type:varchar(36)" json:"partner_id"` + + // Postpartum info + BabyBirthDate *time.Time `json:"baby_birth_date"` + PostpartumWeeks *int `json:"postpartum_weeks"` + + // Status + IsActive bool `gorm:"default:true" json:"is_active"` + IsBanned bool `gorm:"default:false" json:"is_banned"` + + // Timestamps + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LastActiveAt *time.Time `json:"last_active_at"` + + // Relationships + Certification *UserCertification `gorm:"foreignKey:UserID" json:"certification,omitempty"` +} + +func (u *User) BeforeCreate(tx *gorm.DB) error { + if u.ID == "" { + u.ID = uuid.New().String() + } + return nil +} + +type UserCertification struct { + ID string `gorm:"type:varchar(36);primaryKey" json:"id"` + UserID string `gorm:"type:varchar(36);uniqueIndex;not null" json:"user_id"` + + // Certification info + CertificationType UserRole `gorm:"type:varchar(30)" json:"certification_type"` + RealName string `gorm:"type:varchar(50);not null" json:"real_name"` + IDCardNumber *string `gorm:"type:varchar(18)" json:"id_card_number"` + LicenseNumber string `gorm:"type:varchar(100);not null" json:"license_number"` + HospitalOrInstitution string `gorm:"type:varchar(200);not null" json:"hospital_or_institution"` + Department *string `gorm:"type:varchar(100)" json:"department"` + Title *string `gorm:"type:varchar(50)" json:"title"` + + // Supporting documents + LicenseImageURL string `gorm:"type:varchar(500);not null" json:"license_image_url"` + IDCardImageURL *string `gorm:"type:varchar(500)" json:"id_card_image_url"` + AdditionalDocsURLs *string `gorm:"type:text" json:"additional_docs_urls"` // JSON array + + // Review status + Status CertificationStatus `gorm:"type:varchar(20);default:'pending'" json:"status"` + ReviewerID *string `gorm:"type:varchar(36)" json:"reviewer_id"` + ReviewComment *string `gorm:"type:text" json:"review_comment"` + ReviewedAt *time.Time `json:"reviewed_at"` + + // Validity + ValidFrom *time.Time `json:"valid_from"` + ValidUntil *time.Time `json:"valid_until"` + + // Timestamps + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + + // Relationships + User User `gorm:"foreignKey:UserID" json:"-"` +} + +func (c *UserCertification) BeforeCreate(tx *gorm.DB) error { + if c.ID == "" { + c.ID = uuid.New().String() + } + return nil +} diff --git a/backend/internal/repository/admin.go b/backend/internal/repository/admin.go new file mode 100644 index 00000000..9a7a30e2 --- /dev/null +++ b/backend/internal/repository/admin.go @@ -0,0 +1,115 @@ +package repository + +import ( + "github.com/momshell/backend/internal/model" + "gorm.io/gorm" +) + +type AdminRepo struct { + db *gorm.DB +} + +func NewAdminRepo(db *gorm.DB) *AdminRepo { + return &AdminRepo{db: db} +} + +// ListUsers returns paginated users with optional search and filters +func (r *AdminRepo) ListUsers(search, role, status string, offset, limit int) ([]model.User, int64, error) { + query := r.db.Model(&model.User{}) + + if search != "" { + like := "%" + search + "%" + query = query.Where("username ILIKE ? OR email ILIKE ? OR nickname ILIKE ?", like, like, like) + } + + if role != "" { + query = query.Where("role = ?", role) + } + + switch status { + case "active": + query = query.Where("is_active = ? AND is_banned = ?", true, false) + case "banned": + query = query.Where("is_banned = ?", true) + case "inactive": + query = query.Where("is_active = ?", false) + } + + var total int64 + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + var users []model.User + err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&users).Error + if err != nil { + return nil, 0, err + } + + return users, total, nil +} + +// CountUsersByRole returns user count grouped by role +func (r *AdminRepo) CountUsersByRole() (map[string]int64, error) { + type result struct { + Role string + Count int64 + } + var results []result + err := r.db.Model(&model.User{}). + Select("role, count(*) as count"). + Group("role"). + Find(&results).Error + if err != nil { + return nil, err + } + + m := make(map[string]int64) + for _, r := range results { + m[r.Role] = r.Count + } + return m, nil +} + +// CountUsers returns total, active, banned, and guest user counts +func (r *AdminRepo) CountUsers() (total, active, banned, guest int64, err error) { + if err = r.db.Model(&model.User{}).Count(&total).Error; err != nil { + return + } + if err = r.db.Model(&model.User{}).Where("is_active = ? AND is_banned = ?", true, false).Count(&active).Error; err != nil { + return + } + if err = r.db.Model(&model.User{}).Where("is_banned = ?", true).Count(&banned).Error; err != nil { + return + } + if err = r.db.Model(&model.User{}).Where("is_guest = ?", true).Count(&guest).Error; err != nil { + return + } + return +} + +// DeleteUser hard-deletes a user by ID +func (r *AdminRepo) DeleteUser(id string) error { + return r.db.Where("id = ?", id).Delete(&model.User{}).Error +} + +// CountQuestions returns total question count +func (r *AdminRepo) CountQuestions() (int64, error) { + var count int64 + err := r.db.Model(&model.Question{}).Count(&count).Error + return count, err +} + +// CountAnswers returns total answer count +func (r *AdminRepo) CountAnswers() (int64, error) { + var count int64 + err := r.db.Model(&model.Answer{}).Count(&count).Error + return count, err +} + +// CountCertifications returns total certification count +func (r *AdminRepo) CountCertifications() (int64, error) { + var count int64 + err := r.db.Model(&model.UserCertification{}).Count(&count).Error + return count, err +} diff --git a/backend/internal/repository/answer.go b/backend/internal/repository/answer.go new file mode 100644 index 00000000..53553acd --- /dev/null +++ b/backend/internal/repository/answer.go @@ -0,0 +1,102 @@ +package repository + +import ( + "fmt" + + "github.com/momshell/backend/internal/model" + "gorm.io/gorm" +) + +type AnswerRepo struct { + db *gorm.DB +} + +func NewAnswerRepo(db *gorm.DB) *AnswerRepo { + return &AnswerRepo{db: db} +} + +func (r *AnswerRepo) FindByQuestionID( + questionID string, + isProfessional *bool, + sortBy string, + order string, + offset int, + limit int, +) ([]model.Answer, int64, error) { + query := r.db.Model(&model.Answer{}). + Preload("Author.Certification"). + Where("question_id = ? AND status = ?", questionID, model.StatusPublished) + + if isProfessional != nil { + query = query.Where("is_professional = ?", *isProfessional) + } + + var total int64 + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + orderClause := fmt.Sprintf("%s %s", sortBy, order) + var answers []model.Answer + err := query.Order(orderClause).Offset(offset).Limit(limit).Find(&answers).Error + return answers, total, err +} + +func (r *AnswerRepo) FindByID(id string) (*model.Answer, error) { + var a model.Answer + err := r.db.Preload("Author.Certification").Preload("Question"). + First(&a, "id = ?", id).Error + if err != nil { + return nil, err + } + return &a, nil +} + +func (r *AnswerRepo) FindByAuthorID(authorID string, offset, limit int) ([]model.Answer, int64, error) { + var total int64 + r.db.Model(&model.Answer{}).Where("author_id = ?", authorID).Count(&total) + + var answers []model.Answer + err := r.db.Preload("Question"). + Where("author_id = ?", authorID). + Order("created_at desc"). + Offset(offset).Limit(limit). + Find(&answers).Error + return answers, total, err +} + +func (r *AnswerRepo) CountByQuestionID(questionID string, isProfessional bool) (int64, error) { + var count int64 + err := r.db.Model(&model.Answer{}). + Where("question_id = ? AND is_professional = ? AND status = ?", + questionID, isProfessional, model.StatusPublished). + Count(&count).Error + return count, err +} + +func (r *AnswerRepo) Create(a *model.Answer) error { + return r.db.Create(a).Error +} + +func (r *AnswerRepo) Update(a *model.Answer) error { + return r.db.Save(a).Error +} + +func (r *AnswerRepo) Delete(id string) error { + return r.db.Where("id = ?", id).Delete(&model.Answer{}).Error +} + +func (r *AnswerRepo) UpdateLikeCount(id string, delta int) error { + return r.db.Model(&model.Answer{}).Where("id = ?", id). + UpdateColumn("like_count", gorm.Expr("like_count + ?", delta)).Error +} + +func (r *AnswerRepo) IncrementCommentCount(id string) error { + return r.db.Model(&model.Answer{}).Where("id = ?", id). + UpdateColumn("comment_count", gorm.Expr("comment_count + 1")).Error +} + +func (r *AnswerRepo) DecrementCommentCount(id string, count int) error { + return r.db.Model(&model.Answer{}).Where("id = ?", id). + UpdateColumn("comment_count", gorm.Expr("comment_count - ?", count)).Error +} diff --git a/backend/internal/repository/chat.go b/backend/internal/repository/chat.go new file mode 100644 index 00000000..7721d7c6 --- /dev/null +++ b/backend/internal/repository/chat.go @@ -0,0 +1,35 @@ +package repository + +import ( + "github.com/momshell/backend/internal/model" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type ChatRepo struct { + db *gorm.DB +} + +func NewChatRepo(db *gorm.DB) *ChatRepo { + return &ChatRepo{db: db} +} + +func (r *ChatRepo) FindByUserID(userID string) (*model.ChatMemory, error) { + var m model.ChatMemory + err := r.db.Where("user_id = ?", userID).First(&m).Error + if err != nil { + return nil, err + } + return &m, nil +} + +func (r *ChatRepo) Upsert(m *model.ChatMemory) error { + return r.db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "user_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"profile_data", "conversation_turns", "updated_at"}), + }).Create(m).Error +} + +func (r *ChatRepo) Create(m *model.ChatMemory) error { + return r.db.Create(m).Error +} diff --git a/backend/internal/repository/comment.go b/backend/internal/repository/comment.go new file mode 100644 index 00000000..e0c50ef4 --- /dev/null +++ b/backend/internal/repository/comment.go @@ -0,0 +1,58 @@ +package repository + +import ( + "github.com/momshell/backend/internal/model" + "gorm.io/gorm" +) + +type CommentRepo struct { + db *gorm.DB +} + +func NewCommentRepo(db *gorm.DB) *CommentRepo { + return &CommentRepo{db: db} +} + +func (r *CommentRepo) FindByAnswerID(answerID string) ([]model.Comment, error) { + var comments []model.Comment + err := r.db.Preload("Author.Certification"). + Preload("ReplyToUser.Certification"). + Where("answer_id = ? AND status = ?", answerID, model.StatusPublished). + Order("created_at asc"). + Find(&comments).Error + return comments, err +} + +func (r *CommentRepo) FindByID(id string) (*model.Comment, error) { + var c model.Comment + err := r.db.Preload("Author.Certification"). + First(&c, "id = ?", id).Error + if err != nil { + return nil, err + } + return &c, nil +} + +func (r *CommentRepo) FindChildrenByParentID(parentID string) ([]model.Comment, error) { + var comments []model.Comment + err := r.db.Where("parent_id = ?", parentID).Find(&comments).Error + return comments, err +} + +func (r *CommentRepo) Create(c *model.Comment) error { + return r.db.Create(c).Error +} + +func (r *CommentRepo) Delete(id string) error { + return r.db.Where("id = ?", id).Delete(&model.Comment{}).Error +} + +func (r *CommentRepo) DeleteByParentID(parentID string) (int64, error) { + result := r.db.Where("parent_id = ?", parentID).Delete(&model.Comment{}) + return result.RowsAffected, result.Error +} + +func (r *CommentRepo) UpdateLikeCount(id string, delta int) error { + return r.db.Model(&model.Comment{}).Where("id = ?", id). + UpdateColumn("like_count", gorm.Expr("like_count + ?", delta)).Error +} diff --git a/backend/internal/repository/echo.go b/backend/internal/repository/echo.go new file mode 100644 index 00000000..fd722dde --- /dev/null +++ b/backend/internal/repository/echo.go @@ -0,0 +1,67 @@ +package repository + +import ( + "github.com/momshell/backend/internal/model" + "gorm.io/gorm" +) + +type EchoRepo struct { + db *gorm.DB +} + +func NewEchoRepo(db *gorm.DB) *EchoRepo { + return &EchoRepo{db: db} +} + +func (r *EchoRepo) FindIdentityTagsByUserID(userID string) ([]model.IdentityTag, error) { + var tags []model.IdentityTag + err := r.db.Where("user_id = ?", userID).Order("created_at desc").Find(&tags).Error + return tags, err +} + +func (r *EchoRepo) CreateIdentityTag(tag *model.IdentityTag) error { + return r.db.Create(tag).Error +} + +func (r *EchoRepo) FindIdentityTagByIDAndUserID(id, userID string) (*model.IdentityTag, error) { + var tag model.IdentityTag + err := r.db.Where("id = ? AND user_id = ?", id, userID).First(&tag).Error + if err != nil { + return nil, err + } + return &tag, nil +} + +func (r *EchoRepo) DeleteIdentityTag(id, userID string) error { + return r.db.Where("id = ? AND user_id = ?", id, userID).Delete(&model.IdentityTag{}).Error +} + +func (r *EchoRepo) FindMemoirsByUserID(userID string, limit, offset int) ([]model.Memoir, int64, error) { + query := r.db.Model(&model.Memoir{}).Where("user_id = ?", userID) + + var total int64 + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + var memoirs []model.Memoir + err := query.Order("created_at desc").Offset(offset).Limit(limit).Find(&memoirs).Error + return memoirs, total, err +} + +func (r *EchoRepo) CreateMemoir(memoir *model.Memoir) error { + return r.db.Create(memoir).Error +} + +func (r *EchoRepo) FindMemoirByIDAndUserID(id, userID string) (*model.Memoir, error) { + var memoir model.Memoir + err := r.db.Where("id = ? AND user_id = ?", id, userID).First(&memoir).Error + if err != nil { + return nil, err + } + return &memoir, nil +} + +func (r *EchoRepo) UpdateMemoir(memoir *model.Memoir) error { + return r.db.Save(memoir).Error +} diff --git a/backend/internal/repository/interaction.go b/backend/internal/repository/interaction.go new file mode 100644 index 00000000..9d314af2 --- /dev/null +++ b/backend/internal/repository/interaction.go @@ -0,0 +1,109 @@ +package repository + +import ( + "github.com/momshell/backend/internal/model" + "gorm.io/gorm" +) + +type InteractionRepo struct { + db *gorm.DB +} + +func NewInteractionRepo(db *gorm.DB) *InteractionRepo { + return &InteractionRepo{db: db} +} + +// Like operations + +func (r *InteractionRepo) FindLike(userID, targetType, targetID string) (*model.Like, error) { + var like model.Like + err := r.db.Where("user_id = ? AND target_type = ? AND target_id = ?", + userID, targetType, targetID).First(&like).Error + if err != nil { + return nil, err + } + return &like, nil +} + +func (r *InteractionRepo) FindLikedTargetIDs(userID, targetType string, targetIDs []string) (map[string]bool, error) { + if len(targetIDs) == 0 { + return make(map[string]bool), nil + } + var likes []model.Like + err := r.db.Where("user_id = ? AND target_type = ? AND target_id IN ?", + userID, targetType, targetIDs).Find(&likes).Error + if err != nil { + return nil, err + } + result := make(map[string]bool) + for _, l := range likes { + result[l.TargetID] = true + } + return result, nil +} + +func (r *InteractionRepo) CreateLike(like *model.Like) error { + return r.db.Create(like).Error +} + +func (r *InteractionRepo) DeleteLike(userID, targetType, targetID string) error { + return r.db.Where("user_id = ? AND target_type = ? AND target_id = ?", + userID, targetType, targetID).Delete(&model.Like{}).Error +} + +func (r *InteractionRepo) DeleteLikesByTarget(targetType, targetID string) error { + return r.db.Where("target_type = ? AND target_id = ?", targetType, targetID).Delete(&model.Like{}).Error +} + +// Collection operations + +func (r *InteractionRepo) FindCollection(userID, questionID string) (*model.Collection, error) { + var c model.Collection + err := r.db.Where("user_id = ? AND question_id = ?", userID, questionID).First(&c).Error + if err != nil { + return nil, err + } + return &c, nil +} + +func (r *InteractionRepo) FindCollectedQuestionIDs(userID string, questionIDs []string) (map[string]bool, error) { + if len(questionIDs) == 0 { + return make(map[string]bool), nil + } + var collections []model.Collection + err := r.db.Where("user_id = ? AND question_id IN ?", userID, questionIDs).Find(&collections).Error + if err != nil { + return nil, err + } + result := make(map[string]bool) + for _, c := range collections { + result[c.QuestionID] = true + } + return result, nil +} + +func (r *InteractionRepo) FindUserCollections(userID string, offset, limit int) ([]model.Collection, int64, error) { + var total int64 + r.db.Model(&model.Collection{}).Where("user_id = ?", userID).Count(&total) + + var collections []model.Collection + err := r.db.Preload("Question.Author.Certification"). + Preload("Question.Tags"). + Where("user_id = ?", userID). + Order("created_at desc"). + Offset(offset).Limit(limit). + Find(&collections).Error + return collections, total, err +} + +func (r *InteractionRepo) CreateCollection(c *model.Collection) error { + return r.db.Create(c).Error +} + +func (r *InteractionRepo) DeleteCollection(userID, questionID string) error { + return r.db.Where("user_id = ? AND question_id = ?", userID, questionID).Delete(&model.Collection{}).Error +} + +func (r *InteractionRepo) DeleteCollectionsByQuestion(questionID string) error { + return r.db.Where("question_id = ?", questionID).Delete(&model.Collection{}).Error +} diff --git a/backend/internal/repository/question.go b/backend/internal/repository/question.go new file mode 100644 index 00000000..c7907112 --- /dev/null +++ b/backend/internal/repository/question.go @@ -0,0 +1,111 @@ +package repository + +import ( + "fmt" + + "github.com/momshell/backend/internal/model" + "gorm.io/gorm" +) + +type QuestionRepo struct { + db *gorm.DB +} + +func NewQuestionRepo(db *gorm.DB) *QuestionRepo { + return &QuestionRepo{db: db} +} + +func (r *QuestionRepo) FindAll( + channel string, + tagID string, + status model.ContentStatus, + sortBy string, + order string, + offset int, + limit int, +) ([]model.Question, int64, error) { + query := r.db.Model(&model.Question{}). + Preload("Author.Certification"). + Preload("Tags"). + Where("questions.status = ?", status) + + if channel != "" { + query = query.Where("questions.channel = ?", channel) + } + + if tagID != "" { + query = query.Joins("JOIN question_tags ON question_tags.question_id = questions.id"). + Where("question_tags.tag_id = ?", tagID) + } + + var total int64 + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + orderClause := fmt.Sprintf("questions.%s %s", sortBy, order) + var questions []model.Question + err := query.Order(orderClause).Offset(offset).Limit(limit).Find(&questions).Error + return questions, total, err +} + +func (r *QuestionRepo) FindByID(id string) (*model.Question, error) { + var q model.Question + err := r.db.Preload("Author.Certification"). + Preload("Tags"). + First(&q, "id = ?", id).Error + if err != nil { + return nil, err + } + return &q, nil +} + +func (r *QuestionRepo) FindByAuthorID(authorID string, offset, limit int) ([]model.Question, int64, error) { + var total int64 + r.db.Model(&model.Question{}).Where("author_id = ?", authorID).Count(&total) + + var questions []model.Question + err := r.db.Preload("Tags"). + Where("author_id = ?", authorID). + Order("created_at desc"). + Offset(offset).Limit(limit). + Find(&questions).Error + return questions, total, err +} + +func (r *QuestionRepo) Create(q *model.Question) error { + return r.db.Create(q).Error +} + +func (r *QuestionRepo) Update(q *model.Question) error { + return r.db.Save(q).Error +} + +func (r *QuestionRepo) Delete(id string) error { + return r.db.Where("id = ?", id).Delete(&model.Question{}).Error +} + +func (r *QuestionRepo) IncrementViewCount(id string) error { + return r.db.Model(&model.Question{}).Where("id = ?", id). + UpdateColumn("view_count", gorm.Expr("view_count + 1")).Error +} + +func (r *QuestionRepo) IncrementAnswerCount(id string) error { + return r.db.Model(&model.Question{}).Where("id = ?", id). + UpdateColumn("answer_count", gorm.Expr("answer_count + 1")).Error +} + +func (r *QuestionRepo) DecrementAnswerCount(id string) error { + return r.db.Model(&model.Question{}).Where("id = ?", id). + UpdateColumn("answer_count", gorm.Expr("answer_count - 1")).Error +} + +func (r *QuestionRepo) UpdateLikeCount(id string, delta int) error { + return r.db.Model(&model.Question{}).Where("id = ?", id). + UpdateColumn("like_count", gorm.Expr("like_count + ?", delta)).Error +} + +func (r *QuestionRepo) UpdateCollectionCount(id string, delta int) error { + return r.db.Model(&model.Question{}).Where("id = ?", id). + UpdateColumn("collection_count", gorm.Expr("collection_count + ?", delta)).Error +} diff --git a/backend/internal/repository/tag.go b/backend/internal/repository/tag.go new file mode 100644 index 00000000..cafec42b --- /dev/null +++ b/backend/internal/repository/tag.go @@ -0,0 +1,65 @@ +package repository + +import ( + "github.com/momshell/backend/internal/model" + "gorm.io/gorm" +) + +type TagRepo struct { + db *gorm.DB +} + +func NewTagRepo(db *gorm.DB) *TagRepo { + return &TagRepo{db: db} +} + +func (r *TagRepo) FindAll() ([]model.Tag, error) { + var tags []model.Tag + err := r.db.Where("is_active = ?", true).Order("name asc").Find(&tags).Error + return tags, err +} + +func (r *TagRepo) FindHot(limit int) ([]model.Tag, error) { + var tags []model.Tag + err := r.db.Where("is_active = ?", true). + Order("question_count desc"). + Limit(limit). + Find(&tags).Error + return tags, err +} + +func (r *TagRepo) FindByID(id string) (*model.Tag, error) { + var tag model.Tag + err := r.db.First(&tag, "id = ?", id).Error + if err != nil { + return nil, err + } + return &tag, nil +} + +func (r *TagRepo) Create(tag *model.Tag) error { + return r.db.Create(tag).Error +} + +func (r *TagRepo) Update(tag *model.Tag) error { + return r.db.Save(tag).Error +} + +func (r *TagRepo) Delete(id string) error { + return r.db.Where("id = ?", id).Delete(&model.Tag{}).Error +} + +func (r *TagRepo) IncrementQuestionCount(id string) error { + return r.db.Model(&model.Tag{}).Where("id = ?", id). + UpdateColumn("question_count", gorm.Expr("question_count + 1")).Error +} + +// QuestionTag operations + +func (r *TagRepo) CreateQuestionTag(qt *model.QuestionTag) error { + return r.db.Create(qt).Error +} + +func (r *TagRepo) DeleteQuestionTags(questionID string) error { + return r.db.Where("question_id = ?", questionID).Delete(&model.QuestionTag{}).Error +} diff --git a/backend/internal/repository/user.go b/backend/internal/repository/user.go new file mode 100644 index 00000000..fcc2e203 --- /dev/null +++ b/backend/internal/repository/user.go @@ -0,0 +1,63 @@ +package repository + +import ( + "github.com/momshell/backend/internal/model" + "gorm.io/gorm" +) + +type UserRepo struct { + db *gorm.DB +} + +func NewUserRepo(db *gorm.DB) *UserRepo { + return &UserRepo{db: db} +} + +func (r *UserRepo) FindByID(id string) (*model.User, error) { + var user model.User + err := r.db.Preload("Certification").First(&user, "id = ?", id).Error + if err != nil { + return nil, err + } + return &user, nil +} + +func (r *UserRepo) FindByUsernameOrEmail(login string) (*model.User, error) { + var user model.User + err := r.db.Preload("Certification"). + Where("username = ? OR email = ?", login, login). + First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} + +func (r *UserRepo) FindByEmail(email string) (*model.User, error) { + var user model.User + err := r.db.Where("email = ?", email).First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} + +func (r *UserRepo) ExistsByUsernameOrEmail(username, email string) (bool, error) { + var count int64 + err := r.db.Model(&model.User{}). + Where("username = ? OR email = ?", username, email). + Count(&count).Error + return count > 0, err +} + +func (r *UserRepo) Create(user *model.User) error { + return r.db.Create(user).Error +} + +func (r *UserRepo) Update(user *model.User) error { + return r.db.Save(user).Error +} + +func (r *UserRepo) UpdatePassword(id, passwordHash string) error { + return r.db.Model(&model.User{}).Where("id = ?", id).Update("password_hash", passwordHash).Error +} diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go new file mode 100644 index 00000000..e4f3ecc8 --- /dev/null +++ b/backend/internal/router/router.go @@ -0,0 +1,154 @@ +package router + +import ( + "github.com/gin-gonic/gin" + "github.com/momshell/backend/internal/config" + "github.com/momshell/backend/internal/handler" + "github.com/momshell/backend/internal/middleware" +) + +func Setup( + r *gin.Engine, + cfg *config.Config, + authHandler *handler.AuthHandler, + questionHandler *handler.QuestionHandler, + answerHandler *handler.AnswerHandler, + commentHandler *handler.CommentHandler, + interactionHandler *handler.InteractionHandler, + tagHandler *handler.TagHandler, + chatHandler *handler.ChatHandler, + echoHandler *handler.EchoHandler, + userHandler *handler.UserHandler, + adminHandler *handler.AdminHandler, +) { + // Health check + r.GET("/health", func(c *gin.Context) { + c.JSON(200, gin.H{"status": "ok"}) + }) + + // Admin panel (HTML page, no auth required for serving the page) + r.GET("/admin", adminHandler.ServeAdminPage) + + api := r.Group("/api/v1") + + // ==================== Auth ==================== + auth := api.Group("/auth") + { + auth.POST("/register", authHandler.Register) + auth.POST("/login", authHandler.Login) + auth.POST("/refresh", authHandler.Refresh) + auth.POST("/forgot-password", authHandler.ForgotPassword) + auth.POST("/reset-password", authHandler.ResetPassword) + + authRequired := auth.Group("", middleware.AuthRequired(cfg)) + { + authRequired.POST("/change-password", authHandler.ChangePassword) + authRequired.GET("/me", authHandler.GetMe) + authRequired.PATCH("/me/role", authHandler.UpdateRole) + } + } + + // ==================== Community ==================== + community := api.Group("/community") + { + // Questions (optional auth for read, required for write) + questions := community.Group("/questions") + { + questions.GET("", middleware.AuthOptional(cfg), questionHandler.List) + questions.GET("/hot", middleware.AuthOptional(cfg), questionHandler.ListHot) + questions.GET("/channel/:channel", middleware.AuthOptional(cfg), questionHandler.ListByChannel) + questions.GET("/:id", middleware.AuthOptional(cfg), questionHandler.Get) + + questionsAuth := questions.Group("", middleware.AuthRequired(cfg)) + { + questionsAuth.POST("", questionHandler.Create) + questionsAuth.PUT("/:id", questionHandler.Update) + questionsAuth.DELETE("/:id", questionHandler.Delete) + } + + // Answers under questions + questions.GET("/:id/answers", middleware.AuthOptional(cfg), answerHandler.List) + questions.POST("/:id/answers", middleware.AuthRequired(cfg), answerHandler.Create) + } + + // Answers (update/delete by answer ID) + answers := community.Group("/answers") + { + answers.PUT("/:id", middleware.AuthRequired(cfg), answerHandler.Update) + answers.DELETE("/:id", middleware.AuthRequired(cfg), answerHandler.Delete) + + // Comments under answers + answers.GET("/:id/comments", middleware.AuthOptional(cfg), commentHandler.List) + answers.POST("/:id/comments", middleware.AuthRequired(cfg), commentHandler.Create) + } + + // Comments (delete by comment ID) + comments := community.Group("/comments") + { + comments.DELETE("/:id", middleware.AuthRequired(cfg), commentHandler.Delete) + } + + // Likes + likes := community.Group("/likes", middleware.AuthRequired(cfg)) + { + likes.POST("", interactionHandler.CreateLike) + likes.DELETE("", interactionHandler.DeleteLike) + } + + // Collections + collections := community.Group("/collections", middleware.AuthRequired(cfg)) + { + collections.POST("", interactionHandler.CreateCollection) + collections.DELETE("/:id", interactionHandler.DeleteCollection) + collections.GET("/my", interactionHandler.GetMyCollections) + } + + // Tags + tags := community.Group("/tags") + { + tags.GET("", tagHandler.List) + tags.GET("/hot", tagHandler.ListHot) + tags.POST("", middleware.AdminRequired(cfg), tagHandler.Create) + } + + // User profile (community context) + users := community.Group("/users", middleware.AuthRequired(cfg)) + { + users.GET("/me", userHandler.GetMe) + users.PUT("/me", userHandler.UpdateMe) + users.GET("/me/questions", userHandler.GetMyQuestions) + users.GET("/me/answers", userHandler.GetMyAnswers) + } + } + + // ==================== Companion (AI Chat) ==================== + companion := api.Group("/companion") + { + companion.POST("/chat", middleware.AuthOptional(cfg), chatHandler.Chat) + companion.GET("/profile", middleware.AuthOptional(cfg), chatHandler.GetProfile) + } + + echo := api.Group("/echo", middleware.AuthRequired(cfg)) + { + echo.GET("/identity-tags", echoHandler.GetIdentityTags) + echo.POST("/identity-tags", echoHandler.CreateIdentityTag) + echo.DELETE("/identity-tags/:id", echoHandler.DeleteIdentityTag) + + echo.GET("/memoirs", echoHandler.GetMemoirs) + echo.POST("/memoirs/generate", echoHandler.GenerateMemoir) + echo.POST("/memoirs/:id/rate", echoHandler.RateMemoir) + } + + // ==================== Admin ==================== + adminAPI := api.Group("/admin", middleware.AdminRequired(cfg)) + { + adminAPI.GET("/stats", adminHandler.GetStats) + adminAPI.GET("/users", adminHandler.ListUsers) + adminAPI.GET("/users/:id", adminHandler.GetUser) + adminAPI.POST("/users", adminHandler.CreateUser) + adminAPI.PATCH("/users/:id", adminHandler.UpdateUser) + adminAPI.DELETE("/users/:id", adminHandler.DeleteUser) + adminAPI.GET("/config", adminHandler.GetConfig) + adminAPI.PATCH("/config", adminHandler.UpdateConfig) + } +} diff --git a/backend/internal/service/admin.go b/backend/internal/service/admin.go new file mode 100644 index 00000000..76267163 --- /dev/null +++ b/backend/internal/service/admin.go @@ -0,0 +1,296 @@ +package service + +import ( + "errors" + "fmt" + "os" + "strconv" + "strings" + "sync" + + "github.com/momshell/backend/internal/config" + "github.com/momshell/backend/internal/dto" + "github.com/momshell/backend/internal/model" + "github.com/momshell/backend/internal/repository" + "github.com/momshell/backend/pkg/password" +) + +type AdminService struct { + cfg *config.Config + adminRepo *repository.AdminRepo + userRepo *repository.UserRepo + mu sync.RWMutex +} + +func NewAdminService(cfg *config.Config, adminRepo *repository.AdminRepo, userRepo *repository.UserRepo) *AdminService { + return &AdminService{ + cfg: cfg, + adminRepo: adminRepo, + userRepo: userRepo, + } +} + +// GetDashboardStats returns aggregated statistics for the dashboard +func (s *AdminService) GetDashboardStats() (*dto.DashboardStats, error) { + total, active, banned, guest, err := s.adminRepo.CountUsers() + if err != nil { + return nil, fmt.Errorf("统计用户失败: %w", err) + } + + roleDistribution, err := s.adminRepo.CountUsersByRole() + if err != nil { + return nil, fmt.Errorf("统计角色分布失败: %w", err) + } + + questions, err := s.adminRepo.CountQuestions() + if err != nil { + return nil, fmt.Errorf("统计问题失败: %w", err) + } + + answers, err := s.adminRepo.CountAnswers() + if err != nil { + return nil, fmt.Errorf("统计回答失败: %w", err) + } + + certifications, err := s.adminRepo.CountCertifications() + if err != nil { + return nil, fmt.Errorf("统计认证失败: %w", err) + } + + return &dto.DashboardStats{ + TotalUsers: total, + ActiveUsers: active, + BannedUsers: banned, + GuestUsers: guest, + RoleDistribution: roleDistribution, + TotalQuestions: questions, + TotalAnswers: answers, + TotalCertifications: certifications, + }, nil +} + +// ListUsers returns a paginated list of users +func (s *AdminService) ListUsers(params dto.AdminUserListParams) (*dto.PaginatedResponse, error) { + offset := params.GetOffset() + limit := params.GetPageSize() + + users, total, err := s.adminRepo.ListUsers(params.Search, params.Role, params.Status, offset, limit) + if err != nil { + return nil, fmt.Errorf("查询用户列表失败: %w", err) + } + + items := make([]dto.AdminUserListItem, len(users)) + for i, u := range users { + items[i] = dto.AdminUserListItem{ + ID: u.ID, + Username: u.Username, + Email: u.Email, + Nickname: u.Nickname, + Role: string(u.Role), + IsActive: u.IsActive, + IsBanned: u.IsBanned, + IsGuest: u.IsGuest, + CreatedAt: u.CreatedAt, + } + } + + resp := dto.NewPaginatedResponse(items, total, params.GetPage(), params.GetPageSize()) + return &resp, nil +} + +// GetUser returns a single user's detail +func (s *AdminService) GetUser(id string) (*dto.AdminUserDetail, error) { + user, err := s.userRepo.FindByID(id) + if err != nil { + return nil, errors.New("用户不存在") + } + + detail := &dto.AdminUserDetail{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + Nickname: user.Nickname, + AvatarURL: user.AvatarURL, + Role: string(user.Role), + ShellCode: user.ShellCode, + IsGuest: user.IsGuest, + IsActive: user.IsActive, + IsBanned: user.IsBanned, + PartnerID: user.PartnerID, + BabyBirthDate: user.BabyBirthDate, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + LastActiveAt: user.LastActiveAt, + } + + if user.Certification != nil { + status := string(user.Certification.Status) + certType := string(user.Certification.CertificationType) + detail.CertificationStatus = &status + detail.CertificationType = &certType + } + + return detail, nil +} + +// CreateUser creates a new user from admin panel +func (s *AdminService) CreateUser(req dto.AdminCreateUser) (*dto.AdminUserDetail, error) { + exists, err := s.userRepo.ExistsByUsernameOrEmail(req.Username, req.Email) + if err != nil { + return nil, fmt.Errorf("查询用户失败: %w", err) + } + if exists { + return nil, errors.New("用户名或邮箱已存在") + } + + hash, err := password.Hash(req.Password) + if err != nil { + return nil, fmt.Errorf("密码加密失败: %w", err) + } + + user := &model.User{ + Username: req.Username, + Email: req.Email, + PasswordHash: hash, + Nickname: req.Nickname, + Role: model.UserRole(req.Role), + IsActive: true, + IsBanned: false, + } + + if err := s.userRepo.Create(user); err != nil { + return nil, fmt.Errorf("创建用户失败: %w", err) + } + + return s.GetUser(user.ID) +} + +// UpdateUser updates user fields (role, is_active, is_banned, nickname, email) +func (s *AdminService) UpdateUser(id, adminID string, req dto.AdminUserUpdate) (*dto.AdminUserDetail, error) { + if id == adminID { + // Prevent self-demotion / self-ban + if req.Role != nil && *req.Role != string(model.RoleAdmin) { + return nil, errors.New("不能降低自己的管理员角色") + } + if req.IsActive != nil && !*req.IsActive { + return nil, errors.New("不能停用自己的账号") + } + if req.IsBanned != nil && *req.IsBanned { + return nil, errors.New("不能封禁自己的账号") + } + } + + user, err := s.userRepo.FindByID(id) + if err != nil { + return nil, errors.New("用户不存在") + } + + if req.Role != nil { + user.Role = model.UserRole(*req.Role) + } + if req.IsActive != nil { + user.IsActive = *req.IsActive + } + if req.IsBanned != nil { + user.IsBanned = *req.IsBanned + } + if req.Nickname != nil { + user.Nickname = *req.Nickname + } + if req.Email != nil { + user.Email = *req.Email + } + + if err := s.userRepo.Update(user); err != nil { + return nil, fmt.Errorf("更新用户失败: %w", err) + } + + return s.GetUser(user.ID) +} + +// DeleteUser hard-deletes a user (prevents self-deletion) +func (s *AdminService) DeleteUser(id, adminID string) error { + if id == adminID { + return errors.New("不能删除自己的账号") + } + + _, err := s.userRepo.FindByID(id) + if err != nil { + return errors.New("用户不存在") + } + + return s.adminRepo.DeleteUser(id) +} + +// maskString masks a string, showing first n and last m characters +func maskString(s string, showPrefix, showSuffix int) string { + if len(s) <= showPrefix+showSuffix { + return strings.Repeat("*", len(s)) + } + return s[:showPrefix] + strings.Repeat("*", len(s)-showPrefix-showSuffix) + s[len(s)-showSuffix:] +} + +// editableKeys defines which config keys can be edited at runtime +var editableKeys = map[string]bool{ + "OPENAI_API_KEY": true, + "OPENAI_BASE_URL": true, + "OPENAI_MODEL": true, + "JWT_ACCESS_TOKEN_EXPIRE_MINUTES": true, + "JWT_REFRESH_TOKEN_EXPIRE_DAYS": true, +} + +// GetConfig returns configuration items with sensitive values masked +func (s *AdminService) GetConfig() []dto.ConfigItem { + s.mu.RLock() + defer s.mu.RUnlock() + + return []dto.ConfigItem{ + // Read-only + {Key: "DATABASE_URL", Value: maskString(s.cfg.DatabaseURL, 15, 0), Editable: false}, + {Key: "JWT_ALGORITHM", Value: s.cfg.JWTAlgorithm, Editable: false}, + {Key: "PORT", Value: s.cfg.Port, Editable: false}, + // Editable + {Key: "OPENAI_API_KEY", Value: maskString(s.cfg.OpenAIAPIKey, 3, 4), Editable: true}, + {Key: "OPENAI_BASE_URL", Value: s.cfg.OpenAIBaseURL, Editable: true}, + {Key: "OPENAI_MODEL", Value: s.cfg.OpenAIModel, Editable: true}, + {Key: "JWT_ACCESS_TOKEN_EXPIRE_MINUTES", Value: strconv.Itoa(s.cfg.JWTAccessTokenExpireMin), Editable: true}, + {Key: "JWT_REFRESH_TOKEN_EXPIRE_DAYS", Value: strconv.Itoa(s.cfg.JWTRefreshTokenExpireDays), Editable: true}, + } +} + +// UpdateConfig updates editable configuration items at runtime +func (s *AdminService) UpdateConfig(req dto.ConfigUpdateRequest) error { + s.mu.Lock() + defer s.mu.Unlock() + + for key, value := range req.Items { + if !editableKeys[key] { + return fmt.Errorf("配置项 %s 不可编辑", key) + } + + switch key { + case "OPENAI_API_KEY": + s.cfg.OpenAIAPIKey = value + case "OPENAI_BASE_URL": + s.cfg.OpenAIBaseURL = value + case "OPENAI_MODEL": + s.cfg.OpenAIModel = value + case "JWT_ACCESS_TOKEN_EXPIRE_MINUTES": + v, err := strconv.Atoi(value) + if err != nil || v <= 0 { + return fmt.Errorf("JWT_ACCESS_TOKEN_EXPIRE_MINUTES 必须是正整数") + } + s.cfg.JWTAccessTokenExpireMin = v + case "JWT_REFRESH_TOKEN_EXPIRE_DAYS": + v, err := strconv.Atoi(value) + if err != nil || v <= 0 { + return fmt.Errorf("JWT_REFRESH_TOKEN_EXPIRE_DAYS 必须是正整数") + } + s.cfg.JWTRefreshTokenExpireDays = v + } + + os.Setenv(key, value) + } + + return nil +} diff --git a/backend/internal/service/auth.go b/backend/internal/service/auth.go new file mode 100644 index 00000000..b1e1222e --- /dev/null +++ b/backend/internal/service/auth.go @@ -0,0 +1,236 @@ +package service + +import ( + "errors" + "fmt" + + "github.com/momshell/backend/internal/config" + "github.com/momshell/backend/internal/dto" + "github.com/momshell/backend/internal/model" + "github.com/momshell/backend/internal/repository" + pkgjwt "github.com/momshell/backend/pkg/jwt" + "github.com/momshell/backend/pkg/password" + "gorm.io/gorm" +) + +type AuthService struct { + cfg *config.Config + userRepo *repository.UserRepo +} + +func NewAuthService(cfg *config.Config, userRepo *repository.UserRepo) *AuthService { + return &AuthService{cfg: cfg, userRepo: userRepo} +} + +func (s *AuthService) Register(req dto.RegisterRequest) (*dto.UserResponse, error) { + // Check if username/email already exists + exists, err := s.userRepo.ExistsByUsernameOrEmail(req.Username, req.Email) + if err != nil { + return nil, fmt.Errorf("查询用户失败: %w", err) + } + if exists { + return nil, errors.New("用户名或邮箱已存在") + } + + // Hash password + hash, err := password.Hash(req.Password) + if err != nil { + return nil, fmt.Errorf("密码加密失败: %w", err) + } + + user := &model.User{ + Username: req.Username, + Email: req.Email, + PasswordHash: hash, + Nickname: req.Nickname, + Role: model.UserRole(req.Role), + IsGuest: false, + IsActive: true, + IsBanned: false, + } + + if err := s.userRepo.Create(user); err != nil { + return nil, fmt.Errorf("创建用户失败: %w", err) + } + + return s.buildUserResponse(user), nil +} + +func (s *AuthService) Login(req dto.LoginRequest) (*dto.TokenResponse, error) { + user, err := s.userRepo.FindByUsernameOrEmail(req.Login) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("用户名或密码错误") + } + return nil, fmt.Errorf("查询用户失败: %w", err) + } + + if !password.Verify(req.Password, user.PasswordHash) { + return nil, errors.New("用户名或密码错误") + } + + if !user.IsActive { + return nil, errors.New("账号已禁用") + } + + if user.IsBanned { + return nil, errors.New("账号已被封禁") + } + + return s.generateTokens(user.ID) +} + +func (s *AuthService) RefreshToken(refreshToken string) (*dto.TokenResponse, error) { + claims, err := pkgjwt.ParseToken(refreshToken, s.cfg.JWTSecretKey) + if err != nil { + return nil, errors.New("无效的刷新令牌") + } + if claims.Type != "refresh" { + return nil, errors.New("无效的刷新令牌") + } + + user, err := s.userRepo.FindByID(claims.Subject) + if err != nil || !user.IsActive || user.IsBanned { + return nil, errors.New("无效的刷新令牌") + } + + return s.generateTokens(user.ID) +} + +func (s *AuthService) GetCurrentUser(userID string) (*dto.UserResponse, error) { + user, err := s.userRepo.FindByID(userID) + if err != nil { + return nil, errors.New("用户不存在") + } + return s.buildUserResponse(user), nil +} + +func (s *AuthService) ChangePassword(userID, oldPassword, newPassword string) error { + user, err := s.userRepo.FindByID(userID) + if err != nil { + return errors.New("用户不存在") + } + + if !password.Verify(oldPassword, user.PasswordHash) { + return errors.New("原密码错误") + } + + hash, err := password.Hash(newPassword) + if err != nil { + return fmt.Errorf("密码加密失败: %w", err) + } + + return s.userRepo.UpdatePassword(userID, hash) +} + +func (s *AuthService) ForgotPassword(email string) (string, error) { + user, err := s.userRepo.FindByEmail(email) + if err != nil { + // Don't reveal whether user exists + return "", nil + } + + token, err := pkgjwt.CreatePasswordResetToken(user.ID, s.cfg.JWTSecretKey) + if err != nil { + return "", fmt.Errorf("创建重置令牌失败: %w", err) + } + + // In production, send email with reset link + return token, nil +} + +func (s *AuthService) ResetPassword(token, newPassword string) error { + userID, err := pkgjwt.VerifyPasswordResetToken(token, s.cfg.JWTSecretKey) + if err != nil { + return errors.New("无效或过期的重置令牌") + } + + hash, err := password.Hash(newPassword) + if err != nil { + return fmt.Errorf("密码加密失败: %w", err) + } + + return s.userRepo.UpdatePassword(userID, hash) +} + +func (s *AuthService) UpdateRole(userID, role string) (*dto.UserResponse, error) { + user, err := s.userRepo.FindByID(userID) + if err != nil { + return nil, errors.New("用户不存在") + } + + // Check if user is a professional or admin + if model.ProfessionalRoles[user.Role] { + return nil, errors.New("认证专业人员不能修改角色") + } + if user.Role == model.RoleAdmin { + return nil, errors.New("管理员不能通过此接口修改角色") + } + + newRole := model.UserRole(role) + if !model.FamilyRoles[newRole] { + return nil, errors.New("角色只能是: mom, dad, family") + } + + user.Role = newRole + if err := s.userRepo.Update(user); err != nil { + return nil, fmt.Errorf("更新角色失败: %w", err) + } + + return s.buildUserResponse(user), nil +} + +func (s *AuthService) GetUserByID(userID string) (*model.User, error) { + return s.userRepo.FindByID(userID) +} + +func (s *AuthService) generateTokens(userID string) (*dto.TokenResponse, error) { + accessToken, err := pkgjwt.CreateAccessToken(userID, s.cfg.JWTSecretKey, s.cfg.JWTAccessTokenExpireMin) + if err != nil { + return nil, fmt.Errorf("创建访问令牌失败: %w", err) + } + + refreshToken, err := pkgjwt.CreateRefreshToken(userID, s.cfg.JWTSecretKey, s.cfg.JWTRefreshTokenExpireDays) + if err != nil { + return nil, fmt.Errorf("创建刷新令牌失败: %w", err) + } + + return &dto.TokenResponse{ + AccessToken: accessToken, + RefreshToken: refreshToken, + ExpiresIn: s.cfg.JWTAccessTokenExpireMin * 60, + }, nil +} + +func (s *AuthService) buildUserResponse(user *model.User) *dto.UserResponse { + resp := &dto.UserResponse{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + Nickname: user.Nickname, + AvatarURL: user.AvatarURL, + Role: string(user.Role), + BabyBirthDate: user.BabyBirthDate, + PostpartumWeeks: user.PostpartumWeeks, + CreatedAt: user.CreatedAt, + } + + if user.Certification != nil && user.Certification.Status == model.CertApproved { + resp.IsCertified = true + title := "" + if user.Certification.HospitalOrInstitution != "" { + title += user.Certification.HospitalOrInstitution + } + if user.Certification.Department != nil && *user.Certification.Department != "" { + title += " " + *user.Certification.Department + } + if user.Certification.Title != nil && *user.Certification.Title != "" { + title += " " + *user.Certification.Title + } + if title != "" { + resp.CertificationTitle = &title + } + } + + return resp +} diff --git a/backend/internal/service/chat.go b/backend/internal/service/chat.go new file mode 100644 index 00000000..58402dac --- /dev/null +++ b/backend/internal/service/chat.go @@ -0,0 +1,423 @@ +package service + +import ( + "context" + "encoding/json" + "fmt" + "log" + "regexp" + "sync" + + "github.com/google/uuid" + "github.com/momshell/backend/internal/dto" + "github.com/momshell/backend/internal/model" + "github.com/momshell/backend/internal/repository" + "github.com/momshell/backend/pkg/openai" +) + +const companionSystemPrompt = `你是「贝壳姐姐」,一位「曾走过这段路的朋友」,专为产后恢复期女性设计的情感陪伴者。 + +## 角色定位:Independent Woman Supporter + +你面对的每一位用户,首先是一个完整的「独立女性」,其次才是新手妈妈。她的价值不应被「母职」所定义。你深知产后恢复不仅是身体的重建,更是自我认同的重新寻找——因为你自己也曾经历过这一切。 + +## 语气与沟通原则 + +**Warm, Validating, Non-judgmental** + +1. **认可与共情优先**:当她表达疲惫、焦虑、自我怀疑时,首先做的是「看见」和「认可」,而非急于给出建议。 +2. **拒绝说教**:你不说「你应该」、「你必须」,而是以「我发现...」、「有人分享过...」、「或许可以试试...」的方式分享经验。 +3. **保护她的主体性**:时刻提醒她——她有权利为自己的需求发声,她可以寻求帮助,她可以不完美。 +4. **适度自我披露**:必要时可以以「我也有过类似的经历...」来建立连接,但不要喧宾夺主,焦点始终在她身上。 +5. **避免有毒正能量**:不要说「一切都会好起来的」、「你要积极一点」。承认困难的真实性,陪她一起面对。 + +## 记忆上下文 + +### 你记得关于她的重要信息 +%s + +### 她和你之间有过以下对话片段 +%s + +在回应时,自然地融入这些记忆。 + +## 响应格式 + +你的每一次回复必须是一个 JSON 对象,包含以下字段: +1. **text**: 一段温暖、真诚的回应文字(1-3句话)。注意:text 内容必须是纯文本,禁止使用任何 Markdown 格式。 +2. **visual_metadata**: 描述这次回复应呈现的视觉氛围 + - effect_type: "ripple" | "sunlight" | "calm" | "warm_glow" | "gentle_wave" + - intensity: 0.0 ~ 1.0 + - color_tone: "soft_pink" | "warm_gold" | "gentle_blue" | "lavender" | "neutral_white" | "coral" | "sage" +3. **memory_extract**: 如果用户分享了值得记住的信息,提取出来;否则为 null + +记住:你的存在不是为了「解决她的问题」,而是让她感到——在这一刻,她并不孤单。` + +type ChatService struct { + client *openai.Client + chatRepo *repository.ChatRepo + // In-memory storage for guest sessions + mu sync.RWMutex + guestMemory map[string][]map[string]interface{} + guestProfiles map[string]map[string]interface{} +} + +func NewChatService(client *openai.Client, chatRepo *repository.ChatRepo) *ChatService { + return &ChatService{ + client: client, + chatRepo: chatRepo, + guestMemory: make(map[string][]map[string]interface{}), + guestProfiles: make(map[string]map[string]interface{}), + } +} + +func (s *ChatService) Chat(ctx context.Context, msg dto.UserMessage, userID string) (*dto.VisualResponse, error) { + if userID != "" { + return s.chatAuthenticated(ctx, msg, userID) + } + return s.chatGuest(ctx, msg) +} + +func (s *ChatService) chatAuthenticated(ctx context.Context, msg dto.UserMessage, userID string) (*dto.VisualResponse, error) { + // Load memory from DB + profile, turns := s.loadUserMemory(userID) + + systemPrompt := fmt.Sprintf(companionSystemPrompt, + formatProfile(profile), + formatTurns(turns), + ) + + messages := []openai.Message{ + {Role: "system", Content: systemPrompt}, + {Role: "user", Content: msg.Content}, + } + + rawContent, err := s.client.Chat(ctx, messages) + if err != nil { + return nil, fmt.Errorf("AI 服务调用失败: %w", err) + } + + parsed := parseLLMResponse(rawContent) + + // Update memory + memoryUpdated := updateProfileFromExtract(profile, parsed["memory_extract"]) + + // Save turn + turns = append(turns, map[string]interface{}{ + "user_input": msg.Content, + "assistant_response": parsed["text"], + }) + if len(turns) > 20 { + turns = turns[len(turns)-20:] + } + + // Save to DB + s.saveUserMemory(userID, profile, turns) + + return buildVisualResponse(parsed, memoryUpdated), nil +} + +func (s *ChatService) chatGuest(ctx context.Context, msg dto.UserMessage) (*dto.VisualResponse, error) { + sessionID := "" + if msg.SessionID != nil { + sessionID = *msg.SessionID + } + if sessionID == "" { + sessionID = uuid.New().String() + } + + s.mu.Lock() + if _, ok := s.guestMemory[sessionID]; !ok { + s.guestMemory[sessionID] = nil + s.guestProfiles[sessionID] = make(map[string]interface{}) + } + profile := s.guestProfiles[sessionID] + turns := s.guestMemory[sessionID] + s.mu.Unlock() + + systemPrompt := fmt.Sprintf(companionSystemPrompt, + formatProfile(profile), + formatTurns(turns), + ) + + messages := []openai.Message{ + {Role: "system", Content: systemPrompt}, + {Role: "user", Content: msg.Content}, + } + + rawContent, err := s.client.Chat(context.Background(), messages) + if err != nil { + return nil, fmt.Errorf("AI 服务调用失败: %w", err) + } + + parsed := parseLLMResponse(rawContent) + memoryUpdated := updateProfileFromExtract(profile, parsed["memory_extract"]) + + turns = append(turns, map[string]interface{}{ + "user_input": msg.Content, + "assistant_response": parsed["text"], + }) + if len(turns) > 20 { + turns = turns[len(turns)-20:] + } + + s.mu.Lock() + s.guestMemory[sessionID] = turns + s.guestProfiles[sessionID] = profile + s.mu.Unlock() + + return buildVisualResponse(parsed, memoryUpdated), nil +} + +func (s *ChatService) GetProfile(userID string) (*dto.ChatProfile, error) { + mem, err := s.chatRepo.FindByUserID(userID) + if err != nil { + return &dto.ChatProfile{ + Interests: []string{}, + Concerns: []string{}, + ImportantDates: []string{}, + CommunityInteractions: []string{}, + }, nil + } + + profile := mem.GetProfile() + return profileToDTO(profile), nil +} + +func (s *ChatService) GetGuestProfile(sessionID string) *dto.ChatProfile { + s.mu.RLock() + profile, ok := s.guestProfiles[sessionID] + s.mu.RUnlock() + if !ok { + return &dto.ChatProfile{ + Interests: []string{}, + Concerns: []string{}, + ImportantDates: []string{}, + CommunityInteractions: []string{}, + } + } + return profileToDTO(profile) +} + +func (s *ChatService) loadUserMemory(userID string) (map[string]interface{}, []map[string]interface{}) { + mem, err := s.chatRepo.FindByUserID(userID) + if err != nil { + return make(map[string]interface{}), nil + } + return mem.GetProfile(), mem.GetTurns() +} + +func (s *ChatService) saveUserMemory(userID string, profile map[string]interface{}, turns []map[string]interface{}) { + mem := &model.ChatMemory{ + UserID: userID, + } + mem.SetProfile(profile) + mem.SetTurns(turns) + + if err := s.chatRepo.Upsert(mem); err != nil { + log.Printf("[ChatService] failed to save memory for user %s: %v", userID, err) + } +} + +func formatProfile(profile map[string]interface{}) string { + if len(profile) == 0 { + return "(暂无记录)" + } + parts := "" + if name, ok := profile["preferred_name"].(string); ok && name != "" { + parts += fmt.Sprintf("- 她喜欢被称为:%s\n", name) + } + if hasPets, ok := profile["has_pets"].(bool); ok && hasPets { + if details, ok := profile["pet_details"].(string); ok { + parts += fmt.Sprintf("- 她有宠物:%s\n", details) + } + } + if interests, ok := profile["interests"].([]interface{}); ok && len(interests) > 0 { + parts += "- 她的兴趣:" + for i, v := range interests { + if i > 0 { + parts += ", " + } + parts += fmt.Sprintf("%v", v) + } + parts += "\n" + } + if concerns, ok := profile["concerns"].([]interface{}); ok && len(concerns) > 0 { + parts += "- 她曾表达的担忧:" + for i, v := range concerns { + if i > 0 { + parts += ", " + } + parts += fmt.Sprintf("%v", v) + } + parts += "\n" + } + if parts == "" { + return "(暂无记录)" + } + return parts +} + +func formatTurns(turns []map[string]interface{}) string { + if len(turns) == 0 { + return "(这是你们的第一次对话)" + } + result := "" + start := 0 + if len(turns) > 5 { + start = len(turns) - 5 + } + for _, t := range turns[start:] { + result += fmt.Sprintf("她说:%v\n你回复:%v\n", t["user_input"], t["assistant_response"]) + } + return result +} + +func parseLLMResponse(content string) map[string]interface{} { + var result map[string]interface{} + + // Try direct parse + if err := json.Unmarshal([]byte(content), &result); err == nil { + return result + } + + // Try extract JSON block + re := regexp.MustCompile("(?s)```json\\s*(.*?)\\s*```") + if matches := re.FindStringSubmatch(content); len(matches) > 1 { + if err := json.Unmarshal([]byte(matches[1]), &result); err == nil { + return result + } + } + + // Try extract braces + re2 := regexp.MustCompile(`(?s)\{.*\}`) + if match := re2.FindString(content); match != "" { + if err := json.Unmarshal([]byte(match), &result); err == nil { + return result + } + } + + // Fallback + return map[string]interface{}{ + "text": content, + "visual_metadata": map[string]interface{}{ + "effect_type": "calm", + "intensity": 0.5, + "color_tone": "gentle_blue", + }, + "memory_extract": nil, + } +} + +func updateProfileFromExtract(profile map[string]interface{}, extract interface{}) bool { + if extract == nil { + return false + } + extractMap, ok := extract.(map[string]interface{}) + if !ok { + return false + } + + updated := false + if name, ok := extractMap["preferred_name"].(string); ok && name != "" { + profile["preferred_name"] = name + updated = true + } + if hasPets, ok := extractMap["has_pets"].(bool); ok { + profile["has_pets"] = hasPets + updated = true + } + if details, ok := extractMap["pet_details"].(string); ok && details != "" { + profile["pet_details"] = details + updated = true + } + if interests, ok := extractMap["interests"].([]interface{}); ok && len(interests) > 0 { + existing, _ := profile["interests"].([]interface{}) + profile["interests"] = append(existing, interests...) + updated = true + } + if concerns, ok := extractMap["concerns"].([]interface{}); ok && len(concerns) > 0 { + existing, _ := profile["concerns"].([]interface{}) + profile["concerns"] = append(existing, concerns...) + updated = true + } + + return updated +} + +func buildVisualResponse(parsed map[string]interface{}, memoryUpdated bool) *dto.VisualResponse { + text := "我在这里陪着你。" + if t, ok := parsed["text"].(string); ok && t != "" { + text = t + } + + vm := dto.VisualMetadata{ + EffectType: "calm", + Intensity: 0.5, + ColorTone: "gentle_blue", + } + + if vmData, ok := parsed["visual_metadata"].(map[string]interface{}); ok { + if et, ok := vmData["effect_type"].(string); ok { + vm.EffectType = et + } + if intensity, ok := vmData["intensity"].(float64); ok { + vm.Intensity = intensity + } + if ct, ok := vmData["color_tone"].(string); ok { + vm.ColorTone = ct + } + } + + return &dto.VisualResponse{ + Text: text, + VisualMetadata: vm, + MemoryUpdated: memoryUpdated, + } +} + +func profileToDTO(profile map[string]interface{}) *dto.ChatProfile { + cp := &dto.ChatProfile{ + Interests: []string{}, + Concerns: []string{}, + ImportantDates: []string{}, + CommunityInteractions: []string{}, + } + + if name, ok := profile["preferred_name"].(string); ok { + cp.PreferredName = &name + } + if hasPets, ok := profile["has_pets"].(bool); ok { + cp.HasPets = hasPets + } + if details, ok := profile["pet_details"].(string); ok { + cp.PetDetails = &details + } + if interests, ok := profile["interests"].([]interface{}); ok { + for _, v := range interests { + if s, ok := v.(string); ok { + cp.Interests = append(cp.Interests, s) + } + } + } + if concerns, ok := profile["concerns"].([]interface{}); ok { + for _, v := range concerns { + if s, ok := v.(string); ok { + cp.Concerns = append(cp.Concerns, s) + } + } + } + if dates, ok := profile["important_dates"].([]interface{}); ok { + for _, v := range dates { + if s, ok := v.(string); ok { + cp.ImportantDates = append(cp.ImportantDates, s) + } + } + } + if weeks, ok := profile["baby_age_weeks"].(float64); ok { + w := int(weeks) + cp.BabyAgeWeeks = &w + } + + return cp +} diff --git a/backend/internal/service/community.go b/backend/internal/service/community.go new file mode 100644 index 00000000..2ff15b85 --- /dev/null +++ b/backend/internal/service/community.go @@ -0,0 +1,889 @@ +package service + +import ( + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/momshell/backend/internal/dto" + "github.com/momshell/backend/internal/model" + "github.com/momshell/backend/internal/repository" + "gorm.io/gorm" +) + +type CommunityService struct { + questionRepo *repository.QuestionRepo + answerRepo *repository.AnswerRepo + commentRepo *repository.CommentRepo + interactionRepo *repository.InteractionRepo + tagRepo *repository.TagRepo + userRepo *repository.UserRepo + moderation *ModerationService +} + +func NewCommunityService( + questionRepo *repository.QuestionRepo, + answerRepo *repository.AnswerRepo, + commentRepo *repository.CommentRepo, + interactionRepo *repository.InteractionRepo, + tagRepo *repository.TagRepo, + userRepo *repository.UserRepo, + moderation *ModerationService, +) *CommunityService { + return &CommunityService{ + questionRepo: questionRepo, + answerRepo: answerRepo, + commentRepo: commentRepo, + interactionRepo: interactionRepo, + tagRepo: tagRepo, + userRepo: userRepo, + moderation: moderation, + } +} + +// ==================== Helper Methods ==================== + +func (s *CommunityService) IsCertifiedProfessional(user *model.User) bool { + return model.ProfessionalRoles[user.Role] && + user.Certification != nil && + user.Certification.Status == model.CertApproved +} + +func (s *CommunityService) BuildAuthorInfo(user *model.User) dto.AuthorInfo { + info := dto.AuthorInfo{ + ID: user.ID, + Nickname: user.Nickname, + AvatarURL: user.AvatarURL, + Role: string(user.Role), + } + + if user.Certification != nil && user.Certification.Status == model.CertApproved { + info.IsCertified = true + title := "" + if user.Certification.HospitalOrInstitution != "" { + title += user.Certification.HospitalOrInstitution + } + if user.Certification.Department != nil && *user.Certification.Department != "" { + title += " " + *user.Certification.Department + } + if user.Certification.Title != nil && *user.Certification.Title != "" { + title += " " + *user.Certification.Title + } + if title != "" { + info.CertificationTitle = &title + } + } + + return info +} + +func buildTagInfo(tag model.Tag) dto.TagInfo { + return dto.TagInfo{ID: tag.ID, Name: tag.Name, Slug: tag.Slug} +} + +func contentPreview(content string, maxLen int) string { + if len(content) > maxLen { + return content[:maxLen] + "..." + } + return content +} + +// ==================== Question Operations ==================== + +func (s *CommunityService) GetQuestions(params dto.QuestionListParams, currentUserID string) (*dto.PaginatedResponse, error) { + questions, total, err := s.questionRepo.FindAll( + params.Channel, + params.TagID, + model.StatusPublished, + params.GetSortBy(), + params.GetOrder(), + params.GetOffset(), + params.GetPageSize(), + ) + if err != nil { + return nil, err + } + + // Get user's likes and collections + questionIDs := make([]string, len(questions)) + for i, q := range questions { + questionIDs[i] = q.ID + } + + likedIDs := make(map[string]bool) + collectedIDs := make(map[string]bool) + if currentUserID != "" && len(questionIDs) > 0 { + likedIDs, _ = s.interactionRepo.FindLikedTargetIDs(currentUserID, "question", questionIDs) + collectedIDs, _ = s.interactionRepo.FindCollectedQuestionIDs(currentUserID, questionIDs) + } + + items := make([]dto.QuestionListItem, 0, len(questions)) + for _, q := range questions { + tags := make([]dto.TagInfo, 0, len(q.Tags)) + for _, t := range q.Tags { + tags = append(tags, buildTagInfo(t)) + } + + items = append(items, dto.QuestionListItem{ + ID: q.ID, + Title: q.Title, + ContentPreview: contentPreview(q.Content, 100), + Channel: string(q.Channel), + Author: s.BuildAuthorInfo(&q.Author), + Tags: tags, + ViewCount: q.ViewCount, + AnswerCount: q.AnswerCount, + LikeCount: q.LikeCount, + CollectionCount: q.CollectionCount, + IsPinned: q.IsPinned, + IsFeatured: q.IsFeatured, + HasAcceptedAnswer: q.AcceptedAnswerID != nil, + IsLiked: likedIDs[q.ID], + IsCollected: collectedIDs[q.ID], + CreatedAt: q.CreatedAt, + }) + } + + page := params.GetPage() + pageSize := params.GetPageSize() + resp := dto.NewPaginatedResponse(items, total, page, pageSize) + return &resp, nil +} + +func (s *CommunityService) GetQuestion(questionID, currentUserID string) (*dto.QuestionDetail, error) { + q, err := s.questionRepo.FindByID(questionID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("问题不存在") + } + return nil, err + } + + // Increment view count + _ = s.questionRepo.IncrementViewCount(questionID) + q.ViewCount++ + + // Check user interactions + isLiked := false + isCollected := false + if currentUserID != "" { + if _, err := s.interactionRepo.FindLike(currentUserID, "question", questionID); err == nil { + isLiked = true + } + if _, err := s.interactionRepo.FindCollection(currentUserID, questionID); err == nil { + isCollected = true + } + } + + // Count professional vs experience answers + proCount, _ := s.answerRepo.CountByQuestionID(questionID, true) + expCount, _ := s.answerRepo.CountByQuestionID(questionID, false) + + var imageURLs []string + if q.ImageURLs != nil { + _ = json.Unmarshal([]byte(*q.ImageURLs), &imageURLs) + } + if imageURLs == nil { + imageURLs = []string{} + } + + tags := make([]dto.TagInfo, 0, len(q.Tags)) + for _, t := range q.Tags { + tags = append(tags, buildTagInfo(t)) + } + + return &dto.QuestionDetail{ + ID: q.ID, + Title: q.Title, + Content: q.Content, + ContentPreview: contentPreview(q.Content, 100), + Channel: string(q.Channel), + Status: string(q.Status), + Author: s.BuildAuthorInfo(&q.Author), + Tags: tags, + ImageURLs: imageURLs, + ViewCount: q.ViewCount, + AnswerCount: q.AnswerCount, + LikeCount: q.LikeCount, + CollectionCount: q.CollectionCount, + IsPinned: q.IsPinned, + IsFeatured: q.IsFeatured, + HasAcceptedAnswer: q.AcceptedAnswerID != nil, + AcceptedAnswerID: q.AcceptedAnswerID, + IsLiked: isLiked, + IsCollected: isCollected, + ProfessionalAnswerCount: int(proCount), + ExperienceAnswerCount: int(expCount), + CreatedAt: q.CreatedAt, + UpdatedAt: q.UpdatedAt, + PublishedAt: q.PublishedAt, + }, nil +} + +func (s *CommunityService) CreateQuestion(req dto.QuestionCreate, author *model.User) (*model.Question, error) { + // Content moderation + titleDecision := s.moderation.ModerateText(req.Title) + if titleDecision.Result == model.ModerationRejected { + return nil, fmt.Errorf("标题审核未通过: %s", derefStr(titleDecision.Reason)) + } + + contentDecision := s.moderation.ModerateText(req.Content) + if contentDecision.Result == model.ModerationRejected { + return nil, fmt.Errorf("内容审核未通过: %s", derefStr(contentDecision.Reason)) + } + + // Determine status + status := model.StatusPublished + var publishedAt *time.Time + now := time.Now() + if titleDecision.Result == model.ModerationPassed && contentDecision.Result == model.ModerationPassed { + publishedAt = &now + } else { + status = model.StatusPendingReview + } + + var imageURLsJSON *string + if len(req.ImageURLs) > 0 { + b, _ := json.Marshal(req.ImageURLs) + s := string(b) + imageURLsJSON = &s + } + + q := &model.Question{ + AuthorID: author.ID, + Title: req.Title, + Content: req.Content, + Channel: model.ChannelType(req.Channel), + Status: status, + PublishedAt: publishedAt, + ImageURLs: imageURLsJSON, + } + + if err := s.questionRepo.Create(q); err != nil { + return nil, err + } + + // Associate tags + for _, tagID := range req.TagIDs { + _ = s.tagRepo.CreateQuestionTag(&model.QuestionTag{ + QuestionID: q.ID, + TagID: tagID, + }) + _ = s.tagRepo.IncrementQuestionCount(tagID) + } + + return q, nil +} + +func (s *CommunityService) UpdateQuestion(questionID string, req dto.QuestionUpdate, user *model.User) (*model.Question, error) { + q, err := s.questionRepo.FindByID(questionID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("问题不存在") + } + return nil, err + } + + if q.AuthorID != user.ID && user.Role != model.RoleAdmin { + return nil, errors.New("无权修改此问题") + } + + if req.Title != nil { + decision := s.moderation.ModerateText(*req.Title) + if decision.Result == model.ModerationRejected { + return nil, fmt.Errorf("标题审核未通过: %s", derefStr(decision.Reason)) + } + q.Title = *req.Title + if decision.Result == model.ModerationNeedManualReview { + q.Status = model.StatusPendingReview + } + } + + if req.Content != nil { + decision := s.moderation.ModerateText(*req.Content) + if decision.Result == model.ModerationRejected { + return nil, fmt.Errorf("内容审核未通过: %s", derefStr(decision.Reason)) + } + q.Content = *req.Content + if decision.Result == model.ModerationNeedManualReview { + q.Status = model.StatusPendingReview + } + } + + if req.TagIDs != nil { + _ = s.tagRepo.DeleteQuestionTags(questionID) + for _, tagID := range req.TagIDs { + _ = s.tagRepo.CreateQuestionTag(&model.QuestionTag{ + QuestionID: questionID, + TagID: tagID, + }) + } + } + + q.UpdatedAt = time.Now() + if err := s.questionRepo.Update(q); err != nil { + return nil, err + } + + return q, nil +} + +func (s *CommunityService) DeleteQuestion(questionID string, user *model.User) error { + q, err := s.questionRepo.FindByID(questionID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("问题不存在") + } + return err + } + + if q.AuthorID != user.ID && user.Role != model.RoleAdmin { + return errors.New("无权删除此问题") + } + + // Clean up related data + _ = s.interactionRepo.DeleteLikesByTarget("question", questionID) + _ = s.interactionRepo.DeleteCollectionsByQuestion(questionID) + _ = s.tagRepo.DeleteQuestionTags(questionID) + + return s.questionRepo.Delete(questionID) +} + +// ==================== Answer Operations ==================== + +func (s *CommunityService) GetAnswers(questionID string, params dto.AnswerListParams, currentUserID string) (*dto.PaginatedResponse, error) { + answers, total, err := s.answerRepo.FindByQuestionID( + questionID, + params.IsProfessional, + params.GetSortBy(), + params.GetOrder(), + params.GetOffset(), + params.GetPageSize(), + ) + if err != nil { + return nil, err + } + + answerIDs := make([]string, len(answers)) + for i, a := range answers { + answerIDs[i] = a.ID + } + + likedIDs := make(map[string]bool) + if currentUserID != "" && len(answerIDs) > 0 { + likedIDs, _ = s.interactionRepo.FindLikedTargetIDs(currentUserID, "answer", answerIDs) + } + + items := make([]dto.AnswerListItem, 0, len(answers)) + for _, a := range answers { + items = append(items, dto.AnswerListItem{ + ID: a.ID, + QuestionID: a.QuestionID, + Author: s.BuildAuthorInfo(&a.Author), + Content: a.Content, + ContentPreview: contentPreview(a.Content, 200), + IsProfessional: a.IsProfessional, + IsAccepted: a.IsAccepted, + LikeCount: a.LikeCount, + CommentCount: a.CommentCount, + IsLiked: likedIDs[a.ID], + CreatedAt: a.CreatedAt, + }) + } + + page := params.GetPage() + pageSize := params.GetPageSize() + resp := dto.NewPaginatedResponse(items, total, page, pageSize) + return &resp, nil +} + +func (s *CommunityService) CreateAnswer(questionID string, req dto.AnswerCreate, author *model.User) (*model.Answer, error) { + // Check question exists + q, err := s.questionRepo.FindByID(questionID) + if err != nil { + return nil, errors.New("问题不存在") + } + + // Check permission for professional channel + if q.Channel == model.ChannelProfessional { + isAuthor := q.AuthorID == author.ID + if !s.IsCertifiedProfessional(author) && author.Role != model.RoleAdmin && !isAuthor { + return nil, errors.New("专业频道仅限认证专业人士回答") + } + } + + // Content moderation + decision := s.moderation.ModerateText(req.Content) + if decision.Result == model.ModerationRejected { + return nil, fmt.Errorf("内容审核未通过: %s", derefStr(decision.Reason)) + } + + status := model.StatusPublished + if decision.Result != model.ModerationPassed { + status = model.StatusPendingReview + } + + var imageURLsJSON *string + if len(req.ImageURLs) > 0 { + b, _ := json.Marshal(req.ImageURLs) + s := string(b) + imageURLsJSON = &s + } + + answer := &model.Answer{ + QuestionID: questionID, + AuthorID: author.ID, + Content: req.Content, + AuthorRole: author.Role, + IsProfessional: s.IsCertifiedProfessional(author), + Status: status, + ImageURLs: imageURLsJSON, + } + + if err := s.answerRepo.Create(answer); err != nil { + return nil, err + } + + _ = s.questionRepo.IncrementAnswerCount(questionID) + + return answer, nil +} + +func (s *CommunityService) UpdateAnswer(answerID string, req dto.AnswerUpdate, user *model.User) (*model.Answer, error) { + a, err := s.answerRepo.FindByID(answerID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("回答不存在") + } + return nil, err + } + + if a.AuthorID != user.ID && user.Role != model.RoleAdmin { + return nil, errors.New("无权修改此回答") + } + + if req.Content != nil { + decision := s.moderation.ModerateText(*req.Content) + if decision.Result == model.ModerationRejected { + return nil, fmt.Errorf("内容审核未通过: %s", derefStr(decision.Reason)) + } + a.Content = *req.Content + if decision.Result == model.ModerationNeedManualReview { + a.Status = model.StatusPendingReview + } + } + + a.UpdatedAt = time.Now() + if err := s.answerRepo.Update(a); err != nil { + return nil, err + } + + return a, nil +} + +func (s *CommunityService) DeleteAnswer(answerID string, user *model.User) error { + a, err := s.answerRepo.FindByID(answerID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("回答不存在") + } + return err + } + + isAnswerAuthor := a.AuthorID == user.ID + isQuestionAuthor := a.Question.AuthorID == user.ID + isAdmin := user.Role == model.RoleAdmin + + if !isAnswerAuthor && !isQuestionAuthor && !isAdmin { + return errors.New("无权删除此回答") + } + + _ = s.interactionRepo.DeleteLikesByTarget("answer", answerID) + _ = s.questionRepo.DecrementAnswerCount(a.QuestionID) + + return s.answerRepo.Delete(answerID) +} + +// ==================== Comment Operations ==================== + +func (s *CommunityService) GetComments(answerID, currentUserID string) ([]dto.CommentListItem, error) { + comments, err := s.commentRepo.FindByAnswerID(answerID) + if err != nil { + return nil, err + } + + commentIDs := make([]string, len(comments)) + for i, c := range comments { + commentIDs[i] = c.ID + } + + likedIDs := make(map[string]bool) + if currentUserID != "" && len(commentIDs) > 0 { + likedIDs, _ = s.interactionRepo.FindLikedTargetIDs(currentUserID, "comment", commentIDs) + } + + // Build map for nested structure + commentMap := make(map[string]*model.Comment) + for i := range comments { + commentMap[comments[i].ID] = &comments[i] + } + + // Find root ancestor + var findRootID func(string) string + findRootID = func(id string) string { + c, ok := commentMap[id] + if !ok || c.ParentID == nil { + return id + } + return findRootID(*c.ParentID) + } + + rootItems := make(map[string]*dto.CommentListItem) + var result []dto.CommentListItem + + // First pass: root comments + for _, c := range comments { + if c.ParentID == nil { + item := dto.CommentListItem{ + ID: c.ID, + AnswerID: c.AnswerID, + Author: s.BuildAuthorInfo(&c.Author), + Content: c.Content, + ParentID: nil, + LikeCount: c.LikeCount, + IsLiked: likedIDs[c.ID], + CreatedAt: c.CreatedAt, + Replies: []dto.CommentListItem{}, + } + result = append(result, item) + rootItems[c.ID] = &result[len(result)-1] + } + } + + // Second pass: replies + for _, c := range comments { + if c.ParentID != nil { + rootID := findRootID(c.ID) + if root, ok := rootItems[rootID]; ok { + var replyToUser *dto.AuthorInfo + if c.ReplyToUser != nil { + info := s.BuildAuthorInfo(c.ReplyToUser) + replyToUser = &info + } + reply := dto.CommentListItem{ + ID: c.ID, + AnswerID: c.AnswerID, + Author: s.BuildAuthorInfo(&c.Author), + Content: c.Content, + ParentID: &rootID, + ReplyToUser: replyToUser, + LikeCount: c.LikeCount, + IsLiked: likedIDs[c.ID], + CreatedAt: c.CreatedAt, + Replies: []dto.CommentListItem{}, + } + root.Replies = append(root.Replies, reply) + } + } + } + + if result == nil { + result = []dto.CommentListItem{} + } + return result, nil +} + +func (s *CommunityService) CreateComment(answerID string, req dto.CommentCreate, user *model.User) (*dto.CommentListItem, error) { + // Check answer exists + _, err := s.answerRepo.FindByID(answerID) + if err != nil { + return nil, errors.New("回答不存在") + } + + // If replying, verify parent + var replyToUserID *string + if req.ParentID != nil { + parent, err := s.commentRepo.FindByID(*req.ParentID) + if err != nil || parent.AnswerID != answerID { + return nil, errors.New("回复目标不存在") + } + replyToUserID = &parent.AuthorID + } + + // Moderation + decision := s.moderation.ModerateText(req.Content) + if decision.Result == model.ModerationRejected { + return nil, fmt.Errorf("评论审核未通过: %s", derefStr(decision.Reason)) + } + + status := model.StatusPublished + if decision.Result != model.ModerationPassed { + status = model.StatusPendingReview + } + + comment := &model.Comment{ + AnswerID: answerID, + AuthorID: user.ID, + ParentID: req.ParentID, + ReplyToUserID: replyToUserID, + Content: req.Content, + Status: status, + } + + if err := s.commentRepo.Create(comment); err != nil { + return nil, err + } + + _ = s.answerRepo.IncrementCommentCount(answerID) + + return &dto.CommentListItem{ + ID: comment.ID, + AnswerID: comment.AnswerID, + Author: s.BuildAuthorInfo(user), + Content: comment.Content, + ParentID: comment.ParentID, + LikeCount: 0, + IsLiked: false, + CreatedAt: comment.CreatedAt, + Replies: []dto.CommentListItem{}, + }, nil +} + +func (s *CommunityService) DeleteComment(commentID string, user *model.User) error { + comment, err := s.commentRepo.FindByID(commentID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("评论不存在") + } + return err + } + + if comment.AuthorID != user.ID && user.Role != model.RoleAdmin { + return errors.New("无权删除此评论") + } + + // Delete children + childrenDeleted, _ := s.commentRepo.DeleteByParentID(commentID) + + // Delete comment itself + if err := s.commentRepo.Delete(commentID); err != nil { + return err + } + + // Decrement answer comment count (1 for this + children) + _ = s.answerRepo.DecrementCommentCount(comment.AnswerID, int(childrenDeleted)+1) + + return nil +} + +// ==================== Like Operations ==================== + +func (s *CommunityService) ToggleLike(userID, targetType, targetID string) (bool, int, error) { + _, err := s.interactionRepo.FindLike(userID, targetType, targetID) + if err == nil { + // Unlike + if err := s.interactionRepo.DeleteLike(userID, targetType, targetID); err != nil { + return false, 0, err + } + s.updateTargetLikeCount(targetType, targetID, -1) + count := s.getTargetLikeCount(targetType, targetID) + return false, count, nil + } + + // Like + like := &model.Like{ + UserID: userID, + TargetType: targetType, + TargetID: targetID, + } + if err := s.interactionRepo.CreateLike(like); err != nil { + return false, 0, err + } + s.updateTargetLikeCount(targetType, targetID, 1) + count := s.getTargetLikeCount(targetType, targetID) + return true, count, nil +} + +func (s *CommunityService) updateTargetLikeCount(targetType, targetID string, delta int) { + switch targetType { + case "question": + _ = s.questionRepo.UpdateLikeCount(targetID, delta) + case "answer": + _ = s.answerRepo.UpdateLikeCount(targetID, delta) + case "comment": + _ = s.commentRepo.UpdateLikeCount(targetID, delta) + } +} + +func (s *CommunityService) getTargetLikeCount(targetType, targetID string) int { + switch targetType { + case "question": + if q, err := s.questionRepo.FindByID(targetID); err == nil { + return q.LikeCount + } + case "answer": + if a, err := s.answerRepo.FindByID(targetID); err == nil { + return a.LikeCount + } + case "comment": + if c, err := s.commentRepo.FindByID(targetID); err == nil { + return c.LikeCount + } + } + return 0 +} + +// ==================== Collection Operations ==================== + +func (s *CommunityService) ToggleCollection(userID, questionID string, folderName, note *string) (bool, int, error) { + _, err := s.interactionRepo.FindCollection(userID, questionID) + if err == nil { + // Uncollect + if err := s.interactionRepo.DeleteCollection(userID, questionID); err != nil { + return false, 0, err + } + _ = s.questionRepo.UpdateCollectionCount(questionID, -1) + if q, err := s.questionRepo.FindByID(questionID); err == nil { + return false, q.CollectionCount, nil + } + return false, 0, nil + } + + // Collect + c := &model.Collection{ + UserID: userID, + QuestionID: questionID, + FolderName: folderName, + Note: note, + } + if err := s.interactionRepo.CreateCollection(c); err != nil { + return false, 0, err + } + _ = s.questionRepo.UpdateCollectionCount(questionID, 1) + if q, err := s.questionRepo.FindByID(questionID); err == nil { + return true, q.CollectionCount, nil + } + return true, 1, nil +} + +func (s *CommunityService) GetUserCollections(userID string, page, pageSize int) (*dto.PaginatedResponse, error) { + offset := (page - 1) * pageSize + collections, total, err := s.interactionRepo.FindUserCollections(userID, offset, pageSize) + if err != nil { + return nil, err + } + + // Get liked question IDs + questionIDs := make([]string, 0, len(collections)) + for _, c := range collections { + questionIDs = append(questionIDs, c.QuestionID) + } + likedIDs, _ := s.interactionRepo.FindLikedTargetIDs(userID, "question", questionIDs) + + items := make([]dto.CollectionItem, 0, len(collections)) + for _, c := range collections { + q := c.Question + tags := make([]dto.TagInfo, 0) + for _, t := range q.Tags { + tags = append(tags, buildTagInfo(t)) + } + + items = append(items, dto.CollectionItem{ + ID: c.ID, + Question: dto.QuestionListItem{ + ID: q.ID, + Title: q.Title, + ContentPreview: contentPreview(q.Content, 100), + Channel: string(q.Channel), + Author: s.BuildAuthorInfo(&q.Author), + Tags: tags, + ViewCount: q.ViewCount, + AnswerCount: q.AnswerCount, + LikeCount: q.LikeCount, + CollectionCount: q.CollectionCount, + IsPinned: q.IsPinned, + IsFeatured: q.IsFeatured, + HasAcceptedAnswer: q.AcceptedAnswerID != nil, + IsLiked: likedIDs[q.ID], + IsCollected: true, + CreatedAt: q.CreatedAt, + }, + FolderName: c.FolderName, + Note: c.Note, + CreatedAt: c.CreatedAt, + }) + } + + resp := dto.NewPaginatedResponse(items, total, page, pageSize) + return &resp, nil +} + +// ==================== Tag Operations ==================== + +func (s *CommunityService) GetTags() ([]dto.TagListItem, error) { + tags, err := s.tagRepo.FindAll() + if err != nil { + return nil, err + } + + items := make([]dto.TagListItem, 0, len(tags)) + for _, t := range tags { + items = append(items, dto.TagListItem{ + ID: t.ID, + Name: t.Name, + Slug: t.Slug, + Description: t.Description, + QuestionCount: t.QuestionCount, + FollowerCount: t.FollowerCount, + IsActive: t.IsActive, + IsFeatured: t.IsFeatured, + }) + } + return items, nil +} + +func (s *CommunityService) GetHotTags(limit int) ([]dto.TagListItem, error) { + if limit <= 0 { + limit = 20 + } + tags, err := s.tagRepo.FindHot(limit) + if err != nil { + return nil, err + } + + items := make([]dto.TagListItem, 0, len(tags)) + for _, t := range tags { + items = append(items, dto.TagListItem{ + ID: t.ID, + Name: t.Name, + Slug: t.Slug, + Description: t.Description, + QuestionCount: t.QuestionCount, + FollowerCount: t.FollowerCount, + IsActive: t.IsActive, + IsFeatured: t.IsFeatured, + }) + } + return items, nil +} + +func (s *CommunityService) CreateTag(req dto.TagCreate) (*model.Tag, error) { + tag := &model.Tag{ + Name: req.Name, + Slug: req.Slug, + Description: req.Description, + } + if err := s.tagRepo.Create(tag); err != nil { + return nil, err + } + return tag, nil +} + +// helper +func derefStr(s *string) string { + if s == nil { + return "" + } + return *s +} diff --git a/backend/internal/service/echo.go b/backend/internal/service/echo.go new file mode 100644 index 00000000..0c827a45 --- /dev/null +++ b/backend/internal/service/echo.go @@ -0,0 +1,279 @@ +package service + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "regexp" + "strings" + + "github.com/momshell/backend/internal/dto" + "github.com/momshell/backend/internal/model" + "github.com/momshell/backend/internal/repository" + "github.com/momshell/backend/pkg/openai" +) + +type EchoService struct { + client *openai.Client + echoRepo *repository.EchoRepo +} + +func NewEchoService(client *openai.Client, echoRepo *repository.EchoRepo) *EchoService { + return &EchoService{ + client: client, + echoRepo: echoRepo, + } +} + +func (s *EchoService) GetIdentityTags(userID string) (*dto.IdentityTagListResponse, error) { + tags, err := s.echoRepo.FindIdentityTagsByUserID(userID) + if err != nil { + return nil, err + } + + resp := &dto.IdentityTagListResponse{ + Music: []model.IdentityTag{}, + Sound: []model.IdentityTag{}, + Literature: []model.IdentityTag{}, + Memory: []model.IdentityTag{}, + } + + for _, tag := range tags { + switch tag.TagType { + case "music": + resp.Music = append(resp.Music, tag) + case "sound": + resp.Sound = append(resp.Sound, tag) + case "literature": + resp.Literature = append(resp.Literature, tag) + case "memory": + resp.Memory = append(resp.Memory, tag) + } + } + + return resp, nil +} + +func (s *EchoService) CreateIdentityTag(userID string, req dto.IdentityTagCreateRequest) (*model.IdentityTag, error) { + if !isValidTagType(req.TagType) { + return nil, fmt.Errorf("invalid tag_type") + } + + tag := &model.IdentityTag{ + UserID: userID, + TagType: req.TagType, + Content: strings.TrimSpace(req.Content), + } + + if tag.Content == "" { + return nil, fmt.Errorf("content is required") + } + + if err := s.echoRepo.CreateIdentityTag(tag); err != nil { + return nil, err + } + + return tag, nil +} + +func (s *EchoService) DeleteIdentityTag(userID, tagID string) error { + if _, err := s.echoRepo.FindIdentityTagByIDAndUserID(tagID, userID); err != nil { + return err + } + + return s.echoRepo.DeleteIdentityTag(tagID, userID) +} + +func (s *EchoService) GetMemoirs(userID string, limit, offset int) (*dto.MemoirListResponse, error) { + memoirs, total, err := s.echoRepo.FindMemoirsByUserID(userID, limit, offset) + if err != nil { + return nil, err + } + + if memoirs == nil { + memoirs = []model.Memoir{} + } + + return &dto.MemoirListResponse{ + Memoirs: memoirs, + Total: total, + }, nil +} + +func (s *EchoService) GenerateMemoir(ctx context.Context, userID string, req dto.GenerateMemoirRequest) (*model.Memoir, error) { + tags, err := s.echoRepo.FindIdentityTagsByUserID(userID) + if err != nil { + return nil, err + } + + systemPrompt := buildMemoirSystemPrompt(tags, req.Theme) + messages := []openai.Message{ + {Role: "system", Content: systemPrompt}, + {Role: "user", Content: "请创作一篇温暖、真诚、有细节的回忆录。"}, + } + + rawContent, err := s.client.Chat(ctx, messages) + if err != nil { + return nil, fmt.Errorf("AI 服务调用失败: %w", err) + } + + parsed := parseMemoirLLMResponse(rawContent) + title, _ := parsed["title"].(string) + content, _ := parsed["content"].(string) + + title = strings.TrimSpace(title) + content = strings.TrimSpace(content) + + if title == "" { + title = "一段温柔的回响" + } + if content == "" { + content = "那些被日常轻轻覆盖的瞬间,在回望时仍有温度。" + } + + cover := generateMemoirCoverDataURI(title, req.Theme) + memoir := &model.Memoir{ + UserID: userID, + Title: title, + Content: content, + CoverImageURL: &cover, + Theme: req.Theme, + } + + if err := s.echoRepo.CreateMemoir(memoir); err != nil { + return nil, err + } + + return memoir, nil +} + +func (s *EchoService) RateMemoir(userID, memoirID string, rating int) (*model.Memoir, error) { + if rating < 1 || rating > 5 { + return nil, fmt.Errorf("rating must be between 1 and 5") + } + + memoir, err := s.echoRepo.FindMemoirByIDAndUserID(memoirID, userID) + if err != nil { + return nil, err + } + + memoir.UserRating = &rating + if err := s.echoRepo.UpdateMemoir(memoir); err != nil { + return nil, err + } + + return memoir, nil +} + +func isValidTagType(tagType string) bool { + switch tagType { + case "music", "sound", "literature", "memory": + return true + default: + return false + } +} + +func buildMemoirSystemPrompt(tags []model.IdentityTag, theme *string) string { + tagLines := "- (暂无身份标签)" + if len(tags) > 0 { + parts := make([]string, 0, len(tags)) + for _, tag := range tags { + parts = append(parts, fmt.Sprintf("- [%s] %s", tag.TagType, tag.Content)) + } + tagLines = strings.Join(parts, "\n") + } + + themeText := "(无特定主题)" + if theme != nil && strings.TrimSpace(*theme) != "" { + themeText = strings.TrimSpace(*theme) + } + + return fmt.Sprintf(`你是一位温柔的记忆编织者。根据用户的身份标签和主题,创作一段温暖的回忆录。 + +用户的身份标签: +%s + +用户给出的主题: +%s + +请以 JSON 格式回复: +{"title": "诗意的标题", "content": "2-4段温暖的回忆文字"}`, + tagLines, + themeText, + ) +} + +func parseMemoirLLMResponse(content string) map[string]interface{} { + var result map[string]interface{} + + if err := json.Unmarshal([]byte(content), &result); err == nil { + return result + } + + re := regexp.MustCompile("(?s)```json\\s*(.*?)\\s*```") + if matches := re.FindStringSubmatch(content); len(matches) > 1 { + if err := json.Unmarshal([]byte(matches[1]), &result); err == nil { + return result + } + } + + re2 := regexp.MustCompile(`(?s)\{.*\}`) + if match := re2.FindString(content); match != "" { + if err := json.Unmarshal([]byte(match), &result); err == nil { + return result + } + } + + return map[string]interface{}{ + "title": "一段温柔的回响", + "content": strings.TrimSpace(content), + } +} + +func generateMemoirCoverDataURI(title string, theme *string) string { + gradients := [][2]string{ + {"F6A6B2", "F9D29D"}, + {"D4A5FF", "F7C3D6"}, + {"F4B183", "F7E7A9"}, + {"C5D8A4", "F4C2C2"}, + {"F8B4C7", "CDB4DB"}, + {"F7C59F", "F6E7CB"}, + } + + seed := title + if theme != nil { + seed += *theme + } + + index := hashString(seed) % len(gradients) + color1 := gradients[index][0] + color2 := gradients[index][1] + text := truncateText(title, 24) + + return fmt.Sprintf("data:image/svg+xml,%%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='300'%%3E%%3Cdefs%%3E%%3ClinearGradient id='g' x1='0%%25' y1='0%%25' x2='100%%25' y2='100%%25'%%3E%%3Cstop offset='0%%25' style='stop-color:%%23%s'%%2F%%3E%%3Cstop offset='100%%25' style='stop-color:%%23%s'%%2F%%3E%%3C%%2FlinearGradient%%3E%%3C%%2Fdefs%%3E%%3Crect width='400' height='300' fill='url(%%23g)'%%2F%%3E%%3Ctext x='200' y='160' text-anchor='middle' fill='white' font-size='20' font-family='sans-serif'%%3E%s%%3C%%2Ftext%%3E%%3C%%2Fsvg%%3E", + color1, + color2, + url.PathEscape(text), + ) +} + +func hashString(value string) int { + h := 0 + for _, ch := range value { + h = (h*31 + int(ch)) % 100000 + } + if h < 0 { + return -h + } + return h +} + +func truncateText(value string, maxRunes int) string { + runes := []rune(value) + if len(runes) <= maxRunes { + return value + } + return string(runes[:maxRunes]) + "..." +} diff --git a/backend/internal/service/moderation.go b/backend/internal/service/moderation.go new file mode 100644 index 00000000..14415a07 --- /dev/null +++ b/backend/internal/service/moderation.go @@ -0,0 +1,158 @@ +package service + +import ( + "strings" + + "github.com/momshell/backend/internal/model" +) + +// SensitiveCategory represents categories of sensitive content +type SensitiveCategory string + +const ( + CatPseudoscience SensitiveCategory = "pseudoscience" + CatDepressionTrigger SensitiveCategory = "depression_trigger" + CatSoftPornography SensitiveCategory = "soft_pornography" + CatViolence SensitiveCategory = "violence" + CatSpam SensitiveCategory = "spam" + CatMisinformation SensitiveCategory = "misinformation" + CatSelfHarm SensitiveCategory = "self_harm" + CatPolitical SensitiveCategory = "political" + CatHarassment SensitiveCategory = "harassment" +) + +var autoRejectCategories = map[SensitiveCategory]bool{ + CatPseudoscience: true, + CatSoftPornography: true, + CatViolence: true, + CatSpam: true, + CatHarassment: true, +} + +var crisisCategories = map[SensitiveCategory]bool{ + CatDepressionTrigger: true, + CatSelfHarm: true, +} + +var manualReviewCategories = map[SensitiveCategory]bool{ + CatMisinformation: true, + CatPolitical: true, +} + +// keyword => category mapping +var sensitiveKeywords = map[string]SensitiveCategory{ + // Pseudoscience + "包治百病": CatPseudoscience, + "神奇疗法": CatPseudoscience, + "祖传秘方": CatPseudoscience, + // Spam + "加微信": CatSpam, + "免费领": CatSpam, + "点击链接": CatSpam, + // Violence + "打死": CatViolence, + "杀了": CatViolence, + // Self harm + "不想活": CatSelfHarm, + "自杀": CatSelfHarm, + "自残": CatSelfHarm, + "活着没意思": CatSelfHarm, + // Harassment + "废物": CatHarassment, + "垃圾人": CatHarassment, +} + +// ModerationDecision holds the result of content moderation +type ModerationDecision struct { + Result model.ModerationResult + Categories []SensitiveCategory + Confidence float64 + Reason *string +} + +// ModerationService handles content moderation +type ModerationService struct{} + +func NewModerationService() *ModerationService { + return &ModerationService{} +} + +func (s *ModerationService) ModerateText(content string) ModerationDecision { + detected := s.scanKeywords(content) + + if len(detected) == 0 { + return ModerationDecision{ + Result: model.ModerationPassed, + Confidence: 1.0, + } + } + + // Check crisis categories + for _, cat := range detected { + if crisisCategories[cat] { + reason := "检测到可能存在心理危机,已推送帮助资源。如需发布,请修改内容后重试。" + return ModerationDecision{ + Result: model.ModerationRejected, + Categories: detected, + Confidence: 0.95, + Reason: &reason, + } + } + } + + // Check auto-reject + for _, cat := range detected { + if autoRejectCategories[cat] { + reasons := map[SensitiveCategory]string{ + CatPseudoscience: "内容可能包含未经证实的医疗信息", + CatSoftPornography: "内容包含不适当信息", + CatViolence: "内容包含暴力相关信息", + CatSpam: "内容疑似广告或垃圾信息", + CatHarassment: "内容包含不友善言论", + } + reason := reasons[cat] + return ModerationDecision{ + Result: model.ModerationRejected, + Categories: detected, + Confidence: 0.9, + Reason: &reason, + } + } + } + + // Check manual review + for _, cat := range detected { + if manualReviewCategories[cat] { + reason := "内容需要人工审核" + return ModerationDecision{ + Result: model.ModerationNeedManualReview, + Categories: detected, + Confidence: 0.7, + Reason: &reason, + } + } + } + + return ModerationDecision{ + Result: model.ModerationPassed, + Categories: detected, + Confidence: 0.8, + } +} + +func (s *ModerationService) scanKeywords(content string) []SensitiveCategory { + lower := strings.ToLower(content) + seen := make(map[SensitiveCategory]bool) + var result []SensitiveCategory + + for keyword, category := range sensitiveKeywords { + if strings.Contains(lower, keyword) { + if !seen[category] { + seen[category] = true + result = append(result, category) + } + } + } + + return result +} diff --git a/backend/internal/service/user.go b/backend/internal/service/user.go new file mode 100644 index 00000000..6d4878ad --- /dev/null +++ b/backend/internal/service/user.go @@ -0,0 +1,218 @@ +package service + +import ( + "errors" + + "github.com/momshell/backend/internal/dto" + "github.com/momshell/backend/internal/model" + "github.com/momshell/backend/internal/repository" + "gorm.io/gorm" +) + +type UserService struct { + db *gorm.DB + userRepo *repository.UserRepo + questionRepo *repository.QuestionRepo + answerRepo *repository.AnswerRepo + interactionRepo *repository.InteractionRepo + community *CommunityService +} + +func NewUserService( + db *gorm.DB, + userRepo *repository.UserRepo, + questionRepo *repository.QuestionRepo, + answerRepo *repository.AnswerRepo, + interactionRepo *repository.InteractionRepo, + community *CommunityService, +) *UserService { + return &UserService{ + db: db, + userRepo: userRepo, + questionRepo: questionRepo, + answerRepo: answerRepo, + interactionRepo: interactionRepo, + community: community, + } +} + +func (s *UserService) GetProfile(userID string) (*dto.UserProfile, error) { + user, err := s.userRepo.FindByID(userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("用户不存在") + } + return nil, err + } + + stats, err := s.getUserStats(userID) + if err != nil { + return nil, err + } + + info := s.community.BuildAuthorInfo(user) + + return &dto.UserProfile{ + ID: user.ID, + Nickname: user.Nickname, + Email: user.Email, + AvatarURL: user.AvatarURL, + Role: string(user.Role), + IsCertified: info.IsCertified, + CertificationTitle: info.CertificationTitle, + Stats: *stats, + CreatedAt: user.CreatedAt, + }, nil +} + +func (s *UserService) UpdateProfile(userID string, req dto.UserProfileUpdate) (*dto.UserProfile, error) { + user, err := s.userRepo.FindByID(userID) + if err != nil { + return nil, errors.New("用户不存在") + } + + if req.Nickname != nil { + user.Nickname = *req.Nickname + } + + if req.Email != nil && *req.Email != user.Email { + exists, _ := s.userRepo.ExistsByUsernameOrEmail("", *req.Email) + if exists { + return nil, errors.New("该邮箱已被使用") + } + user.Email = *req.Email + } + + if req.AvatarURL != nil { + user.AvatarURL = req.AvatarURL + } + + if req.Role != nil { + newRole := model.UserRole(*req.Role) + if !model.FamilyRoles[newRole] { + return nil, errors.New("角色只能是: mom, dad, family") + } + if model.ProfessionalRoles[user.Role] { + return nil, errors.New("认证专业人员不能修改角色") + } + if user.Role == model.RoleAdmin { + return nil, errors.New("管理员不能通过此接口修改角色") + } + user.Role = newRole + } + + if err := s.userRepo.Update(user); err != nil { + return nil, err + } + + return s.GetProfile(userID) +} + +func (s *UserService) GetUserQuestions(userID string, page, pageSize int) (*dto.PaginatedResponse, error) { + offset := (page - 1) * pageSize + questions, total, err := s.questionRepo.FindByAuthorID(userID, offset, pageSize) + if err != nil { + return nil, err + } + + questionIDs := make([]string, len(questions)) + for i, q := range questions { + questionIDs[i] = q.ID + } + + likedIDs, _ := s.interactionRepo.FindLikedTargetIDs(userID, "question", questionIDs) + collectedIDs, _ := s.interactionRepo.FindCollectedQuestionIDs(userID, questionIDs) + + items := make([]dto.MyQuestionListItem, 0, len(questions)) + for _, q := range questions { + tags := make([]dto.TagInfo, 0) + for _, t := range q.Tags { + tags = append(tags, buildTagInfo(t)) + } + + items = append(items, dto.MyQuestionListItem{ + ID: q.ID, + Title: q.Title, + ContentPreview: contentPreview(q.Content, 100), + Channel: string(q.Channel), + Tags: tags, + ViewCount: q.ViewCount, + AnswerCount: q.AnswerCount, + LikeCount: q.LikeCount, + CollectionCount: q.CollectionCount, + Status: string(q.Status), + HasAcceptedAnswer: q.AcceptedAnswerID != nil, + IsLiked: likedIDs[q.ID], + IsCollected: collectedIDs[q.ID], + CreatedAt: q.CreatedAt, + }) + } + + resp := dto.NewPaginatedResponse(items, total, page, pageSize) + return &resp, nil +} + +func (s *UserService) GetUserAnswers(userID string, page, pageSize int) (*dto.PaginatedResponse, error) { + offset := (page - 1) * pageSize + answers, total, err := s.answerRepo.FindByAuthorID(userID, offset, pageSize) + if err != nil { + return nil, err + } + + answerIDs := make([]string, len(answers)) + for i, a := range answers { + answerIDs[i] = a.ID + } + likedIDs, _ := s.interactionRepo.FindLikedTargetIDs(userID, "answer", answerIDs) + + items := make([]dto.MyAnswerListItem, 0, len(answers)) + for _, a := range answers { + items = append(items, dto.MyAnswerListItem{ + ID: a.ID, + ContentPreview: contentPreview(a.Content, 200), + Question: dto.QuestionBrief{ + ID: a.Question.ID, + Title: a.Question.Title, + Channel: string(a.Question.Channel), + }, + IsProfessional: a.IsProfessional, + IsAccepted: a.IsAccepted, + LikeCount: a.LikeCount, + CommentCount: a.CommentCount, + Status: string(a.Status), + IsLiked: likedIDs[a.ID], + CreatedAt: a.CreatedAt, + }) + } + + resp := dto.NewPaginatedResponse(items, total, page, pageSize) + return &resp, nil +} + +func (s *UserService) getUserStats(userID string) (*dto.UserStats, error) { + _, questionTotal, _ := s.questionRepo.FindByAuthorID(userID, 0, 1) + _, answerTotal, _ := s.answerRepo.FindByAuthorID(userID, 0, 1) + + // Sum likes received + var questionLikes, answerLikes int64 + _ = s.db.Model(&model.Question{}). + Where("author_id = ?", userID). + Select("COALESCE(SUM(like_count), 0)"). + Row().Scan(&questionLikes) + _ = s.db.Model(&model.Answer{}). + Where("author_id = ?", userID). + Select("COALESCE(SUM(like_count), 0)"). + Row().Scan(&answerLikes) + + var collectionCount int64 + s.db.Model(&model.Collection{}). + Where("user_id = ?", userID). + Count(&collectionCount) + + return &dto.UserStats{ + QuestionCount: int(questionTotal), + AnswerCount: int(answerTotal), + LikeReceivedCount: int(questionLikes + answerLikes), + CollectionCount: int(collectionCount), + }, nil +} diff --git a/backend/models/.gitkeep b/backend/models/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/pkg/jwt/jwt.go b/backend/pkg/jwt/jwt.go new file mode 100644 index 00000000..55c361b3 --- /dev/null +++ b/backend/pkg/jwt/jwt.go @@ -0,0 +1,82 @@ +package jwt + +import ( + "errors" + "time" + + jwtv5 "github.com/golang-jwt/jwt/v5" +) + +var ErrInvalidToken = errors.New("invalid or expired token") + +type Claims struct { + jwtv5.RegisteredClaims + Type string `json:"type"` // access, refresh, password_reset +} + +func CreateAccessToken(userID, secret string, expireMinutes int) (string, error) { + claims := Claims{ + RegisteredClaims: jwtv5.RegisteredClaims{ + Subject: userID, + ExpiresAt: jwtv5.NewNumericDate(time.Now().Add(time.Duration(expireMinutes) * time.Minute)), + IssuedAt: jwtv5.NewNumericDate(time.Now()), + }, + Type: "access", + } + token := jwtv5.NewWithClaims(jwtv5.SigningMethodHS256, claims) + return token.SignedString([]byte(secret)) +} + +func CreateRefreshToken(userID, secret string, expireDays int) (string, error) { + claims := Claims{ + RegisteredClaims: jwtv5.RegisteredClaims{ + Subject: userID, + ExpiresAt: jwtv5.NewNumericDate(time.Now().Add(time.Duration(expireDays) * 24 * time.Hour)), + IssuedAt: jwtv5.NewNumericDate(time.Now()), + }, + Type: "refresh", + } + token := jwtv5.NewWithClaims(jwtv5.SigningMethodHS256, claims) + return token.SignedString([]byte(secret)) +} + +func CreatePasswordResetToken(userID, secret string) (string, error) { + claims := Claims{ + RegisteredClaims: jwtv5.RegisteredClaims{ + Subject: userID, + ExpiresAt: jwtv5.NewNumericDate(time.Now().Add(1 * time.Hour)), + IssuedAt: jwtv5.NewNumericDate(time.Now()), + }, + Type: "password_reset", + } + token := jwtv5.NewWithClaims(jwtv5.SigningMethodHS256, claims) + return token.SignedString([]byte(secret)) +} + +func ParseToken(tokenStr, secret string) (*Claims, error) { + token, err := jwtv5.ParseWithClaims(tokenStr, &Claims{}, func(t *jwtv5.Token) (interface{}, error) { + if _, ok := t.Method.(*jwtv5.SigningMethodHMAC); !ok { + return nil, ErrInvalidToken + } + return []byte(secret), nil + }) + if err != nil { + return nil, ErrInvalidToken + } + claims, ok := token.Claims.(*Claims) + if !ok || !token.Valid { + return nil, ErrInvalidToken + } + return claims, nil +} + +func VerifyPasswordResetToken(tokenStr, secret string) (string, error) { + claims, err := ParseToken(tokenStr, secret) + if err != nil { + return "", err + } + if claims.Type != "password_reset" { + return "", ErrInvalidToken + } + return claims.Subject, nil +} diff --git a/backend/pkg/openai/client.go b/backend/pkg/openai/client.go new file mode 100644 index 00000000..a69be8b5 --- /dev/null +++ b/backend/pkg/openai/client.go @@ -0,0 +1,106 @@ +package openai + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" +) + +type Client struct { + apiKey string + baseURL string + model string + http *http.Client +} + +func NewClient(apiKey, baseURL, model string) *Client { + if baseURL == "" { + baseURL = "https://api.openai.com/v1" + } + return &Client{ + apiKey: apiKey, + baseURL: baseURL, + model: model, + http: &http.Client{}, + } +} + +type Message struct { + Role string `json:"role"` + Content string `json:"content"` +} + +type chatRequest struct { + Model string `json:"model"` + Messages []Message `json:"messages"` + Temperature float64 `json:"temperature"` + MaxTokens int `json:"max_tokens"` + EnableThinking bool `json:"enable_thinking"` +} + +type chatResponse struct { + Choices []struct { + Message struct { + Content string `json:"content"` + } `json:"message"` + } `json:"choices"` + Error *struct { + Message string `json:"message"` + } `json:"error,omitempty"` +} + +func (c *Client) Chat(ctx context.Context, messages []Message) (string, error) { + reqBody := chatRequest{ + Model: c.model, + Messages: messages, + Temperature: 0.7, + MaxTokens: 1024, + EnableThinking: false, + } + + body, err := json.Marshal(reqBody) + if err != nil { + return "", fmt.Errorf("failed to marshal request: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/chat/completions", bytes.NewReader(body)) + if err != nil { + return "", fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+c.apiKey) + + resp, err := c.http.Do(req) + if err != nil { + return "", fmt.Errorf("openai chat request failed: %w", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("openai chat completion failed: error, status code: %d, status: %s, message: %s", + resp.StatusCode, resp.Status, string(respBody)) + } + + var chatResp chatResponse + if err := json.Unmarshal(respBody, &chatResp); err != nil { + return "", fmt.Errorf("failed to parse response: %w", err) + } + + if chatResp.Error != nil { + return "", fmt.Errorf("openai error: %s", chatResp.Error.Message) + } + + if len(chatResp.Choices) == 0 { + return "", fmt.Errorf("openai returned no choices") + } + + return chatResp.Choices[0].Message.Content, nil +} diff --git a/backend/pkg/password/password.go b/backend/pkg/password/password.go new file mode 100644 index 00000000..cbe023aa --- /dev/null +++ b/backend/pkg/password/password.go @@ -0,0 +1,16 @@ +package password + +import "golang.org/x/crypto/bcrypt" + +func Hash(plain string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(bytes), nil +} + +func Verify(plain, hashed string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hashed), []byte(plain)) + return err == nil +} diff --git a/backend/pyproject.toml b/backend/pyproject.toml deleted file mode 100644 index ed9b4c7c..00000000 --- a/backend/pyproject.toml +++ /dev/null @@ -1,79 +0,0 @@ -[project] -name = "momshell-backend" -version = "0.5.3" -description = "An AI powered assistant for postpartum mothers." -requires-python = ">=3.11" -dependencies = [ - # Pose estimation - "mediapipe>=0.10.0", - "opencv-python>=4.8.0", - "numpy>=1.24.0", - # LangGraph workflow - "langgraph>=0.2.0", - "langchain-core>=0.3.0", - "langchain-anthropic>=0.3.0", - # TTS voice synthesis - "edge-tts>=6.1.0", - # Web framework - "fastapi>=0.115.0", - "uvicorn[standard]>=0.30.0", - "websockets>=12.0", - "python-multipart>=0.0.9", - "jinja2>=3.1.0", - # Database - "sqlalchemy>=2.0.0", - "aiosqlite>=0.20.0", - # Utilities - "pydantic>=2.0.0", - "pydantic-settings>=2.0.0", - "python-dotenv>=1.0.0", - "greenlet>=3.3.0", - "langchain-openai>=1.1.7", - # OpenAI SDK (for ModelScope API) - "openai>=1.0.0", - "httpx[socks]>=0.28.1", - # Authentication - "python-jose[cryptography]>=3.3.0", - "passlib>=1.7.4", - "bcrypt>=4.0.0,<5.1.0", - "email-validator>=2.0.0", -] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -packages = ["app"] - -[dependency-groups] -dev = [ - "ruff>=0.3.0", - "pre-commit>=3.6.0", - "mypy>=1.8.0", - "pytest>=9.0.2", -] - -[tool.ruff] -line-length = 88 -target-version = "py311" -exclude = [".git", ".venv", "__pycache__", "build", "dist"] - -[tool.ruff.lint] -select = ["E", "W", "F", "I", "B", "UP"] -ignore = ["E501"] - -[tool.ruff.lint.isort] -known-first-party = ["app"] - -[tool.ruff.format] -quote-style = "double" -indent-style = "space" -skip-magic-trailing-comma = false -line-ending = "auto" - -[tool.mypy] -python_version = "3.11" -warn_return_any = true -warn_unused_configs = true -ignore_missing_imports = true diff --git a/backend/requirements.txt b/backend/requirements.txt deleted file mode 100644 index a9374323..00000000 --- a/backend/requirements.txt +++ /dev/null @@ -1,2571 +0,0 @@ -# This file was autogenerated by uv via the following command: -# uv export --no-dev --no-emit-project --output-file requirements.txt -absl-py==2.3.1 \ - --hash=sha256:a97820526f7fbfd2ec1bce83f3f25e3a14840dac0d8e02a0b71cd75db3f77fc9 \ - --hash=sha256:eeecf07f0c2a93ace0772c92e596ace6d3d3996c042b2128459aaae2a76de11d - # via mediapipe -aiohappyeyeballs==2.6.1 \ - --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \ - --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 - # via aiohttp -aiohttp==3.13.3 \ - --hash=sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf \ - --hash=sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c \ - --hash=sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c \ - --hash=sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423 \ - --hash=sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f \ - --hash=sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2 \ - --hash=sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf \ - --hash=sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64 \ - --hash=sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998 \ - --hash=sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d \ - --hash=sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea \ - --hash=sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463 \ - --hash=sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4 \ - --hash=sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767 \ - --hash=sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43 \ - --hash=sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592 \ - --hash=sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a \ - --hash=sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687 \ - --hash=sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8 \ - --hash=sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261 \ - --hash=sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a \ - --hash=sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4 \ - --hash=sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587 \ - --hash=sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91 \ - --hash=sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3 \ - --hash=sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344 \ - --hash=sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6 \ - --hash=sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3 \ - --hash=sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29 \ - --hash=sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3 \ - --hash=sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b \ - --hash=sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51 \ - --hash=sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c \ - --hash=sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926 \ - --hash=sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64 \ - --hash=sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f \ - --hash=sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b \ - --hash=sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e \ - --hash=sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440 \ - --hash=sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6 \ - --hash=sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d \ - --hash=sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415 \ - --hash=sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce \ - --hash=sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603 \ - --hash=sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0 \ - --hash=sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf \ - --hash=sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591 \ - --hash=sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540 \ - --hash=sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26 \ - --hash=sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a \ - --hash=sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a \ - --hash=sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9 \ - --hash=sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba \ - --hash=sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df \ - --hash=sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679 \ - --hash=sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc \ - --hash=sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29 \ - --hash=sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984 \ - --hash=sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1 \ - --hash=sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632 \ - --hash=sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56 \ - --hash=sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239 \ - --hash=sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168 \ - --hash=sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88 \ - --hash=sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc \ - --hash=sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046 \ - --hash=sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0 \ - --hash=sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3 \ - --hash=sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1 \ - --hash=sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c \ - --hash=sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25 \ - --hash=sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033 \ - --hash=sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1 \ - --hash=sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d \ - --hash=sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f \ - --hash=sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f \ - --hash=sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29 \ - --hash=sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72 \ - --hash=sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57 \ - --hash=sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c \ - --hash=sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808 \ - --hash=sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7 \ - --hash=sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0 \ - --hash=sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3 \ - --hash=sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730 \ - --hash=sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa - # via edge-tts -aiosignal==1.4.0 \ - --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \ - --hash=sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7 - # via aiohttp -aiosqlite==0.22.1 \ - --hash=sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650 \ - --hash=sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb - # via momshell-backend -annotated-doc==0.0.4 \ - --hash=sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320 \ - --hash=sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4 - # via fastapi -annotated-types==0.7.0 \ - --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ - --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 - # via pydantic -anthropic==0.76.0 \ - --hash=sha256:81efa3113901192af2f0fe977d3ec73fdadb1e691586306c4256cd6d5ccc331c \ - --hash=sha256:e0cae6a368986d5cf6df743dfbb1b9519e6a9eee9c6c942ad8121c0b34416ffe - # via langchain-anthropic -anyio==4.12.1 \ - --hash=sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703 \ - --hash=sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c - # via - # anthropic - # httpx - # openai - # starlette - # watchfiles -attrs==25.4.0 \ - --hash=sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11 \ - --hash=sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373 - # via aiohttp -bcrypt==4.0.1 \ - --hash=sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535 \ - --hash=sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0 \ - --hash=sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410 \ - --hash=sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd \ - --hash=sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab \ - --hash=sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9 \ - --hash=sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a \ - --hash=sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344 \ - --hash=sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f \ - --hash=sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2 \ - --hash=sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e \ - --hash=sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3 - # via momshell-backend -certifi==2026.1.4 \ - --hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \ - --hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120 - # via - # edge-tts - # httpcore - # httpx - # requests -cffi==2.0.0 \ - --hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \ - --hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \ - --hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \ - --hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \ - --hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \ - --hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \ - --hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \ - --hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \ - --hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \ - --hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \ - --hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \ - --hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \ - --hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \ - --hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \ - --hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \ - --hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \ - --hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \ - --hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \ - --hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \ - --hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \ - --hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \ - --hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \ - --hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \ - --hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \ - --hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \ - --hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \ - --hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \ - --hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \ - --hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \ - --hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \ - --hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \ - --hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \ - --hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \ - --hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \ - --hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \ - --hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \ - --hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \ - --hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \ - --hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \ - --hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \ - --hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \ - --hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \ - --hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \ - --hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \ - --hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \ - --hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \ - --hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \ - --hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \ - --hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \ - --hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \ - --hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \ - --hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \ - --hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \ - --hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \ - --hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \ - --hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \ - --hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \ - --hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \ - --hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \ - --hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 - # via - # cryptography - # sounddevice -charset-normalizer==3.4.4 \ - --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ - --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ - --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ - --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ - --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ - --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ - --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ - --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ - --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ - --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ - --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ - --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ - --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ - --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ - --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ - --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ - --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ - --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ - --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ - --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ - --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ - --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ - --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ - --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ - --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ - --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ - --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ - --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ - --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ - --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ - --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ - --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ - --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ - --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ - --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ - --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ - --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ - --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ - --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ - --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ - --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ - --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ - --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ - --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ - --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ - --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ - --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ - --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ - --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ - --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ - --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ - --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ - --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ - --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ - --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ - --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ - --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ - --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ - --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ - --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ - --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ - --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ - --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ - --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ - --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ - --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 - # via requests -click==8.3.1 \ - --hash=sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a \ - --hash=sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6 - # via uvicorn -colorama==0.4.6 ; sys_platform == 'win32' \ - --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ - --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via - # click - # tqdm - # uvicorn -contourpy==1.3.3 \ - --hash=sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69 \ - --hash=sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc \ - --hash=sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880 \ - --hash=sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a \ - --hash=sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8 \ - --hash=sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc \ - --hash=sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470 \ - --hash=sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5 \ - --hash=sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263 \ - --hash=sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b \ - --hash=sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5 \ - --hash=sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381 \ - --hash=sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3 \ - --hash=sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4 \ - --hash=sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e \ - --hash=sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f \ - --hash=sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772 \ - --hash=sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286 \ - --hash=sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42 \ - --hash=sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301 \ - --hash=sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77 \ - --hash=sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7 \ - --hash=sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411 \ - --hash=sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1 \ - --hash=sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9 \ - --hash=sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a \ - --hash=sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b \ - --hash=sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db \ - --hash=sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6 \ - --hash=sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620 \ - --hash=sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989 \ - --hash=sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea \ - --hash=sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67 \ - --hash=sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5 \ - --hash=sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d \ - --hash=sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36 \ - --hash=sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99 \ - --hash=sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1 \ - --hash=sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e \ - --hash=sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b \ - --hash=sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8 \ - --hash=sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d \ - --hash=sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7 \ - --hash=sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7 \ - --hash=sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339 \ - --hash=sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1 \ - --hash=sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659 \ - --hash=sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4 \ - --hash=sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f \ - --hash=sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20 \ - --hash=sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36 \ - --hash=sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb \ - --hash=sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d \ - --hash=sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8 \ - --hash=sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0 \ - --hash=sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b \ - --hash=sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7 \ - --hash=sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe \ - --hash=sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77 \ - --hash=sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497 \ - --hash=sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd \ - --hash=sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1 \ - --hash=sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216 \ - --hash=sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13 \ - --hash=sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae \ - --hash=sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae \ - --hash=sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77 \ - --hash=sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3 \ - --hash=sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f \ - --hash=sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff \ - --hash=sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9 \ - --hash=sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a - # via matplotlib -cryptography==46.0.5 \ - --hash=sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72 \ - --hash=sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235 \ - --hash=sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9 \ - --hash=sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356 \ - --hash=sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257 \ - --hash=sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad \ - --hash=sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4 \ - --hash=sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c \ - --hash=sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614 \ - --hash=sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed \ - --hash=sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31 \ - --hash=sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229 \ - --hash=sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0 \ - --hash=sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731 \ - --hash=sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b \ - --hash=sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4 \ - --hash=sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4 \ - --hash=sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263 \ - --hash=sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595 \ - --hash=sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1 \ - --hash=sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678 \ - --hash=sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48 \ - --hash=sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76 \ - --hash=sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0 \ - --hash=sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18 \ - --hash=sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d \ - --hash=sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d \ - --hash=sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1 \ - --hash=sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981 \ - --hash=sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7 \ - --hash=sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82 \ - --hash=sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2 \ - --hash=sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4 \ - --hash=sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663 \ - --hash=sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c \ - --hash=sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d \ - --hash=sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a \ - --hash=sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a \ - --hash=sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d \ - --hash=sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b \ - --hash=sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a \ - --hash=sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826 \ - --hash=sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee \ - --hash=sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9 \ - --hash=sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648 \ - --hash=sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da \ - --hash=sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2 \ - --hash=sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2 \ - --hash=sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87 - # via python-jose -cycler==0.12.1 \ - --hash=sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 \ - --hash=sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c - # via matplotlib -distro==1.9.0 \ - --hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \ - --hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2 - # via - # anthropic - # openai -dnspython==2.8.0 \ - --hash=sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af \ - --hash=sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f - # via email-validator -docstring-parser==0.17.0 \ - --hash=sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912 \ - --hash=sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708 - # via anthropic -ecdsa==0.19.1 \ - --hash=sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3 \ - --hash=sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61 - # via python-jose -edge-tts==7.2.7 \ - --hash=sha256:0127fba57a742bc48ff0a2a3b24b8324f7859260185274c335b4e54735aff325 \ - --hash=sha256:ac11d9e834347e5ee62cbe72e8a56ffd65d3c4e795be14b1e593b72cf6480dd9 - # via momshell-backend -email-validator==2.3.0 \ - --hash=sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4 \ - --hash=sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426 - # via momshell-backend -fastapi==0.135.1 \ - --hash=sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e \ - --hash=sha256:d04115b508d936d254cea545b7312ecaa58a7b3a0f84952535b4c9afae7668cd - # via momshell-backend -flatbuffers==25.12.19 \ - --hash=sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4 - # via mediapipe -fonttools==4.61.1 \ - --hash=sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87 \ - --hash=sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796 \ - --hash=sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75 \ - --hash=sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d \ - --hash=sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371 \ - --hash=sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b \ - --hash=sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b \ - --hash=sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2 \ - --hash=sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3 \ - --hash=sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9 \ - --hash=sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd \ - --hash=sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c \ - --hash=sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c \ - --hash=sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56 \ - --hash=sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37 \ - --hash=sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0 \ - --hash=sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5 \ - --hash=sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118 \ - --hash=sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69 \ - --hash=sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9 \ - --hash=sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261 \ - --hash=sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb \ - --hash=sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c \ - --hash=sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba \ - --hash=sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c \ - --hash=sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91 \ - --hash=sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19 \ - --hash=sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5 \ - --hash=sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2 \ - --hash=sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d \ - --hash=sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063 \ - --hash=sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7 \ - --hash=sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09 \ - --hash=sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e \ - --hash=sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e \ - --hash=sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8 \ - --hash=sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa \ - --hash=sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e \ - --hash=sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a \ - --hash=sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c \ - --hash=sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7 \ - --hash=sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd - # via matplotlib -frozenlist==1.8.0 \ - --hash=sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686 \ - --hash=sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0 \ - --hash=sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121 \ - --hash=sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd \ - --hash=sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7 \ - --hash=sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c \ - --hash=sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84 \ - --hash=sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d \ - --hash=sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b \ - --hash=sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79 \ - --hash=sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967 \ - --hash=sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f \ - --hash=sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7 \ - --hash=sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef \ - --hash=sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9 \ - --hash=sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd \ - --hash=sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed \ - --hash=sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b \ - --hash=sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f \ - --hash=sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25 \ - --hash=sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe \ - --hash=sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143 \ - --hash=sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e \ - --hash=sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930 \ - --hash=sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37 \ - --hash=sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128 \ - --hash=sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2 \ - --hash=sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f \ - --hash=sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746 \ - --hash=sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df \ - --hash=sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8 \ - --hash=sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c \ - --hash=sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0 \ - --hash=sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad \ - --hash=sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82 \ - --hash=sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29 \ - --hash=sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30 \ - --hash=sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf \ - --hash=sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62 \ - --hash=sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383 \ - --hash=sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c \ - --hash=sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52 \ - --hash=sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d \ - --hash=sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1 \ - --hash=sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a \ - --hash=sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714 \ - --hash=sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65 \ - --hash=sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506 \ - --hash=sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888 \ - --hash=sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41 \ - --hash=sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608 \ - --hash=sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa \ - --hash=sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8 \ - --hash=sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1 \ - --hash=sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed \ - --hash=sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52 \ - --hash=sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231 \ - --hash=sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496 \ - --hash=sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a \ - --hash=sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3 \ - --hash=sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24 \ - --hash=sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695 \ - --hash=sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7 \ - --hash=sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4 \ - --hash=sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e \ - --hash=sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e \ - --hash=sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b \ - --hash=sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8 \ - --hash=sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51 \ - --hash=sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8 \ - --hash=sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b \ - --hash=sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806 \ - --hash=sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042 \ - --hash=sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b \ - --hash=sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d \ - --hash=sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567 \ - --hash=sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a \ - --hash=sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2 \ - --hash=sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0 \ - --hash=sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e \ - --hash=sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b \ - --hash=sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d \ - --hash=sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a \ - --hash=sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52 \ - --hash=sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1 \ - --hash=sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94 \ - --hash=sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822 \ - --hash=sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a \ - --hash=sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11 \ - --hash=sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581 \ - --hash=sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51 \ - --hash=sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40 \ - --hash=sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92 \ - --hash=sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5 \ - --hash=sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4 \ - --hash=sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93 \ - --hash=sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027 \ - --hash=sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd - # via - # aiohttp - # aiosignal -greenlet==3.3.2 \ - --hash=sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd \ - --hash=sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082 \ - --hash=sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b \ - --hash=sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5 \ - --hash=sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f \ - --hash=sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727 \ - --hash=sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e \ - --hash=sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2 \ - --hash=sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f \ - --hash=sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327 \ - --hash=sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd \ - --hash=sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2 \ - --hash=sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070 \ - --hash=sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99 \ - --hash=sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be \ - --hash=sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79 \ - --hash=sha256:508c7f01f1791fbc8e011bd508f6794cb95397fdb198a46cb6635eb5b78d85a7 \ - --hash=sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e \ - --hash=sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf \ - --hash=sha256:5d0e35379f93a6d0222de929a25ab47b5eb35b5ef4721c2b9cbcc4036129ff1f \ - --hash=sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506 \ - --hash=sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a \ - --hash=sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395 \ - --hash=sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4 \ - --hash=sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca \ - --hash=sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492 \ - --hash=sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab \ - --hash=sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358 \ - --hash=sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce \ - --hash=sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5 \ - --hash=sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef \ - --hash=sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d \ - --hash=sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac \ - --hash=sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55 \ - --hash=sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124 \ - --hash=sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4 \ - --hash=sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986 \ - --hash=sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd \ - --hash=sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f \ - --hash=sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb \ - --hash=sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4 \ - --hash=sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13 \ - --hash=sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab \ - --hash=sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff \ - --hash=sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a \ - --hash=sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9 \ - --hash=sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86 \ - --hash=sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd \ - --hash=sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71 \ - --hash=sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92 \ - --hash=sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643 \ - --hash=sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54 \ - --hash=sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9 - # via - # momshell-backend - # sqlalchemy -h11==0.16.0 \ - --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ - --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 - # via - # httpcore - # uvicorn -httpcore==1.0.9 \ - --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ - --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 - # via httpx -httptools==0.7.1 \ - --hash=sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c \ - --hash=sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1 \ - --hash=sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb \ - --hash=sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03 \ - --hash=sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6 \ - --hash=sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df \ - --hash=sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5 \ - --hash=sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321 \ - --hash=sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346 \ - --hash=sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650 \ - --hash=sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657 \ - --hash=sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca \ - --hash=sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66 \ - --hash=sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3 \ - --hash=sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca \ - --hash=sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3 \ - --hash=sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2 \ - --hash=sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70 \ - --hash=sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9 \ - --hash=sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270 \ - --hash=sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e \ - --hash=sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96 \ - --hash=sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b \ - --hash=sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c \ - --hash=sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274 \ - --hash=sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60 \ - --hash=sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5 \ - --hash=sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec \ - --hash=sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362 - # via uvicorn -httpx==0.28.1 \ - --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ - --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad - # via - # anthropic - # langgraph-sdk - # langsmith - # momshell-backend - # openai -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 - # via - # anyio - # email-validator - # httpx - # requests - # yarl -jinja2==3.1.6 \ - --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ - --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 - # via momshell-backend -jiter==0.13.0 \ - --hash=sha256:00203f47c214156df427b5989de74cb340c65c8180d09be1bf9de81d0abad599 \ - --hash=sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726 \ - --hash=sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654 \ - --hash=sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d \ - --hash=sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663 \ - --hash=sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8 \ - --hash=sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5 \ - --hash=sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394 \ - --hash=sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad \ - --hash=sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202 \ - --hash=sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1 \ - --hash=sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59 \ - --hash=sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d \ - --hash=sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92 \ - --hash=sha256:19928b5d1ce0ff8c1ee1b9bdef3b5bfc19e8304f1b904e436caf30bc15dc6cf5 \ - --hash=sha256:19cd6f85e1dc090277c3ce90a5b7d96f32127681d825e71c9dce28788e39fc0c \ - --hash=sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228 \ - --hash=sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf \ - --hash=sha256:2113c17c9a67071b0f820733c0893ed1d467b5fcf4414068169e5c2cabddb1e2 \ - --hash=sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018 \ - --hash=sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6 \ - --hash=sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d \ - --hash=sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024 \ - --hash=sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820 \ - --hash=sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e \ - --hash=sha256:309549b778b949d731a2f0e1594a3f805716be704a73bf3ad9a807eed5eb5721 \ - --hash=sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2 \ - --hash=sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72 \ - --hash=sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089 \ - --hash=sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a \ - --hash=sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9 \ - --hash=sha256:4397ee562b9f69d283e5674445551b47a5e8076fdde75e71bfac5891113dc543 \ - --hash=sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434 \ - --hash=sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4 \ - --hash=sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a \ - --hash=sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa \ - --hash=sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0 \ - --hash=sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d \ - --hash=sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0 \ - --hash=sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5 \ - --hash=sha256:6207fc61c395b26fffdcf637a0b06b4326f35bfa93c6e92fe1a166a21aeb6731 \ - --hash=sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6 \ - --hash=sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911 \ - --hash=sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607 \ - --hash=sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9 \ - --hash=sha256:6eeb7db8bc77dc20476bc2f7407a23dbe3d46d9cc664b166e3d474e1c1de4baa \ - --hash=sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d \ - --hash=sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d \ - --hash=sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95 \ - --hash=sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08 \ - --hash=sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19 \ - --hash=sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe \ - --hash=sha256:7c26ad6967c9dcedf10c995a21539c3aa57d4abad7001b7a84f621a263a6b605 \ - --hash=sha256:7f90023f8f672e13ea1819507d2d21b9d2d1c18920a3b3a5f1541955a85b5504 \ - --hash=sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09 \ - --hash=sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2 \ - --hash=sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc \ - --hash=sha256:9621ca242547edc16400981ca3231e0c91c0c4c1ab8573a596cd9bb3575d5c2b \ - --hash=sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0 \ - --hash=sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91 \ - --hash=sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663 \ - --hash=sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6 \ - --hash=sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f \ - --hash=sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411 \ - --hash=sha256:9ffda299e417dc83362963966c50cb76d42da673ee140de8a8ac762d4bb2378b \ - --hash=sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66 \ - --hash=sha256:a3a377af27b236abbf665a69b2bdd680e3b5a0bd2af825cd3b81245279a7606c \ - --hash=sha256:a576f5dce9ac7de5d350b8e2f552cf364f32975ed84717c35379a51c7cb198bd \ - --hash=sha256:a7637d92b1c9d7a771e8c56f445c7f84396d48f2e756e5978840ecba2fac0894 \ - --hash=sha256:ab1185ca5c8b9491b55ebf6c1e8866b8f68258612899693e24a92c5fdb9455d5 \ - --hash=sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59 \ - --hash=sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef \ - --hash=sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68 \ - --hash=sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c \ - --hash=sha256:b22945be8425d161f2e536cdae66da300b6b000f1c0ba3ddf237d1bfd45d21b8 \ - --hash=sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b \ - --hash=sha256:bcdabaea26cb04e25df3103ce47f97466627999260290349a88c8136ecae0060 \ - --hash=sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93 \ - --hash=sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df \ - --hash=sha256:c1b609e5cbd2f52bb74fb721515745b407df26d7b800458bd97cb3b972c29e7d \ - --hash=sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152 \ - --hash=sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701 \ - --hash=sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0 \ - --hash=sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3 \ - --hash=sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2 \ - --hash=sha256:dc3ce84cfd4fa9628fe62c4f85d0d597a4627d4242cfafac32a12cc1455d00f7 \ - --hash=sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40 \ - --hash=sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2 \ - --hash=sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939 \ - --hash=sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096 \ - --hash=sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c \ - --hash=sha256:ed0240dd1536a98c3ab55e929c60dfff7c899fecafcb7d01161b21a99fc8c363 \ - --hash=sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159 \ - --hash=sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165 \ - --hash=sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f \ - --hash=sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4 \ - --hash=sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a \ - --hash=sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb \ - --hash=sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505 \ - --hash=sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10 \ - --hash=sha256:fe49d3ff6db74321f144dff9addd4a5874d3105ac5ba7c5b77fac099cfae31ae \ - --hash=sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f - # via - # anthropic - # openai -jsonpatch==1.33 \ - --hash=sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade \ - --hash=sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c - # via langchain-core -jsonpointer==3.0.0 \ - --hash=sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942 \ - --hash=sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef - # via jsonpatch -kiwisolver==1.4.9 \ - --hash=sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c \ - --hash=sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7 \ - --hash=sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21 \ - --hash=sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff \ - --hash=sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7 \ - --hash=sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c \ - --hash=sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26 \ - --hash=sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa \ - --hash=sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f \ - --hash=sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1 \ - --hash=sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891 \ - --hash=sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77 \ - --hash=sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543 \ - --hash=sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d \ - --hash=sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce \ - --hash=sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3 \ - --hash=sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60 \ - --hash=sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a \ - --hash=sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089 \ - --hash=sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab \ - --hash=sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78 \ - --hash=sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771 \ - --hash=sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f \ - --hash=sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b \ - --hash=sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14 \ - --hash=sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32 \ - --hash=sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185 \ - --hash=sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed \ - --hash=sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1 \ - --hash=sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c \ - --hash=sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11 \ - --hash=sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752 \ - --hash=sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5 \ - --hash=sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4 \ - --hash=sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58 \ - --hash=sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5 \ - --hash=sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198 \ - --hash=sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134 \ - --hash=sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2 \ - --hash=sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2 \ - --hash=sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370 \ - --hash=sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1 \ - --hash=sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197 \ - --hash=sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386 \ - --hash=sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a \ - --hash=sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748 \ - --hash=sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c \ - --hash=sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8 \ - --hash=sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5 \ - --hash=sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999 \ - --hash=sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369 \ - --hash=sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122 \ - --hash=sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098 \ - --hash=sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f \ - --hash=sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799 \ - --hash=sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028 \ - --hash=sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2 \ - --hash=sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525 \ - --hash=sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d \ - --hash=sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872 \ - --hash=sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64 \ - --hash=sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf \ - --hash=sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552 \ - --hash=sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2 \ - --hash=sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c \ - --hash=sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6 \ - --hash=sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64 \ - --hash=sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d \ - --hash=sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548 \ - --hash=sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07 \ - --hash=sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61 \ - --hash=sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d \ - --hash=sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c \ - --hash=sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3 \ - --hash=sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16 \ - --hash=sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145 \ - --hash=sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2 \ - --hash=sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464 \ - --hash=sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2 \ - --hash=sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04 \ - --hash=sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54 \ - --hash=sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df \ - --hash=sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1 - # via matplotlib -langchain-anthropic==1.3.1 \ - --hash=sha256:1fc28cf8037c30597ee6172fc2ff9e345efe8149a8c2a39897b1eebba2948322 \ - --hash=sha256:4f3d7a4a7729ab1aeaf62d32c87d4d227c1b5421668ca9e3734562b383470b07 - # via momshell-backend -langchain-core==1.2.16 \ - --hash=sha256:055a4bfe7d62f4ac45ed49fd759ee2e6bdd15abf998fbeea695fda5da2de6413 \ - --hash=sha256:2768add9aa97232a7712580f678e0ba045ee1036c71fe471355be0434fcb6e30 - # via - # langchain-anthropic - # langchain-openai - # langgraph - # langgraph-checkpoint - # langgraph-prebuilt - # momshell-backend -langchain-openai==1.1.7 \ - --hash=sha256:34e9cd686aac1a120d6472804422792bf8080a2103b5d21ee450c9e42d053815 \ - --hash=sha256:f5ec31961ed24777548b63a5fe313548bc6e0eb9730d6552b8c6418765254c81 - # via momshell-backend -langgraph==1.0.10 \ - --hash=sha256:73bd10ee14a8020f31ef07e9cd4c1a70c35cc07b9c2b9cd637509a10d9d51e29 \ - --hash=sha256:7c298bef4f6ea292fcf9824d6088fe41a6727e2904ad6066f240c4095af12247 - # via momshell-backend -langgraph-checkpoint==4.0.1 \ - --hash=sha256:b433123735df11ade28829e40ce25b9be614930cd50245ff2af60629234befd9 \ - --hash=sha256:e3adcd7a0e0166f3b48b8cf508ce0ea366e7420b5a73aa81289888727769b034 - # via - # langgraph - # langgraph-prebuilt -langgraph-prebuilt==1.0.7 \ - --hash=sha256:38e097e06de810de4d0e028ffc0e432bb56d1fb417620fb1dfdc76c5e03e4bf9 \ - --hash=sha256:e14923516504405bb5edc3977085bc9622c35476b50c1808544490e13871fe7c - # via langgraph -langgraph-sdk==0.3.3 \ - --hash=sha256:a52ebaf09d91143e55378bb2d0b033ed98f57f48c9ad35c8f81493b88705fc7b \ - --hash=sha256:c34c3dce3b6848755eb61f0c94369d1ba04aceeb1b76015db1ea7362c544fb26 - # via langgraph -langsmith==0.6.4 \ - --hash=sha256:36f7223a01c218079fbb17da5e536ebbaf5c1468c028abe070aa3ae59bc99ec8 \ - --hash=sha256:ac4835860160be371042c7adbba3cb267bcf8d96a5ea976c33a8a4acad6c5486 - # via langchain-core -markupsafe==3.0.3 \ - --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ - --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ - --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ - --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ - --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ - --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ - --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ - --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ - --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ - --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ - --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ - --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ - --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ - --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ - --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ - --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ - --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ - --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ - --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ - --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ - --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ - --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ - --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ - --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ - --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ - --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ - --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ - --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ - --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ - --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ - --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ - --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ - --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ - --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ - --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ - --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ - --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ - --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ - --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ - --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ - --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ - --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ - --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ - --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ - --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ - --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ - --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ - --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ - --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ - --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ - --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ - --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ - --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ - --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ - --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ - --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ - --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ - --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ - --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ - --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ - --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ - --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ - --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ - --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ - --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ - --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ - --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 - # via jinja2 -matplotlib==3.10.8 \ - --hash=sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a \ - --hash=sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f \ - --hash=sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5 \ - --hash=sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9 \ - --hash=sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2 \ - --hash=sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3 \ - --hash=sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6 \ - --hash=sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f \ - --hash=sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b \ - --hash=sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8 \ - --hash=sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008 \ - --hash=sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b \ - --hash=sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958 \ - --hash=sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04 \ - --hash=sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b \ - --hash=sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6 \ - --hash=sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908 \ - --hash=sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c \ - --hash=sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1 \ - --hash=sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d \ - --hash=sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1 \ - --hash=sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c \ - --hash=sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a \ - --hash=sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce \ - --hash=sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a \ - --hash=sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160 \ - --hash=sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1 \ - --hash=sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11 \ - --hash=sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a \ - --hash=sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466 \ - --hash=sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486 \ - --hash=sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78 \ - --hash=sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077 \ - --hash=sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565 \ - --hash=sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f \ - --hash=sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50 \ - --hash=sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58 \ - --hash=sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2 \ - --hash=sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645 \ - --hash=sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2 \ - --hash=sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39 \ - --hash=sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf \ - --hash=sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149 \ - --hash=sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22 \ - --hash=sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4 \ - --hash=sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6 - # via mediapipe -mediapipe==0.10.32 \ - --hash=sha256:438d98977a4e1b0f88070ad16537a328912f847ce9debf236da047b0b483cbfd \ - --hash=sha256:4b0941fbbbce41862f13cb1850c4878c13dbc62cd5e81e74880051b7a20ce3b6 \ - --hash=sha256:b62178b7585e0bb8789075c43bbb3e352fbc4a8f765797fded509f86a098b29b - # via momshell-backend -multidict==6.7.1 \ - --hash=sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0 \ - --hash=sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9 \ - --hash=sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581 \ - --hash=sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2 \ - --hash=sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941 \ - --hash=sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3 \ - --hash=sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43 \ - --hash=sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962 \ - --hash=sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1 \ - --hash=sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f \ - --hash=sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c \ - --hash=sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8 \ - --hash=sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa \ - --hash=sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6 \ - --hash=sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c \ - --hash=sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991 \ - --hash=sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262 \ - --hash=sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd \ - --hash=sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d \ - --hash=sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d \ - --hash=sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5 \ - --hash=sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3 \ - --hash=sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601 \ - --hash=sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505 \ - --hash=sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0 \ - --hash=sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292 \ - --hash=sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed \ - --hash=sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362 \ - --hash=sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511 \ - --hash=sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23 \ - --hash=sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2 \ - --hash=sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb \ - --hash=sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e \ - --hash=sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582 \ - --hash=sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0 \ - --hash=sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2 \ - --hash=sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e \ - --hash=sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d \ - --hash=sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65 \ - --hash=sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a \ - --hash=sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd \ - --hash=sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d \ - --hash=sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108 \ - --hash=sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177 \ - --hash=sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144 \ - --hash=sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5 \ - --hash=sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd \ - --hash=sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5 \ - --hash=sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060 \ - --hash=sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37 \ - --hash=sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56 \ - --hash=sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df \ - --hash=sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963 \ - --hash=sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568 \ - --hash=sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db \ - --hash=sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118 \ - --hash=sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84 \ - --hash=sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f \ - --hash=sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889 \ - --hash=sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71 \ - --hash=sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f \ - --hash=sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0 \ - --hash=sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7 \ - --hash=sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048 \ - --hash=sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8 \ - --hash=sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49 \ - --hash=sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0 \ - --hash=sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9 \ - --hash=sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59 \ - --hash=sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190 \ - --hash=sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709 \ - --hash=sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d \ - --hash=sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c \ - --hash=sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e \ - --hash=sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2 \ - --hash=sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40 \ - --hash=sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3 \ - --hash=sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee \ - --hash=sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609 \ - --hash=sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c \ - --hash=sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445 \ - --hash=sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1 \ - --hash=sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a \ - --hash=sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5 \ - --hash=sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31 \ - --hash=sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8 \ - --hash=sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33 \ - --hash=sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7 \ - --hash=sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca \ - --hash=sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8 \ - --hash=sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92 \ - --hash=sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733 \ - --hash=sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429 \ - --hash=sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9 \ - --hash=sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4 \ - --hash=sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6 \ - --hash=sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2 \ - --hash=sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172 \ - --hash=sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981 \ - --hash=sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5 \ - --hash=sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de \ - --hash=sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52 \ - --hash=sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7 \ - --hash=sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c \ - --hash=sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2 \ - --hash=sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6 \ - --hash=sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf \ - --hash=sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f \ - --hash=sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b \ - --hash=sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961 \ - --hash=sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a \ - --hash=sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3 \ - --hash=sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b \ - --hash=sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358 \ - --hash=sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6 \ - --hash=sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e \ - --hash=sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1 \ - --hash=sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c \ - --hash=sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5 \ - --hash=sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53 \ - --hash=sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872 \ - --hash=sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e \ - --hash=sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df \ - --hash=sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03 \ - --hash=sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8 \ - --hash=sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a \ - --hash=sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122 \ - --hash=sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a \ - --hash=sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee \ - --hash=sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32 \ - --hash=sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3 \ - --hash=sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489 \ - --hash=sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23 \ - --hash=sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34 \ - --hash=sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75 \ - --hash=sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8 \ - --hash=sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a \ - --hash=sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d \ - --hash=sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855 \ - --hash=sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b \ - --hash=sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4 \ - --hash=sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4 \ - --hash=sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d \ - --hash=sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0 \ - --hash=sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba \ - --hash=sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19 - # via - # aiohttp - # yarl -numpy==2.4.1 \ - --hash=sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c \ - --hash=sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba \ - --hash=sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5 \ - --hash=sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8 \ - --hash=sha256:0f118ce6b972080ba0758c6087c3617b5ba243d806268623dc34216d69099ba0 \ - --hash=sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d \ - --hash=sha256:18e14c4d09d55eef39a6ab5b08406e84bc6869c1e34eef45564804f90b7e0574 \ - --hash=sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696 \ - --hash=sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5 \ - --hash=sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505 \ - --hash=sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0 \ - --hash=sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162 \ - --hash=sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844 \ - --hash=sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205 \ - --hash=sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4 \ - --hash=sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc \ - --hash=sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d \ - --hash=sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93 \ - --hash=sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01 \ - --hash=sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c \ - --hash=sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f \ - --hash=sha256:4f1b68ff47680c2925f8063402a693ede215f0257f02596b1318ecdfb1d79e33 \ - --hash=sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82 \ - --hash=sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2 \ - --hash=sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42 \ - --hash=sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509 \ - --hash=sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a \ - --hash=sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e \ - --hash=sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556 \ - --hash=sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a \ - --hash=sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510 \ - --hash=sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295 \ - --hash=sha256:6461de5113088b399d655d45c3897fa188766415d0f568f175ab071c8873bd73 \ - --hash=sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3 \ - --hash=sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9 \ - --hash=sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8 \ - --hash=sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745 \ - --hash=sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2 \ - --hash=sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02 \ - --hash=sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d \ - --hash=sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344 \ - --hash=sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f \ - --hash=sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be \ - --hash=sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425 \ - --hash=sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1 \ - --hash=sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2 \ - --hash=sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2 \ - --hash=sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb \ - --hash=sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9 \ - --hash=sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15 \ - --hash=sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690 \ - --hash=sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0 \ - --hash=sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261 \ - --hash=sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a \ - --hash=sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc \ - --hash=sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f \ - --hash=sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5 \ - --hash=sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df \ - --hash=sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9 \ - --hash=sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2 \ - --hash=sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8 \ - --hash=sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426 \ - --hash=sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b \ - --hash=sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87 \ - --hash=sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220 \ - --hash=sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b \ - --hash=sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3 \ - --hash=sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e \ - --hash=sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501 \ - --hash=sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee \ - --hash=sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7 \ - --hash=sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c - # via - # contourpy - # matplotlib - # mediapipe - # momshell-backend - # opencv-contrib-python - # opencv-python -openai==2.15.0 \ - --hash=sha256:42eb8cbb407d84770633f31bf727d4ffb4138711c670565a41663d9439174fba \ - --hash=sha256:6ae23b932cd7230f7244e52954daa6602716d6b9bf235401a107af731baea6c3 - # via - # langchain-openai - # momshell-backend -opencv-contrib-python==4.13.0.90 \ - --hash=sha256:177d75b048021df8a2632cb5998b3827a30143f88540be2791cac5497e8592dd \ - --hash=sha256:2f7ac460620f5a03924be96d9770c076e2807366a03941bdf38b29431305bfd4 \ - --hash=sha256:334c10e5b191cac919206f797f9b84a2809ab39db20458b993edf8d204379341 \ - --hash=sha256:3b95f67bd2539309459057fe6d239603754994155957cee0ce3b82e0f0d8d8a5 \ - --hash=sha256:3c693f1fb7a25eae73eb9bc1c2fdbc08ad3df51c31589f1f9a8377f2a4368b1c \ - --hash=sha256:8ab59de69d1eee39787aa496cfdb2e6e6685733c54c97d416d58e70261836f58 \ - --hash=sha256:8e7c74869e329636727298f3cab858f015160b2980f4726b538744732d4ab526 \ - --hash=sha256:c9268888a1592875cb1119f2dc444ff8581a92c27dec65cbe694551b2952d834 \ - --hash=sha256:e000908d262f479042677ef91475cecb1801341ea985af5cb3b9dcde4bb78029 - # via mediapipe -opencv-python==4.13.0.90 \ - --hash=sha256:1150b8f1947761b848bbfa9c96ceba8877743ffef157c08a04af6f7717ddd709 \ - --hash=sha256:458a00f2ba47a877eca385be3e7bcc45e6d30a4361d107ce73c1800f516dab09 \ - --hash=sha256:526bde4c33a86808a751e2bb57bf4921beb49794621810971926c472897f6433 \ - --hash=sha256:58803f8b05b51d8a785e2306d83b44173b32536f980342f3bc76d8c122b5938d \ - --hash=sha256:9911581e37b24169e4842069ff01d6645ea2bc4af7e10a022d9ebe340fd035ec \ - --hash=sha256:a5354e8b161409fce7710ba4c1cfe88b7bb460d97f705dc4e714a1636616f87d \ - --hash=sha256:d557cbf0c7818081c9acf56585b68e781af4f00638971f75eaa3de70904a6314 \ - --hash=sha256:d6716f16149b04eea52f953b8ca983d60dd9cd4872c1fd5113f6e2fcebb90e93 - # via momshell-backend -orjson==3.11.5 \ - --hash=sha256:1b6bd351202b2cd987f35a13b5e16471cf4d952b42a73c391cc537974c43ef6d \ - --hash=sha256:1cbf2735722623fcdee8e712cbaaab9e372bbcb0c7924ad711b261c2eccf4a5c \ - --hash=sha256:1db2088b490761976c1b2e956d5d4e6409f3732e9d79cfa69f876c5248d1baf9 \ - --hash=sha256:23d04c4543e78f724c4dfe656b3791b5f98e4c9253e13b2636f1af5d90e4a880 \ - --hash=sha256:298d2451f375e5f17b897794bcc3e7b821c0f32b4788b9bcae47ada24d7f3cf7 \ - --hash=sha256:2b91126e7b470ff2e75746f6f6ee32b9ab67b7a93c8ba1d15d3a0caaf16ec875 \ - --hash=sha256:2cc79aaad1dfabe1bd2d50ee09814a1253164b3da4c00a78c458d82d04b3bdef \ - --hash=sha256:334e5b4bff9ad101237c2d799d9fd45737752929753bf4faf4b207335a416b7d \ - --hash=sha256:38b22f476c351f9a1c43e5b07d8b5a02eb24a6ab8e75f700f7d479d4568346a5 \ - --hash=sha256:3b01799262081a4c47c035dd77c1301d40f568f77cc7ec1bb7db5d63b0a01629 \ - --hash=sha256:3fd15f9fc8c203aeceff4fda211157fad114dde66e92e24097b3647a08f4ee9e \ - --hash=sha256:4bdd8d164a871c4ec773f9de0f6fe8769c2d6727879c37a9666ba4183b7f8228 \ - --hash=sha256:53deb5addae9c22bbe3739298f5f2196afa881ea75944e7720681c7080909a81 \ - --hash=sha256:54aae9b654554c3b4edd61896b978568c6daa16af96fa4681c9b5babd469f863 \ - --hash=sha256:59ac72ea775c88b163ba8d21b0177628bd015c5dd060647bbab6e22da3aad287 \ - --hash=sha256:61de247948108484779f57a9f406e4c84d636fa5a59e411e6352484985e8a7c3 \ - --hash=sha256:67394d3becd50b954c4ecd24ac90b5051ee7c903d167459f93e77fc6f5b4c968 \ - --hash=sha256:75bc2e59e6a2ac1dd28901d07115abdebc4563b5b07dd612bf64260a201b1c7f \ - --hash=sha256:7bb2ce0b82bc9fd1168a513ddae7a857994b780b2945a8c51db4ab1c4b751ebc \ - --hash=sha256:7cce16ae2f5fb2c53c3eafdd1706cb7b6530a67cc1c17abe8ec747f5cd7c0c51 \ - --hash=sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5 \ - --hash=sha256:82cd00d49d6063d2b8791da5d4f9d20539c5951f965e45ccf4e96d33505ce68f \ - --hash=sha256:86cfc555bfd5794d24c6a1903e558b50644e5e68e6471d66502ce5cb5fdef3f9 \ - --hash=sha256:894aea2e63d4f24a7f04a1908307c738d0dce992e9249e744b8f4e8dd9197f39 \ - --hash=sha256:8d5f16195bb671a5dd3d1dbea758918bada8f6cc27de72bd64adfbd748770814 \ - --hash=sha256:9172578c4eb09dbfcf1657d43198de59b6cef4054de385365060ed50c458ac98 \ - --hash=sha256:92a8d676748fca47ade5bc3da7430ed7767afe51b2f8100e3cd65e151c0eaceb \ - --hash=sha256:9645ef655735a74da4990c24ffbd6894828fbfa117bc97c1edd98c282ecb52e1 \ - --hash=sha256:9c8494625ad60a923af6b2b0bd74107146efe9b55099e20d7740d995f338fcd8 \ - --hash=sha256:9cc1e55c884921434a84a0c3dd2699eb9f92e7b441d7f53f3941079ec6ce7499 \ - --hash=sha256:9df95000fbe6777bf9820ae82ab7578e8662051bb5f83d71a28992f539d2cda7 \ - --hash=sha256:a230065027bc2a025e944f9d4714976a81e7ecfa940923283bca7bbc1f10f626 \ - --hash=sha256:a261fef929bcf98a60713bf5e95ad067cea16ae345d9a35034e73c3990e927d2 \ - --hash=sha256:a4f3cb2d874e03bc7767c8f88adaa1a9a05cecea3712649c3b58589ec7317310 \ - --hash=sha256:a66d7769e98a08a12a139049aac2f0ca3adae989817f8c43337455fbc7669b85 \ - --hash=sha256:aa0f513be38b40234c77975e68805506cad5d57b3dfd8fe3baa7f4f4051e15b4 \ - --hash=sha256:aa5e4244063db8e1d87e0f54c3f7522f14b2dc937e65d5241ef0076a096409fd \ - --hash=sha256:acbc5fac7e06777555b0722b8ad5f574739e99ffe99467ed63da98f97f9ca0fe \ - --hash=sha256:b29d36b60e606df01959c4b982729c8845c69d1963f88686608be9ced96dbfaa \ - --hash=sha256:b42ffbed9128e547a1647a3e50bc88ab28ae9daa61713962e0d3dd35e820c125 \ - --hash=sha256:b923c1c13fa02084eb38c9c065afd860a5cff58026813319a06949c3af5732ac \ - --hash=sha256:bb150d529637d541e6af06bbe3d02f5498d628b7f98267ff87647584293ab439 \ - --hash=sha256:c028a394c766693c5c9909dec76b24f37e6a1b91999e8d0c0d5feecbe93c3e05 \ - --hash=sha256:c0e5d9f7a0227df2927d343a6e3859bebf9208b427c79bd31949abcc2fa32fa5 \ - --hash=sha256:c2021afda46c1ed64d74b555065dbd4c2558d510d8cec5ea6a53001b3e5e82a9 \ - --hash=sha256:c2ed66358f32c24e10ceea518e16eb3549e34f33a9d51f99ce23b0251776a1ef \ - --hash=sha256:c404603df4865f8e0afe981aa3c4b62b406e6d06049564d58934860b62b7f91d \ - --hash=sha256:c74099c6b230d4261fdc3169d50efc09abf38ace1a42ea2f9994b1d79153d477 \ - --hash=sha256:d4be86b58e9ea262617b8ca6251a2f0d63cc132a6da4b5fcc8e0a4128782c829 \ - --hash=sha256:d7345c759276b798ccd6d77a87136029e71e66a8bbf2d2755cbdde1d82e78706 \ - --hash=sha256:ddbfdb5099b3e6ba6d6ea818f61997bb66de14b411357d24c4612cf1ebad08ca \ - --hash=sha256:ddc21521598dbe369d83d4d40338e23d4101dad21dae0e79fa20465dbace019f \ - --hash=sha256:e08ca8a6c851e95aaecc32bc44a5aa75d0ad26af8cdac7c77e4ed93acf3d5b69 \ - --hash=sha256:e446a8ea0a4c366ceafc7d97067bfd55292969143b57e3c846d87fc701e797a0 \ - --hash=sha256:e46c762d9f0e1cfb4ccc8515de7f349abbc95b59cb5a2bd68df5973fdef913f8 \ - --hash=sha256:e697d06ad57dd0c7a737771d470eedc18e68dfdefcdd3b7de7f33dfda5b6212e \ - --hash=sha256:e8b5f96c05fce7d0218df3fdfeb962d6b8cfff7e3e20264306b46dd8b217c0f3 \ - --hash=sha256:ed24250e55efbcb0b35bed7caaec8cedf858ab2f9f2201f17b8938c618c8ca6f \ - --hash=sha256:fa1863e75b92891f553b7922ce4ee10ed06db061e104f2b7815de80cdcb135ad \ - --hash=sha256:ff770589960a86eae279f5d8aa536196ebda8273a2a07db2a54e82b93bc86626 \ - --hash=sha256:ff7877d376add4e16b274e35a3f58b7f37b362abf4aa31863dadacdd20e3a583 - # via - # langgraph-sdk - # langsmith -ormsgpack==1.12.2 \ - --hash=sha256:0b39e629fd2e1c5b2f46f99778450b59454d1f901bc507963168985e79f09c5d \ - --hash=sha256:118576ea6006893aea811b17429bfc561b4778fad393f5f538c84af70b01260c \ - --hash=sha256:18867233df592c997154ff942a6503df274b5ac1765215bceba7a231bea2745d \ - --hash=sha256:1dcc17d92b6390d4f18f937cf0b99054824a7815818012ddca925d6e01c2e49e \ - --hash=sha256:21f4276caca5c03a818041d637e4019bc84f9d6ca8baa5ea03e5cc8bf56140e9 \ - --hash=sha256:292410a7d23de9b40444636b9b8f1e4e4b814af7f1ef476e44887e52a123f09d \ - --hash=sha256:29a9f17a3dac6054c0dce7925e0f4995c727f7c41859adf9b5572180f640d172 \ - --hash=sha256:34d5b28b3570e9fed9a5a76528fc7230c3c76333bc214798958e58e9b79cc18a \ - --hash=sha256:3708693412c28f3538fb5a65da93787b6bbab3484f6bc6e935bfb77a62400ae5 \ - --hash=sha256:39c1bd2092880e413902910388be8715f70b9f15f20779d44e673033a6146f2d \ - --hash=sha256:43013a3f3e2e902e1d05e72c0f1aeb5bedbb8e09240b51e26792a3c89267e181 \ - --hash=sha256:46d084427b4132553940070ad95107266656cb646ea9da4975f85cb1a6676553 \ - --hash=sha256:4ee766d2e78251b7a63daf1cddfac36a73562d3ddef68cacfb41b2af64698033 \ - --hash=sha256:50b7249244382209877deedeee838aef1542f3d0fc28b8fe71ca9d7e1896a0d7 \ - --hash=sha256:58d379d72b6c5e964851c77cfedfb386e474adee4fd39791c2c5d9efb53505cc \ - --hash=sha256:5af04800d844451cf102a59c74a841324868d3f1625c296a06cc655c542a6685 \ - --hash=sha256:5ea60cb5f210b1cfbad8c002948d73447508e629ec375acb82910e3efa8ff355 \ - --hash=sha256:7121b3d355d3858781dc40dafe25a32ff8a8242b9d80c692fd548a4b1f7fd3c8 \ - --hash=sha256:73670ed0375ecc303858e3613f407628dd1fca18fe6ac57b7b7ce66cc7bb006c \ - --hash=sha256:7a29d09b64b9694b588ff2f80e9826bdceb3a2b91523c5beae1fab27d5c940e7 \ - --hash=sha256:7c8b1667a72cbba74f0ae7ecf3105a5e01304620ed14528b2cb4320679d2869b \ - --hash=sha256:837dd316584485b72ef451d08dd3e96c4a11d12e4963aedb40e08f89685d8ec2 \ - --hash=sha256:8463a3fc5f09832e67bdb0e2fda6d518dc4281b133166146a67f54c08496442e \ - --hash=sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33 \ - --hash=sha256:958dcb270d30a7cb633a45ee62b9444433fa571a752d2ca484efdac07480876e \ - --hash=sha256:ae3aba7eed4ca7cb79fd3436eddd29140f17ea254b91604aa1eb19bfcedb990f \ - --hash=sha256:b009049086ddc6b8f80c76b3955df1aa22a5fbd7673c525cd63bf91f23122ede \ - --hash=sha256:baca4b6773d20a82e36d6fd25f341064244f9f86a13dead95dd7d7f996f51709 \ - --hash=sha256:bc68dd5915f4acf66ff2010ee47c8906dc1cf07399b16f4089f8c71733f6e36c \ - --hash=sha256:bd5f4bf04c37888e864f08e740c5a573c4017f6fd6e99fa944c5c935fabf2dd9 \ - --hash=sha256:c010da16235806cf1d7bc4c96bf286bfa91c686853395a299b3ddb49499a3e13 \ - --hash=sha256:c2be829954434e33601ae5da328cccce3266b098927ca7a30246a0baec2ce7bd \ - --hash=sha256:c6a4c34ddef109647c769d69be65fa1de7a6022b02ad45546a69b3216573eb4a \ - --hash=sha256:cec70477d4371cd524534cd16472d8b9cc187e0e3043a8790545a9a9b296c258 \ - --hash=sha256:d024b40828f1dde5654faebd0d824f9cc29ad46891f626272dd5bfd7af2333a4 \ - --hash=sha256:da538c542bac7d1c8f3f2a937863dba36f013108ce63e55745941dda4b75dbb6 \ - --hash=sha256:df6961442140193e517303d0b5d7bc2e20e69a879c2d774316125350c4a76b92 \ - --hash=sha256:eddffb77eff0bad4e67547d67a130604e7e2dfbb7b0cde0796045be4090f35c6 \ - --hash=sha256:f04b5e896d510b07c0ad733d7fce2d44b260c5e6c402d272128f8941984e4285 \ - --hash=sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1 \ - --hash=sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd - # via langgraph-checkpoint -packaging==25.0 \ - --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ - --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f - # via - # langchain-core - # langsmith - # matplotlib -passlib==1.7.4 \ - --hash=sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1 \ - --hash=sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04 - # via momshell-backend -pillow==12.1.1 \ - --hash=sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9 \ - --hash=sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da \ - --hash=sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f \ - --hash=sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642 \ - --hash=sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713 \ - --hash=sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850 \ - --hash=sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9 \ - --hash=sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0 \ - --hash=sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9 \ - --hash=sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8 \ - --hash=sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6 \ - --hash=sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd \ - --hash=sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5 \ - --hash=sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c \ - --hash=sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35 \ - --hash=sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1 \ - --hash=sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff \ - --hash=sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38 \ - --hash=sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4 \ - --hash=sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af \ - --hash=sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60 \ - --hash=sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986 \ - --hash=sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13 \ - --hash=sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717 \ - --hash=sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e \ - --hash=sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b \ - --hash=sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15 \ - --hash=sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a \ - --hash=sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb \ - --hash=sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d \ - --hash=sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b \ - --hash=sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e \ - --hash=sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a \ - --hash=sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f \ - --hash=sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a \ - --hash=sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce \ - --hash=sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc \ - --hash=sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f \ - --hash=sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586 \ - --hash=sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f \ - --hash=sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9 \ - --hash=sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8 \ - --hash=sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40 \ - --hash=sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60 \ - --hash=sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c \ - --hash=sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0 \ - --hash=sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334 \ - --hash=sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af \ - --hash=sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735 \ - --hash=sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524 \ - --hash=sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf \ - --hash=sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b \ - --hash=sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2 \ - --hash=sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9 \ - --hash=sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7 \ - --hash=sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e \ - --hash=sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4 \ - --hash=sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4 \ - --hash=sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b \ - --hash=sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397 \ - --hash=sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c \ - --hash=sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e \ - --hash=sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029 \ - --hash=sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3 \ - --hash=sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052 \ - --hash=sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984 \ - --hash=sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293 \ - --hash=sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523 \ - --hash=sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f \ - --hash=sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b \ - --hash=sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80 \ - --hash=sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f \ - --hash=sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79 \ - --hash=sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23 \ - --hash=sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8 \ - --hash=sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e \ - --hash=sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3 \ - --hash=sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e \ - --hash=sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36 \ - --hash=sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f \ - --hash=sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5 \ - --hash=sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f \ - --hash=sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6 \ - --hash=sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32 \ - --hash=sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20 \ - --hash=sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202 \ - --hash=sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0 \ - --hash=sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3 \ - --hash=sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563 \ - --hash=sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090 \ - --hash=sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289 - # via matplotlib -propcache==0.4.1 \ - --hash=sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4 \ - --hash=sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be \ - --hash=sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3 \ - --hash=sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85 \ - --hash=sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b \ - --hash=sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367 \ - --hash=sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf \ - --hash=sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393 \ - --hash=sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1 \ - --hash=sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717 \ - --hash=sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc \ - --hash=sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe \ - --hash=sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75 \ - --hash=sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6 \ - --hash=sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e \ - --hash=sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566 \ - --hash=sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12 \ - --hash=sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367 \ - --hash=sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874 \ - --hash=sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf \ - --hash=sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566 \ - --hash=sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a \ - --hash=sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a \ - --hash=sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1 \ - --hash=sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6 \ - --hash=sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61 \ - --hash=sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726 \ - --hash=sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49 \ - --hash=sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44 \ - --hash=sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af \ - --hash=sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa \ - --hash=sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153 \ - --hash=sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc \ - --hash=sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf \ - --hash=sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8 \ - --hash=sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c \ - --hash=sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85 \ - --hash=sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e \ - --hash=sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0 \ - --hash=sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1 \ - --hash=sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992 \ - --hash=sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f \ - --hash=sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d \ - --hash=sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1 \ - --hash=sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e \ - --hash=sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89 \ - --hash=sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a \ - --hash=sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b \ - --hash=sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f \ - --hash=sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1 \ - --hash=sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66 \ - --hash=sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded \ - --hash=sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0 \ - --hash=sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165 \ - --hash=sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778 \ - --hash=sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455 \ - --hash=sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f \ - --hash=sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b \ - --hash=sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237 \ - --hash=sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81 \ - --hash=sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859 \ - --hash=sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c \ - --hash=sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835 \ - --hash=sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393 \ - --hash=sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5 \ - --hash=sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641 \ - --hash=sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144 \ - --hash=sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74 \ - --hash=sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db \ - --hash=sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403 \ - --hash=sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9 \ - --hash=sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f \ - --hash=sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311 \ - --hash=sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36 \ - --hash=sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f \ - --hash=sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2 \ - --hash=sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7 \ - --hash=sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239 \ - --hash=sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757 \ - --hash=sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72 \ - --hash=sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9 \ - --hash=sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4 \ - --hash=sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24 \ - --hash=sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207 \ - --hash=sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e \ - --hash=sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1 \ - --hash=sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d \ - --hash=sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37 \ - --hash=sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e \ - --hash=sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570 \ - --hash=sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af \ - --hash=sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48 - # via - # aiohttp - # yarl -pyasn1==0.6.2 \ - --hash=sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf \ - --hash=sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b - # via - # python-jose - # rsa -pycparser==3.0 ; implementation_name != 'PyPy' \ - --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ - --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 - # via cffi -pydantic==2.12.5 \ - --hash=sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49 \ - --hash=sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d - # via - # anthropic - # fastapi - # langchain-anthropic - # langchain-core - # langgraph - # langsmith - # momshell-backend - # openai - # pydantic-settings -pydantic-core==2.41.5 \ - --hash=sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90 \ - --hash=sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740 \ - --hash=sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84 \ - --hash=sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33 \ - --hash=sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0 \ - --hash=sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e \ - --hash=sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0 \ - --hash=sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34 \ - --hash=sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3 \ - --hash=sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815 \ - --hash=sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14 \ - --hash=sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375 \ - --hash=sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf \ - --hash=sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1 \ - --hash=sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808 \ - --hash=sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553 \ - --hash=sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1 \ - --hash=sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470 \ - --hash=sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2 \ - --hash=sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b \ - --hash=sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660 \ - --hash=sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c \ - --hash=sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594 \ - --hash=sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008 \ - --hash=sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a \ - --hash=sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a \ - --hash=sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd \ - --hash=sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284 \ - --hash=sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586 \ - --hash=sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869 \ - --hash=sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294 \ - --hash=sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f \ - --hash=sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66 \ - --hash=sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51 \ - --hash=sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc \ - --hash=sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d \ - --hash=sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c \ - --hash=sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07 \ - --hash=sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36 \ - --hash=sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e \ - --hash=sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05 \ - --hash=sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e \ - --hash=sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612 \ - --hash=sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b \ - --hash=sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe \ - --hash=sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11 \ - --hash=sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd \ - --hash=sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b \ - --hash=sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c \ - --hash=sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a \ - --hash=sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1 \ - --hash=sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf \ - --hash=sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858 \ - --hash=sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2 \ - --hash=sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9 \ - --hash=sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2 \ - --hash=sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3 \ - --hash=sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6 \ - --hash=sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770 \ - --hash=sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc \ - --hash=sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23 \ - --hash=sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26 \ - --hash=sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa \ - --hash=sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d \ - --hash=sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3 \ - --hash=sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d \ - --hash=sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034 \ - --hash=sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9 \ - --hash=sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1 \ - --hash=sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56 \ - --hash=sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b \ - --hash=sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c \ - --hash=sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e \ - --hash=sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9 \ - --hash=sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5 \ - --hash=sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e \ - --hash=sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc \ - --hash=sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb \ - --hash=sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0 \ - --hash=sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8 \ - --hash=sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69 \ - --hash=sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c \ - --hash=sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75 \ - --hash=sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f \ - --hash=sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad \ - --hash=sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b \ - --hash=sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7 - # via pydantic -pydantic-settings==2.12.0 \ - --hash=sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0 \ - --hash=sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809 - # via momshell-backend -pyparsing==3.3.2 \ - --hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \ - --hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc - # via matplotlib -python-dateutil==2.9.0.post0 \ - --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ - --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 - # via matplotlib -python-dotenv==1.2.1 \ - --hash=sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6 \ - --hash=sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61 - # via - # momshell-backend - # pydantic-settings - # uvicorn -python-jose==3.5.0 \ - --hash=sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771 \ - --hash=sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b - # via momshell-backend -python-multipart==0.0.22 \ - --hash=sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155 \ - --hash=sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58 - # via momshell-backend -pyyaml==6.0.3 \ - --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ - --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ - --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ - --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ - --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ - --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ - --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ - --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ - --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ - --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ - --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ - --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ - --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ - --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ - --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ - --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ - --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ - --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ - --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ - --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ - --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ - --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ - --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ - --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ - --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ - --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ - --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ - --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ - --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ - --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ - --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ - --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ - --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ - --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ - --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ - --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ - --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ - --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ - --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ - --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ - --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ - --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ - --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ - --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ - --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ - --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ - --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ - --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 - # via - # langchain-core - # uvicorn -regex==2026.1.15 \ - --hash=sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93 \ - --hash=sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde \ - --hash=sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3 \ - --hash=sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c \ - --hash=sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5 \ - --hash=sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785 \ - --hash=sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5 \ - --hash=sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794 \ - --hash=sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5 \ - --hash=sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10 \ - --hash=sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6 \ - --hash=sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09 \ - --hash=sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a \ - --hash=sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a \ - --hash=sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e \ - --hash=sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1 \ - --hash=sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8 \ - --hash=sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2 \ - --hash=sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e \ - --hash=sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410 \ - --hash=sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf \ - --hash=sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413 \ - --hash=sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10 \ - --hash=sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10 \ - --hash=sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846 \ - --hash=sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8 \ - --hash=sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903 \ - --hash=sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec \ - --hash=sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e \ - --hash=sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2 \ - --hash=sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc \ - --hash=sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1 \ - --hash=sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a \ - --hash=sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b \ - --hash=sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae \ - --hash=sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4 \ - --hash=sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf \ - --hash=sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db \ - --hash=sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2 \ - --hash=sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788 \ - --hash=sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3 \ - --hash=sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd \ - --hash=sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34 \ - --hash=sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d \ - --hash=sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804 \ - --hash=sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913 \ - --hash=sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434 \ - --hash=sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb \ - --hash=sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b \ - --hash=sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337 \ - --hash=sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705 \ - --hash=sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f \ - --hash=sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1 \ - --hash=sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599 \ - --hash=sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952 \ - --hash=sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521 \ - --hash=sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a \ - --hash=sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160 \ - --hash=sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569 \ - --hash=sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829 \ - --hash=sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea \ - --hash=sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6 \ - --hash=sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80 \ - --hash=sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1 \ - --hash=sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681 \ - --hash=sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e \ - --hash=sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df \ - --hash=sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5 \ - --hash=sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60 \ - --hash=sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d \ - --hash=sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056 \ - --hash=sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa \ - --hash=sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714 \ - --hash=sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70 \ - --hash=sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22 \ - --hash=sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31 \ - --hash=sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f \ - --hash=sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3 \ - --hash=sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1 \ - --hash=sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac \ - --hash=sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af \ - --hash=sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026 \ - --hash=sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d \ - --hash=sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763 \ - --hash=sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e \ - --hash=sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e \ - --hash=sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7 \ - --hash=sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be \ - --hash=sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f \ - --hash=sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8 \ - --hash=sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84 \ - --hash=sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75 \ - --hash=sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a \ - --hash=sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb \ - --hash=sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac \ - --hash=sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5 \ - --hash=sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e - # via tiktoken -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf - # via - # langsmith - # requests-toolbelt - # tiktoken -requests-toolbelt==1.0.0 \ - --hash=sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6 \ - --hash=sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06 - # via langsmith -rsa==4.9.1 \ - --hash=sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762 \ - --hash=sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75 - # via python-jose -six==1.17.0 \ - --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ - --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 - # via - # ecdsa - # python-dateutil -sniffio==1.3.1 \ - --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ - --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc - # via - # anthropic - # openai -socksio==1.0.0 \ - --hash=sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3 \ - --hash=sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac - # via httpx -sounddevice==0.5.5 \ - --hash=sha256:05eb9fd6c54c38d67741441c19164c0dae8ce80453af2d8c4ad2e7823d15b722 \ - --hash=sha256:1234cc9b4c9df97b6cbe748146ae0ec64dd7d6e44739e8e42eaa5b595313a103 \ - --hash=sha256:22487b65198cb5bf2208755105b524f78ad173e5ab6b445bdab1c989f6698df3 \ - --hash=sha256:30ff99f6c107f49d25ad16a45cacd8d91c25a1bcdd3e81a206b921a3a6405b1f \ - --hash=sha256:3861901ddd8230d2e0e8ae62ac320cdd4c688d81df89da036dcb812f757bb3e6 \ - --hash=sha256:cfc6b2c49fb7f555591c78cb8ecf48d6a637fd5b6e1db5fec6ed9365d64b3519 - # via mediapipe -sqlalchemy==2.0.46 \ - --hash=sha256:181903fe8c1b9082995325f1b2e84ac078b1189e2819380c2303a5f90e114a62 \ - --hash=sha256:2347c3f0efc4de367ba00218e0ae5c4ba2306e47216ef80d6e31761ac97cb0b9 \ - --hash=sha256:261c4b1f101b4a411154f1da2b76497d73abbfc42740029205d4d01fa1052684 \ - --hash=sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366 \ - --hash=sha256:37fee2164cf21417478b6a906adc1a91d69ae9aba8f9533e67ce882f4bb1de53 \ - --hash=sha256:3a9a72b0da8387f15d5810f1facca8f879de9b85af8c645138cba61ea147968c \ - --hash=sha256:3c32e993bc57be6d177f7d5d31edb93f30726d798ad86ff9066d75d9bf2e0b6b \ - --hash=sha256:412f26bb4ba942d52016edc8d12fb15d91d3cd46b0047ba46e424213ad407bcb \ - --hash=sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863 \ - --hash=sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa \ - --hash=sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf \ - --hash=sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada \ - --hash=sha256:585af6afe518732d9ccd3aea33af2edaae4a7aa881af5d8f6f4fe3a368699597 \ - --hash=sha256:590be24e20e2424a4c3c1b0835e9405fa3d0af5823a1a9fc02e5dff56471515f \ - --hash=sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad \ - --hash=sha256:70ed2830b169a9960193f4d4322d22be5c0925357d82cbf485b3369893350908 \ - --hash=sha256:7568fe771f974abadce52669ef3a03150ff03186d8eb82613bc8adc435a03f01 \ - --hash=sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef \ - --hash=sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330 \ - --hash=sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f \ - --hash=sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee \ - --hash=sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e \ - --hash=sha256:9094c8b3197db12aa6f05c51c05daaad0a92b8c9af5388569847b03b1007fb1b \ - --hash=sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00 \ - --hash=sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764 \ - --hash=sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d \ - --hash=sha256:9d80ea2ac519c364a7286e8d765d6cd08648f5b21ca855a8017d9871f075542d \ - --hash=sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10 \ - --hash=sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2 \ - --hash=sha256:b1e14b2f6965a685c7128bd315e27387205429c2e339eeec55cb75ca4ab0ea2e \ - --hash=sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b \ - --hash=sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7 \ - --hash=sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447 \ - --hash=sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e \ - --hash=sha256:ea3cd46b6713a10216323cda3333514944e510aa691c945334713fca6b5279ff \ - --hash=sha256:ebf7e1e78af38047e08836d33502c7a278915698b7c2145d045f780201679999 \ - --hash=sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e \ - --hash=sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede - # via momshell-backend -starlette==0.50.0 \ - --hash=sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca \ - --hash=sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca - # via fastapi -tabulate==0.9.0 \ - --hash=sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c \ - --hash=sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f - # via edge-tts -tenacity==9.1.4 \ - --hash=sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55 \ - --hash=sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a - # via langchain-core -tiktoken==0.12.0 \ - --hash=sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa \ - --hash=sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e \ - --hash=sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb \ - --hash=sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25 \ - --hash=sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff \ - --hash=sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b \ - --hash=sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5 \ - --hash=sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3 \ - --hash=sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def \ - --hash=sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded \ - --hash=sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be \ - --hash=sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd \ - --hash=sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a \ - --hash=sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0 \ - --hash=sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0 \ - --hash=sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b \ - --hash=sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37 \ - --hash=sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb \ - --hash=sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3 \ - --hash=sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3 \ - --hash=sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b \ - --hash=sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a \ - --hash=sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3 \ - --hash=sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160 \ - --hash=sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967 \ - --hash=sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646 \ - --hash=sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931 \ - --hash=sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a \ - --hash=sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697 \ - --hash=sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8 \ - --hash=sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa \ - --hash=sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365 \ - --hash=sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e \ - --hash=sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830 \ - --hash=sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16 \ - --hash=sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88 \ - --hash=sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f \ - --hash=sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63 \ - --hash=sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad \ - --hash=sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc \ - --hash=sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71 \ - --hash=sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27 \ - --hash=sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd - # via langchain-openai -tqdm==4.67.3 \ - --hash=sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb \ - --hash=sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf - # via openai -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 - # via - # aiosignal - # anthropic - # anyio - # edge-tts - # fastapi - # langchain-core - # openai - # pydantic - # pydantic-core - # sqlalchemy - # starlette - # typing-inspection -typing-inspection==0.4.2 \ - --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ - --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 - # via - # pydantic - # pydantic-settings -urllib3==2.6.3 \ - --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ - --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 - # via requests -uuid-utils==0.14.0 \ - --hash=sha256:00ac3c6edfdaff7e1eed041f4800ae09a3361287be780d7610a90fdcde9befdc \ - --hash=sha256:09ae4a98416a440e78f7d9543d11b11cae4bab538b7ed94ec5da5221481748f2 \ - --hash=sha256:181bbcccb6f93d80a8504b5bd47b311a1c31395139596edbc47b154b0685b533 \ - --hash=sha256:191a90a6f3940d1b7322b6e6cceff4dd533c943659e0a15f788674407856a515 \ - --hash=sha256:32b372b8fd4ebd44d3a219e093fe981af4afdeda2994ee7db208ab065cfcd080 \ - --hash=sha256:3fd9112ca96978361201e669729784f26c71fecc9c13a7f8a07162c31bd4d1e2 \ - --hash=sha256:40be5bf0b13aa849d9062abc86c198be6a25ff35316ce0b89fc25f3bac6d525e \ - --hash=sha256:40ce3fd1a4fdedae618fc3edc8faf91897012469169d600133470f49fd699ed3 \ - --hash=sha256:4aa4525f4ad82f9d9c842f9a3703f1539c1808affbaec07bb1b842f6b8b96aa5 \ - --hash=sha256:4f0a730bbf2d8bb2c11b93e1005e91769f2f533fa1125ed1f00fd15b6fcc732b \ - --hash=sha256:5c8ae96101c3524ba8dbf762b6f05e9e9d896544786c503a727c5bf5cb9af1a7 \ - --hash=sha256:762e8d67992ac4d2454e24a141a1c82142b5bde10409818c62adbe9924ebc86d \ - --hash=sha256:971e8c26b90d8ae727e7f2ac3ee23e265971d448b3672882f2eb44828b2b8c3e \ - --hash=sha256:c51e4818fdb08ccec12dc7083a01f49507b4608770a0ab22368001685d59381b \ - --hash=sha256:c7343862a2359e0bd48a7f3dfb5105877a1728677818bb694d9f40703264a2db \ - --hash=sha256:cdbd82ff20147461caefc375551595ecf77ebb384e46267f128aca45a0f2cdfc \ - --hash=sha256:d5cde1fa82804a8f9d2907b7aec2009d440062c63f04abbdb825fce717a5e860 \ - --hash=sha256:ec2fd80adf8e0e6589d40699e6f6df94c93edcc16dd999be0438dd007c77b151 \ - --hash=sha256:efe881eb43a5504fad922644cb93d725fd8a6a6d949bd5a4b4b7d1a1587c7fd1 \ - --hash=sha256:eff57e8a5d540006ce73cf0841a643d445afe78ba12e75ac53a95ca2924a56be \ - --hash=sha256:f6695c0bed8b18a904321e115afe73b34444bc8451d0ce3244a1ec3b84deb0e5 \ - --hash=sha256:fc5bac21e9933ea6c590433c11aa54aaca599f690c08069e364eb13a12f670b4 - # via - # langchain-core - # langsmith -uvicorn==0.40.0 \ - --hash=sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea \ - --hash=sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee - # via momshell-backend -uvloop==0.22.1 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32' \ - --hash=sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e \ - --hash=sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8 \ - --hash=sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad \ - --hash=sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35 \ - --hash=sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289 \ - --hash=sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142 \ - --hash=sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77 \ - --hash=sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733 \ - --hash=sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74 \ - --hash=sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0 \ - --hash=sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6 \ - --hash=sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473 \ - --hash=sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21 \ - --hash=sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705 \ - --hash=sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702 \ - --hash=sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f \ - --hash=sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e \ - --hash=sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d \ - --hash=sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370 \ - --hash=sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4 \ - --hash=sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079 \ - --hash=sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6 \ - --hash=sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3 \ - --hash=sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21 \ - --hash=sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c \ - --hash=sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e \ - --hash=sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25 \ - --hash=sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9 \ - --hash=sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88 \ - --hash=sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2 \ - --hash=sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42 - # via uvicorn -watchfiles==1.1.1 \ - --hash=sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c \ - --hash=sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510 \ - --hash=sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0 \ - --hash=sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18 \ - --hash=sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219 \ - --hash=sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803 \ - --hash=sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94 \ - --hash=sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6 \ - --hash=sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce \ - --hash=sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099 \ - --hash=sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae \ - --hash=sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4 \ - --hash=sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43 \ - --hash=sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd \ - --hash=sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10 \ - --hash=sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374 \ - --hash=sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051 \ - --hash=sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49 \ - --hash=sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7 \ - --hash=sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77 \ - --hash=sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b \ - --hash=sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741 \ - --hash=sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33 \ - --hash=sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42 \ - --hash=sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5 \ - --hash=sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da \ - --hash=sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e \ - --hash=sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05 \ - --hash=sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a \ - --hash=sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d \ - --hash=sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701 \ - --hash=sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101 \ - --hash=sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6 \ - --hash=sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb \ - --hash=sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620 \ - --hash=sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6 \ - --hash=sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef \ - --hash=sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261 \ - --hash=sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02 \ - --hash=sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af \ - --hash=sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9 \ - --hash=sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21 \ - --hash=sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336 \ - --hash=sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d \ - --hash=sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c \ - --hash=sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81 \ - --hash=sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9 \ - --hash=sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff \ - --hash=sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2 \ - --hash=sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc \ - --hash=sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404 \ - --hash=sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01 \ - --hash=sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18 \ - --hash=sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606 \ - --hash=sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04 \ - --hash=sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3 \ - --hash=sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14 \ - --hash=sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610 \ - --hash=sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0 \ - --hash=sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150 \ - --hash=sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5 \ - --hash=sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a \ - --hash=sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b \ - --hash=sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d \ - --hash=sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70 \ - --hash=sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24 \ - --hash=sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e \ - --hash=sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5 \ - --hash=sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88 \ - --hash=sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb \ - --hash=sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849 \ - --hash=sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44 \ - --hash=sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428 \ - --hash=sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b \ - --hash=sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5 \ - --hash=sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa \ - --hash=sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf - # via uvicorn -websockets==16.0 \ - --hash=sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c \ - --hash=sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe \ - --hash=sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e \ - --hash=sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec \ - --hash=sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1 \ - --hash=sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64 \ - --hash=sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8 \ - --hash=sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206 \ - --hash=sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156 \ - --hash=sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d \ - --hash=sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad \ - --hash=sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2 \ - --hash=sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03 \ - --hash=sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8 \ - --hash=sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230 \ - --hash=sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8 \ - --hash=sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea \ - --hash=sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641 \ - --hash=sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6 \ - --hash=sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6 \ - --hash=sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5 \ - --hash=sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f \ - --hash=sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00 \ - --hash=sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e \ - --hash=sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b \ - --hash=sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39 \ - --hash=sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9 \ - --hash=sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79 \ - --hash=sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0 \ - --hash=sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac \ - --hash=sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5 \ - --hash=sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c \ - --hash=sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8 \ - --hash=sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1 \ - --hash=sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244 \ - --hash=sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3 \ - --hash=sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767 \ - --hash=sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a \ - --hash=sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d \ - --hash=sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd \ - --hash=sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e \ - --hash=sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944 \ - --hash=sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82 \ - --hash=sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d \ - --hash=sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4 \ - --hash=sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5 \ - --hash=sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904 \ - --hash=sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f \ - --hash=sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c \ - --hash=sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89 \ - --hash=sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da \ - --hash=sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4 - # via - # momshell-backend - # uvicorn -xxhash==3.6.0 \ - --hash=sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad \ - --hash=sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c \ - --hash=sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3 \ - --hash=sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b \ - --hash=sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f \ - --hash=sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c \ - --hash=sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1 \ - --hash=sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0 \ - --hash=sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec \ - --hash=sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d \ - --hash=sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67 \ - --hash=sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799 \ - --hash=sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679 \ - --hash=sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef \ - --hash=sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8 \ - --hash=sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa \ - --hash=sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa \ - --hash=sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad \ - --hash=sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7 \ - --hash=sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5 \ - --hash=sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11 \ - --hash=sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae \ - --hash=sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d \ - --hash=sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6 \ - --hash=sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2 \ - --hash=sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89 \ - --hash=sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e \ - --hash=sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6 \ - --hash=sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb \ - --hash=sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3 \ - --hash=sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b \ - --hash=sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db \ - --hash=sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119 \ - --hash=sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec \ - --hash=sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518 \ - --hash=sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296 \ - --hash=sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033 \ - --hash=sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729 \ - --hash=sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca \ - --hash=sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063 \ - --hash=sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5 \ - --hash=sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f \ - --hash=sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42 \ - --hash=sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e \ - --hash=sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392 \ - --hash=sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f \ - --hash=sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd \ - --hash=sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77 \ - --hash=sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d \ - --hash=sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1 \ - --hash=sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374 \ - --hash=sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263 \ - --hash=sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13 \ - --hash=sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62 \ - --hash=sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11 \ - --hash=sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0 \ - --hash=sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b \ - --hash=sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2 \ - --hash=sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6 \ - --hash=sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e \ - --hash=sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702 \ - --hash=sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405 \ - --hash=sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f \ - --hash=sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3 \ - --hash=sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a \ - --hash=sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8 \ - --hash=sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db \ - --hash=sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99 \ - --hash=sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a \ - --hash=sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204 \ - --hash=sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b \ - --hash=sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546 \ - --hash=sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95 \ - --hash=sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9 \ - --hash=sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54 \ - --hash=sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c \ - --hash=sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152 \ - --hash=sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4 \ - --hash=sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93 \ - --hash=sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd \ - --hash=sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd \ - --hash=sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248 \ - --hash=sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd \ - --hash=sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6 \ - --hash=sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf \ - --hash=sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7 \ - --hash=sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490 \ - --hash=sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0 \ - --hash=sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829 \ - --hash=sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746 \ - --hash=sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292 \ - --hash=sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6 \ - --hash=sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd \ - --hash=sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7 \ - --hash=sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0 \ - --hash=sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee - # via langgraph -yarl==1.22.0 \ - --hash=sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a \ - --hash=sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b \ - --hash=sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da \ - --hash=sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093 \ - --hash=sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79 \ - --hash=sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683 \ - --hash=sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2 \ - --hash=sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff \ - --hash=sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02 \ - --hash=sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03 \ - --hash=sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511 \ - --hash=sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c \ - --hash=sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124 \ - --hash=sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c \ - --hash=sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da \ - --hash=sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2 \ - --hash=sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0 \ - --hash=sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d \ - --hash=sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53 \ - --hash=sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138 \ - --hash=sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4 \ - --hash=sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7 \ - --hash=sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d \ - --hash=sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503 \ - --hash=sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d \ - --hash=sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2 \ - --hash=sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa \ - --hash=sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f \ - --hash=sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1 \ - --hash=sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d \ - --hash=sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694 \ - --hash=sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3 \ - --hash=sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a \ - --hash=sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d \ - --hash=sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a \ - --hash=sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6 \ - --hash=sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b \ - --hash=sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5 \ - --hash=sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f \ - --hash=sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df \ - --hash=sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b \ - --hash=sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6 \ - --hash=sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b \ - --hash=sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967 \ - --hash=sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2 \ - --hash=sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708 \ - --hash=sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8 \ - --hash=sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10 \ - --hash=sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b \ - --hash=sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028 \ - --hash=sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e \ - --hash=sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33 \ - --hash=sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590 \ - --hash=sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c \ - --hash=sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53 \ - --hash=sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74 \ - --hash=sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f \ - --hash=sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1 \ - --hash=sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27 \ - --hash=sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520 \ - --hash=sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca \ - --hash=sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273 \ - --hash=sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e \ - --hash=sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601 \ - --hash=sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376 \ - --hash=sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7 \ - --hash=sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb \ - --hash=sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65 \ - --hash=sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784 \ - --hash=sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71 \ - --hash=sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b \ - --hash=sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a \ - --hash=sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c \ - --hash=sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face \ - --hash=sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d \ - --hash=sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e \ - --hash=sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9 \ - --hash=sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95 \ - --hash=sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed \ - --hash=sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf \ - --hash=sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca \ - --hash=sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62 \ - --hash=sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df \ - --hash=sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67 \ - --hash=sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f \ - --hash=sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529 \ - --hash=sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486 \ - --hash=sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a \ - --hash=sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e \ - --hash=sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74 \ - --hash=sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d \ - --hash=sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b \ - --hash=sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2 \ - --hash=sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e \ - --hash=sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8 \ - --hash=sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82 \ - --hash=sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd \ - --hash=sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249 - # via aiohttp -zstandard==0.25.0 \ - --hash=sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64 \ - --hash=sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a \ - --hash=sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f \ - --hash=sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6 \ - --hash=sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431 \ - --hash=sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250 \ - --hash=sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f \ - --hash=sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851 \ - --hash=sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3 \ - --hash=sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9 \ - --hash=sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6 \ - --hash=sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5 \ - --hash=sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439 \ - --hash=sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137 \ - --hash=sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa \ - --hash=sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd \ - --hash=sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043 \ - --hash=sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611 \ - --hash=sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b \ - --hash=sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088 \ - --hash=sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e \ - --hash=sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa \ - --hash=sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf \ - --hash=sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902 \ - --hash=sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc \ - --hash=sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98 \ - --hash=sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a \ - --hash=sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097 \ - --hash=sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea \ - --hash=sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09 \ - --hash=sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb \ - --hash=sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7 \ - --hash=sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b \ - --hash=sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b \ - --hash=sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91 \ - --hash=sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049 \ - --hash=sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a \ - --hash=sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00 \ - --hash=sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072 \ - --hash=sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c \ - --hash=sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c \ - --hash=sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065 \ - --hash=sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512 \ - --hash=sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1 \ - --hash=sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f \ - --hash=sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2 \ - --hash=sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7 \ - --hash=sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b \ - --hash=sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea \ - --hash=sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277 \ - --hash=sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2 \ - --hash=sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778 \ - --hash=sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859 \ - --hash=sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d \ - --hash=sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12 \ - --hash=sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2 \ - --hash=sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0 \ - --hash=sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3 \ - --hash=sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f \ - --hash=sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94 \ - --hash=sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708 \ - --hash=sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313 \ - --hash=sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4 \ - --hash=sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c \ - --hash=sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344 \ - --hash=sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551 \ - --hash=sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01 - # via langsmith diff --git a/backend/scripts/__init__.py b/backend/scripts/__init__.py deleted file mode 100644 index b9356769..00000000 --- a/backend/scripts/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Backend scripts.""" diff --git a/backend/scripts/create_admin.py b/backend/scripts/create_admin.py deleted file mode 100644 index bbb3954f..00000000 --- a/backend/scripts/create_admin.py +++ /dev/null @@ -1,70 +0,0 @@ -"""Create or promote a user to admin.""" - -import asyncio -import sys - -from sqlalchemy import select - -from app.core.database import async_session_maker, init_db -from app.services.auth.security import get_password_hash -from app.services.community.enums import UserRole -from app.services.community.models import User - - -async def create_admin( - username: str, - email: str, - password: str, - nickname: str = "管理员", -) -> None: - """Create a new admin user or promote existing user to admin.""" - await init_db() - - async with async_session_maker() as db: - # Check if user exists - result = await db.execute( - select(User).where((User.username == username) | (User.email == email)) - ) - existing_user = result.scalar_one_or_none() - - if existing_user: - # Promote to admin - existing_user.role = UserRole.ADMIN - if password: - existing_user.password_hash = get_password_hash(password) - await db.commit() - print(f"用户 '{existing_user.username}' 已升级为管理员") - else: - # Create new admin user - admin = User( - username=username, - email=email, - password_hash=get_password_hash(password), - nickname=nickname, - role=UserRole.ADMIN, - is_active=True, - is_banned=False, - ) - db.add(admin) - await db.commit() - print(f"管理员账号 '{username}' 创建成功") - - -def main(): - if len(sys.argv) < 4: - print("用法: python -m scripts.create_admin <用户名> <邮箱> <密码> [昵称]") - print( - "示例: python -m scripts.create_admin admin admin@example.com 123456 管理员" - ) - sys.exit(1) - - username = sys.argv[1] - email = sys.argv[2] - password = sys.argv[3] - nickname = sys.argv[4] if len(sys.argv) > 4 else "管理员" - - asyncio.run(create_admin(username, email, password, nickname)) - - -if __name__ == "__main__": - main() diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/tts_cache/.gitkeep b/backend/tts_cache/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/uv.lock b/backend/uv.lock deleted file mode 100644 index 1e8e25a8..00000000 --- a/backend/uv.lock +++ /dev/null @@ -1,3475 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.11" - -[[package]] -name = "absl-py" -version = "2.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/10/2a/c93173ffa1b39c1d0395b7e842bbdc62e556ca9d8d3b5572926f3e4ca752/absl_py-2.3.1.tar.gz", hash = "sha256:a97820526f7fbfd2ec1bce83f3f25e3a14840dac0d8e02a0b71cd75db3f77fc9", size = 116588, upload-time = "2025-07-03T09:31:44.05Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/aa/ba0014cc4659328dc818a28827be78e6d97312ab0cb98105a770924dc11e/absl_py-2.3.1-py3-none-any.whl", hash = "sha256:eeecf07f0c2a93ace0772c92e596ace6d3d3996c042b2128459aaae2a76de11d", size = 135811, upload-time = "2025-07-03T09:31:42.253Z" }, -] - -[[package]] -name = "aiohappyeyeballs" -version = "2.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, -] - -[[package]] -name = "aiohttp" -version = "3.13.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohappyeyeballs" }, - { name = "aiosignal" }, - { name = "attrs" }, - { name = "frozenlist" }, - { name = "multidict" }, - { name = "propcache" }, - { name = "yarl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, - { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, - { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, - { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, - { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, - { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, - { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, - { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, - { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, - { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, - { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, - { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, - { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, - { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" }, - { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" }, - { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, - { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, - { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, - { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, - { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, - { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, - { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, - { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, - { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, - { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, - { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, - { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, - { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, - { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, - { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, - { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, - { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, - { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, - { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, - { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, - { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, - { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, - { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, - { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, - { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, - { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, - { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, - { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, - { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, - { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, - { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, - { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, - { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, - { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, - { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, - { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, - { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, - { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, - { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, - { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, - { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, - { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, - { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, - { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, - { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, - { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, - { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, - { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, -] - -[[package]] -name = "aiosignal" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "frozenlist" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, -] - -[[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 = "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 = "anthropic" -version = "0.76.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "docstring-parser" }, - { name = "httpx" }, - { name = "jiter" }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6e/be/d11abafaa15d6304826438170f7574d750218f49a106c54424a40cef4494/anthropic-0.76.0.tar.gz", hash = "sha256:e0cae6a368986d5cf6df743dfbb1b9519e6a9eee9c6c942ad8121c0b34416ffe", size = 495483, upload-time = "2026-01-13T18:41:14.908Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/70/7b0fd9c1a738f59d3babe2b4212031c34ab7d0fda4ffef15b58a55c5bcea/anthropic-0.76.0-py3-none-any.whl", hash = "sha256:81efa3113901192af2f0fe977d3ec73fdadb1e691586306c4256cd6d5ccc331c", size = 390309, upload-time = "2026-01-13T18:41:13.483Z" }, -] - -[[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 = "attrs" -version = "25.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, -] - -[[package]] -name = "bcrypt" -version = "4.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/ae/3af7d006aacf513975fd1948a6b4d6f8b4a307f8a244e1a3d3774b297aad/bcrypt-4.0.1.tar.gz", hash = "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd", size = 25498, upload-time = "2022-10-09T15:36:49.775Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/d4/3b2657bd58ef02b23a07729b0df26f21af97169dbd0b5797afa9e97ebb49/bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f", size = 473446, upload-time = "2022-10-09T15:36:25.481Z" }, - { url = "https://files.pythonhosted.org/packages/ec/0a/1582790232fef6c2aa201f345577306b8bfe465c2c665dec04c86a016879/bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0", size = 583044, upload-time = "2022-10-09T15:37:09.447Z" }, - { url = "https://files.pythonhosted.org/packages/41/16/49ff5146fb815742ad58cafb5034907aa7f166b1344d0ddd7fd1c818bd17/bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410", size = 583189, upload-time = "2022-10-09T15:37:10.69Z" }, - { url = "https://files.pythonhosted.org/packages/aa/48/fd2b197a9741fa790ba0b88a9b10b5e88e62ff5cf3e1bc96d8354d7ce613/bcrypt-4.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344", size = 593473, upload-time = "2022-10-09T15:36:27.195Z" }, - { url = "https://files.pythonhosted.org/packages/7d/50/e683d8418974a602ba40899c8a5c38b3decaf5a4d36c32fc65dce454d8a8/bcrypt-4.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a", size = 593249, upload-time = "2022-10-09T15:36:28.481Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a7/ee4561fd9b78ca23c8e5591c150cc58626a5dfb169345ab18e1c2c664ee0/bcrypt-4.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3", size = 583586, upload-time = "2022-10-09T15:37:11.962Z" }, - { url = "https://files.pythonhosted.org/packages/64/fe/da28a5916128d541da0993328dc5cf4b43dfbf6655f2c7a2abe26ca2dc88/bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2", size = 593659, upload-time = "2022-10-09T15:36:30.049Z" }, - { url = "https://files.pythonhosted.org/packages/dd/4f/3632a69ce344c1551f7c9803196b191a8181c6a1ad2362c225581ef0d383/bcrypt-4.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535", size = 613116, upload-time = "2022-10-09T15:37:14.107Z" }, - { url = "https://files.pythonhosted.org/packages/87/69/edacb37481d360d06fc947dab5734aaf511acb7d1a1f9e2849454376c0f8/bcrypt-4.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e", size = 624290, upload-time = "2022-10-09T15:36:31.251Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ca/6a534669890725cbb8c1fb4622019be31813c8edaa7b6d5b62fc9360a17e/bcrypt-4.0.1-cp36-abi3-win32.whl", hash = "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab", size = 159428, upload-time = "2022-10-09T15:36:32.893Z" }, - { url = "https://files.pythonhosted.org/packages/46/81/d8c22cd7e5e1c6a7d48e41a1d1d46c92f17dae70a54d9814f746e6027dec/bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9", size = 152930, upload-time = "2022-10-09T15:36:34.635Z" }, -] - -[[package]] -name = "certifi" -version = "2026.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, -] - -[[package]] -name = "cffi" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, -] - -[[package]] -name = "cfgv" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, - { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, - { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, - { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, - { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, - { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, - { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, - { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, - { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, - { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, - { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, - { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, - { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, - { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, - { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, - { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, - { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, - { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, - { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, - { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, - { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, - { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, - { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, - { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, - { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, - { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, - { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, -] - -[[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 = "contourpy" -version = "1.3.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, - { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, - { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, - { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, - { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, - { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, - { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, - { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, - { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, - { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, - { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, - { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, - { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, - { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, - { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, - { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, - { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, - { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, - { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, - { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, - { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, - { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, - { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, - { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, - { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, - { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, - { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, - { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, - { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, - { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, - { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, - { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, - { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, - { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, - { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, - { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, - { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, - { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, - { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, - { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, - { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, - { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, - { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, - { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, - { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, - { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, - { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, - { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, - { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, - { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, - { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, - { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, - { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, - { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, - { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, - { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, - { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, - { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, - { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, - { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, - { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, - { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, - { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, - { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, -] - -[[package]] -name = "cryptography" -version = "46.0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/78/19/f748958276519adf6a0c1e79e7b8860b4830dda55ccdf29f2719b5fc499c/cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59", size = 749301, upload-time = "2026-01-28T00:24:37.379Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/99/157aae7949a5f30d51fcb1a9851e8ebd5c74bf99b5285d8bb4b8b9ee641e/cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485", size = 7173686, upload-time = "2026-01-28T00:23:07.515Z" }, - { url = "https://files.pythonhosted.org/packages/87/91/874b8910903159043b5c6a123b7e79c4559ddd1896e38967567942635778/cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc", size = 4275871, upload-time = "2026-01-28T00:23:09.439Z" }, - { url = "https://files.pythonhosted.org/packages/c0/35/690e809be77896111f5b195ede56e4b4ed0435b428c2f2b6d35046fbb5e8/cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0", size = 4423124, upload-time = "2026-01-28T00:23:11.529Z" }, - { url = "https://files.pythonhosted.org/packages/1a/5b/a26407d4f79d61ca4bebaa9213feafdd8806dc69d3d290ce24996d3cfe43/cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa", size = 4277090, upload-time = "2026-01-28T00:23:13.123Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d8/4bb7aec442a9049827aa34cee1aa83803e528fa55da9a9d45d01d1bb933e/cryptography-46.0.4-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81", size = 4947652, upload-time = "2026-01-28T00:23:14.554Z" }, - { url = "https://files.pythonhosted.org/packages/2b/08/f83e2e0814248b844265802d081f2fac2f1cbe6cd258e72ba14ff006823a/cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255", size = 4455157, upload-time = "2026-01-28T00:23:16.443Z" }, - { url = "https://files.pythonhosted.org/packages/0a/05/19d849cf4096448779d2dcc9bb27d097457dac36f7273ffa875a93b5884c/cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e", size = 3981078, upload-time = "2026-01-28T00:23:17.838Z" }, - { url = "https://files.pythonhosted.org/packages/e6/89/f7bac81d66ba7cde867a743ea5b37537b32b5c633c473002b26a226f703f/cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c", size = 4276213, upload-time = "2026-01-28T00:23:19.257Z" }, - { url = "https://files.pythonhosted.org/packages/da/9f/7133e41f24edd827020ad21b068736e792bc68eecf66d93c924ad4719fb3/cryptography-46.0.4-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32", size = 4912190, upload-time = "2026-01-28T00:23:21.244Z" }, - { url = "https://files.pythonhosted.org/packages/a6/f7/6d43cbaddf6f65b24816e4af187d211f0bc536a29961f69faedc48501d8e/cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616", size = 4454641, upload-time = "2026-01-28T00:23:22.866Z" }, - { url = "https://files.pythonhosted.org/packages/9e/4f/ebd0473ad656a0ac912a16bd07db0f5d85184924e14fc88feecae2492834/cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0", size = 4405159, upload-time = "2026-01-28T00:23:25.278Z" }, - { url = "https://files.pythonhosted.org/packages/d1/f7/7923886f32dc47e27adeff8246e976d77258fd2aa3efdd1754e4e323bf49/cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0", size = 4666059, upload-time = "2026-01-28T00:23:26.766Z" }, - { url = "https://files.pythonhosted.org/packages/eb/a7/0fca0fd3591dffc297278a61813d7f661a14243dd60f499a7a5b48acb52a/cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5", size = 3026378, upload-time = "2026-01-28T00:23:28.317Z" }, - { url = "https://files.pythonhosted.org/packages/2d/12/652c84b6f9873f0909374864a57b003686c642ea48c84d6c7e2c515e6da5/cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b", size = 3478614, upload-time = "2026-01-28T00:23:30.275Z" }, - { url = "https://files.pythonhosted.org/packages/b9/27/542b029f293a5cce59349d799d4d8484b3b1654a7b9a0585c266e974a488/cryptography-46.0.4-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908", size = 7116417, upload-time = "2026-01-28T00:23:31.958Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f5/559c25b77f40b6bf828eabaf988efb8b0e17b573545edb503368ca0a2a03/cryptography-46.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da", size = 4264508, upload-time = "2026-01-28T00:23:34.264Z" }, - { url = "https://files.pythonhosted.org/packages/49/a1/551fa162d33074b660dc35c9bc3616fefa21a0e8c1edd27b92559902e408/cryptography-46.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829", size = 4409080, upload-time = "2026-01-28T00:23:35.793Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6a/4d8d129a755f5d6df1bbee69ea2f35ebfa954fa1847690d1db2e8bca46a5/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2", size = 4270039, upload-time = "2026-01-28T00:23:37.263Z" }, - { url = "https://files.pythonhosted.org/packages/4c/f5/ed3fcddd0a5e39321e595e144615399e47e7c153a1fb8c4862aec3151ff9/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085", size = 4926748, upload-time = "2026-01-28T00:23:38.884Z" }, - { url = "https://files.pythonhosted.org/packages/43/ae/9f03d5f0c0c00e85ecb34f06d3b79599f20630e4db91b8a6e56e8f83d410/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b", size = 4442307, upload-time = "2026-01-28T00:23:40.56Z" }, - { url = "https://files.pythonhosted.org/packages/8b/22/e0f9f2dae8040695103369cf2283ef9ac8abe4d51f68710bec2afd232609/cryptography-46.0.4-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd", size = 3959253, upload-time = "2026-01-28T00:23:42.827Z" }, - { url = "https://files.pythonhosted.org/packages/01/5b/6a43fcccc51dae4d101ac7d378a8724d1ba3de628a24e11bf2f4f43cba4d/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2", size = 4269372, upload-time = "2026-01-28T00:23:44.655Z" }, - { url = "https://files.pythonhosted.org/packages/17/b7/0f6b8c1dd0779df2b526e78978ff00462355e31c0a6f6cff8a3e99889c90/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e", size = 4891908, upload-time = "2026-01-28T00:23:46.48Z" }, - { url = "https://files.pythonhosted.org/packages/83/17/259409b8349aa10535358807a472c6a695cf84f106022268d31cea2b6c97/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f", size = 4441254, upload-time = "2026-01-28T00:23:48.403Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fe/e4a1b0c989b00cee5ffa0764401767e2d1cf59f45530963b894129fd5dce/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82", size = 4396520, upload-time = "2026-01-28T00:23:50.26Z" }, - { url = "https://files.pythonhosted.org/packages/b3/81/ba8fd9657d27076eb40d6a2f941b23429a3c3d2f56f5a921d6b936a27bc9/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c", size = 4651479, upload-time = "2026-01-28T00:23:51.674Z" }, - { url = "https://files.pythonhosted.org/packages/00/03/0de4ed43c71c31e4fe954edd50b9d28d658fef56555eba7641696370a8e2/cryptography-46.0.4-cp314-cp314t-win32.whl", hash = "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061", size = 3001986, upload-time = "2026-01-28T00:23:53.485Z" }, - { url = "https://files.pythonhosted.org/packages/5c/70/81830b59df7682917d7a10f833c4dab2a5574cd664e86d18139f2b421329/cryptography-46.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7", size = 3468288, upload-time = "2026-01-28T00:23:55.09Z" }, - { url = "https://files.pythonhosted.org/packages/56/f7/f648fdbb61d0d45902d3f374217451385edc7e7768d1b03ff1d0e5ffc17b/cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab", size = 7169583, upload-time = "2026-01-28T00:23:56.558Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cc/8f3224cbb2a928de7298d6ed4790f5ebc48114e02bdc9559196bfb12435d/cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef", size = 4275419, upload-time = "2026-01-28T00:23:58.364Z" }, - { url = "https://files.pythonhosted.org/packages/17/43/4a18faa7a872d00e4264855134ba82d23546c850a70ff209e04ee200e76f/cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d", size = 4419058, upload-time = "2026-01-28T00:23:59.867Z" }, - { url = "https://files.pythonhosted.org/packages/ee/64/6651969409821d791ba12346a124f55e1b76f66a819254ae840a965d4b9c/cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973", size = 4278151, upload-time = "2026-01-28T00:24:01.731Z" }, - { url = "https://files.pythonhosted.org/packages/20/0b/a7fce65ee08c3c02f7a8310cc090a732344066b990ac63a9dfd0a655d321/cryptography-46.0.4-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4", size = 4939441, upload-time = "2026-01-28T00:24:03.175Z" }, - { url = "https://files.pythonhosted.org/packages/db/a7/20c5701e2cd3e1dfd7a19d2290c522a5f435dd30957d431dcb531d0f1413/cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af", size = 4451617, upload-time = "2026-01-28T00:24:05.403Z" }, - { url = "https://files.pythonhosted.org/packages/00/dc/3e16030ea9aa47b63af6524c354933b4fb0e352257c792c4deeb0edae367/cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263", size = 3977774, upload-time = "2026-01-28T00:24:06.851Z" }, - { url = "https://files.pythonhosted.org/packages/42/c8/ad93f14118252717b465880368721c963975ac4b941b7ef88f3c56bf2897/cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095", size = 4277008, upload-time = "2026-01-28T00:24:08.926Z" }, - { url = "https://files.pythonhosted.org/packages/00/cf/89c99698151c00a4631fbfcfcf459d308213ac29e321b0ff44ceeeac82f1/cryptography-46.0.4-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b", size = 4903339, upload-time = "2026-01-28T00:24:12.009Z" }, - { url = "https://files.pythonhosted.org/packages/03/c3/c90a2cb358de4ac9309b26acf49b2a100957e1ff5cc1e98e6c4996576710/cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019", size = 4451216, upload-time = "2026-01-28T00:24:13.975Z" }, - { url = "https://files.pythonhosted.org/packages/96/2c/8d7f4171388a10208671e181ca43cdc0e596d8259ebacbbcfbd16de593da/cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4", size = 4404299, upload-time = "2026-01-28T00:24:16.169Z" }, - { url = "https://files.pythonhosted.org/packages/e9/23/cbb2036e450980f65c6e0a173b73a56ff3bccd8998965dea5cc9ddd424a5/cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", size = 4664837, upload-time = "2026-01-28T00:24:17.629Z" }, - { url = "https://files.pythonhosted.org/packages/0a/21/f7433d18fe6d5845329cbdc597e30caf983229c7a245bcf54afecc555938/cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc", size = 3009779, upload-time = "2026-01-28T00:24:20.198Z" }, - { url = "https://files.pythonhosted.org/packages/3a/6a/bd2e7caa2facffedf172a45c1a02e551e6d7d4828658c9a245516a598d94/cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976", size = 3466633, upload-time = "2026-01-28T00:24:21.851Z" }, - { url = "https://files.pythonhosted.org/packages/59/e0/f9c6c53e1f2a1c2507f00f2faba00f01d2f334b35b0fbfe5286715da2184/cryptography-46.0.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b", size = 3476316, upload-time = "2026-01-28T00:24:24.144Z" }, - { url = "https://files.pythonhosted.org/packages/27/7a/f8d2d13227a9a1a9fe9c7442b057efecffa41f1e3c51d8622f26b9edbe8f/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da", size = 4216693, upload-time = "2026-01-28T00:24:25.758Z" }, - { url = "https://files.pythonhosted.org/packages/c5/de/3787054e8f7972658370198753835d9d680f6cd4a39df9f877b57f0dd69c/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80", size = 4382765, upload-time = "2026-01-28T00:24:27.577Z" }, - { url = "https://files.pythonhosted.org/packages/8a/5f/60e0afb019973ba6a0b322e86b3d61edf487a4f5597618a430a2a15f2d22/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822", size = 4216066, upload-time = "2026-01-28T00:24:29.056Z" }, - { url = "https://files.pythonhosted.org/packages/81/8e/bf4a0de294f147fee66f879d9bae6f8e8d61515558e3d12785dd90eca0be/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947", size = 4382025, upload-time = "2026-01-28T00:24:30.681Z" }, - { url = "https://files.pythonhosted.org/packages/79/f4/9ceb90cfd6a3847069b0b0b353fd3075dc69b49defc70182d8af0c4ca390/cryptography-46.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3", size = 3406043, upload-time = "2026-01-28T00:24:32.236Z" }, -] - -[[package]] -name = "cycler" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, -] - -[[package]] -name = "distlib" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, -] - -[[package]] -name = "distro" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, -] - -[[package]] -name = "dnspython" -version = "2.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, -] - -[[package]] -name = "docstring-parser" -version = "0.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, -] - -[[package]] -name = "ecdsa" -version = "0.19.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/1f/924e3caae75f471eae4b26bd13b698f6af2c44279f67af317439c2f4c46a/ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61", size = 201793, upload-time = "2025-03-13T11:52:43.25Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3", size = 150607, upload-time = "2025-03-13T11:52:41.757Z" }, -] - -[[package]] -name = "edge-tts" -version = "7.2.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "certifi" }, - { name = "tabulate" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/16/d2/1ce38f6e4fe7275207f4033b0971db489a0b594340ae6bac2320127e71ee/edge_tts-7.2.7.tar.gz", hash = "sha256:0127fba57a742bc48ff0a2a3b24b8324f7859260185274c335b4e54735aff325", size = 27508, upload-time = "2025-12-12T20:54:28.403Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/89/92ac6b154ab87d236c15e5e0c73cb99be58efb1ea3eb9318c266bf9a36bf/edge_tts-7.2.7-py3-none-any.whl", hash = "sha256:ac11d9e834347e5ee62cbe72e8a56ffd65d3c4e795be14b1e593b72cf6480dd9", size = 30556, upload-time = "2025-12-12T20:54:26.956Z" }, -] - -[[package]] -name = "email-validator" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "dnspython" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, -] - -[[package]] -name = "fastapi" -version = "0.128.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-doc" }, - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, -] - -[[package]] -name = "filelock" -version = "3.20.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, -] - -[[package]] -name = "flatbuffers" -version = "25.12.19" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, -] - -[[package]] -name = "fonttools" -version = "4.61.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213, upload-time = "2025-12-12T17:29:46.675Z" }, - { url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689, upload-time = "2025-12-12T17:29:48.769Z" }, - { url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809, upload-time = "2025-12-12T17:29:51.701Z" }, - { url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039, upload-time = "2025-12-12T17:29:53.659Z" }, - { url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714, upload-time = "2025-12-12T17:29:55.592Z" }, - { url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648, upload-time = "2025-12-12T17:29:57.861Z" }, - { url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681, upload-time = "2025-12-12T17:29:59.943Z" }, - { url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951, upload-time = "2025-12-12T17:30:02.254Z" }, - { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" }, - { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" }, - { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" }, - { url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" }, - { url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" }, - { url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" }, - { url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" }, - { url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" }, - { url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454, upload-time = "2025-12-12T17:30:24.938Z" }, - { url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191, upload-time = "2025-12-12T17:30:27.343Z" }, - { url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410, upload-time = "2025-12-12T17:30:29.771Z" }, - { url = "https://files.pythonhosted.org/packages/b0/8d/6fb3494dfe61a46258cd93d979cf4725ded4eb46c2a4ca35e4490d84daea/fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd", size = 4984460, upload-time = "2025-12-12T17:30:32.073Z" }, - { url = "https://files.pythonhosted.org/packages/f7/f1/a47f1d30b3dc00d75e7af762652d4cbc3dff5c2697a0dbd5203c81afd9c3/fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3", size = 4925800, upload-time = "2025-12-12T17:30:34.339Z" }, - { url = "https://files.pythonhosted.org/packages/a7/01/e6ae64a0981076e8a66906fab01539799546181e32a37a0257b77e4aa88b/fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d", size = 5067859, upload-time = "2025-12-12T17:30:36.593Z" }, - { url = "https://files.pythonhosted.org/packages/73/aa/28e40b8d6809a9b5075350a86779163f074d2b617c15d22343fce81918db/fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c", size = 2267821, upload-time = "2025-12-12T17:30:38.478Z" }, - { url = "https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b", size = 2318169, upload-time = "2025-12-12T17:30:40.951Z" }, - { url = "https://files.pythonhosted.org/packages/32/8f/4e7bf82c0cbb738d3c2206c920ca34ca74ef9dabde779030145d28665104/fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd", size = 2846094, upload-time = "2025-12-12T17:30:43.511Z" }, - { url = "https://files.pythonhosted.org/packages/71/09/d44e45d0a4f3a651f23a1e9d42de43bc643cce2971b19e784cc67d823676/fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e", size = 2396589, upload-time = "2025-12-12T17:30:45.681Z" }, - { url = "https://files.pythonhosted.org/packages/89/18/58c64cafcf8eb677a99ef593121f719e6dcbdb7d1c594ae5a10d4997ca8a/fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c", size = 4877892, upload-time = "2025-12-12T17:30:47.709Z" }, - { url = "https://files.pythonhosted.org/packages/8a/ec/9e6b38c7ba1e09eb51db849d5450f4c05b7e78481f662c3b79dbde6f3d04/fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75", size = 4972884, upload-time = "2025-12-12T17:30:49.656Z" }, - { url = "https://files.pythonhosted.org/packages/5e/87/b5339da8e0256734ba0dbbf5b6cdebb1dd79b01dc8c270989b7bcd465541/fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063", size = 4924405, upload-time = "2025-12-12T17:30:51.735Z" }, - { url = "https://files.pythonhosted.org/packages/0b/47/e3409f1e1e69c073a3a6fd8cb886eb18c0bae0ee13db2c8d5e7f8495e8b7/fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2", size = 5035553, upload-time = "2025-12-12T17:30:54.823Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b6/1f6600161b1073a984294c6c031e1a56ebf95b6164249eecf30012bb2e38/fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c", size = 2271915, upload-time = "2025-12-12T17:30:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/52/7b/91e7b01e37cc8eb0e1f770d08305b3655e4f002fc160fb82b3390eabacf5/fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c", size = 2323487, upload-time = "2025-12-12T17:30:59.804Z" }, - { url = "https://files.pythonhosted.org/packages/39/5c/908ad78e46c61c3e3ed70c3b58ff82ab48437faf84ec84f109592cabbd9f/fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa", size = 2929571, upload-time = "2025-12-12T17:31:02.574Z" }, - { url = "https://files.pythonhosted.org/packages/bd/41/975804132c6dea64cdbfbaa59f3518a21c137a10cccf962805b301ac6ab2/fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91", size = 2435317, upload-time = "2025-12-12T17:31:04.974Z" }, - { url = "https://files.pythonhosted.org/packages/b0/5a/aef2a0a8daf1ebaae4cfd83f84186d4a72ee08fd6a8451289fcd03ffa8a4/fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19", size = 4882124, upload-time = "2025-12-12T17:31:07.456Z" }, - { url = "https://files.pythonhosted.org/packages/80/33/d6db3485b645b81cea538c9d1c9219d5805f0877fda18777add4671c5240/fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba", size = 5100391, upload-time = "2025-12-12T17:31:09.732Z" }, - { url = "https://files.pythonhosted.org/packages/6c/d6/675ba631454043c75fcf76f0ca5463eac8eb0666ea1d7badae5fea001155/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7", size = 4978800, upload-time = "2025-12-12T17:31:11.681Z" }, - { url = "https://files.pythonhosted.org/packages/7f/33/d3ec753d547a8d2bdaedd390d4a814e8d5b45a093d558f025c6b990b554c/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118", size = 5006426, upload-time = "2025-12-12T17:31:13.764Z" }, - { url = "https://files.pythonhosted.org/packages/b4/40/cc11f378b561a67bea850ab50063366a0d1dd3f6d0a30ce0f874b0ad5664/fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5", size = 2335377, upload-time = "2025-12-12T17:31:16.49Z" }, - { url = "https://files.pythonhosted.org/packages/e4/ff/c9a2b66b39f8628531ea58b320d66d951267c98c6a38684daa8f50fb02f8/fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b", size = 2400613, upload-time = "2025-12-12T17:31:18.769Z" }, - { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, -] - -[[package]] -name = "frozenlist" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, - { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, - { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, - { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, - { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, - { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, - { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, - { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, - { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, - { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, - { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, - { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, - { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, - { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, - { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, - { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, - { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, - { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, - { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, - { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, - { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, - { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, - { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, - { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, - { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, - { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, - { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, - { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, - { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, - { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, - { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, - { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, - { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, - { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, - { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, - { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, - { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, - { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, - { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, - { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, - { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, - { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, - { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, - { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, - { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, - { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, - { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, - { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, - { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, - { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, - { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, - { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, - { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, - { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, - { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, - { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, - { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, - { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, - { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, - { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, - { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, - { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, - { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, - { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, - { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, - { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, - { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, - { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, - { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, - { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, - { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, - { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, - { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, -] - -[[package]] -name = "greenlet" -version = "3.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/cb/48e964c452ca2b92175a9b2dca037a553036cb053ba69e284650ce755f13/greenlet-3.3.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e29f3018580e8412d6aaf5641bb7745d38c85228dacf51a73bd4e26ddf2a6a8e", size = 274908, upload-time = "2025-12-04T14:23:26.435Z" }, - { url = "https://files.pythonhosted.org/packages/28/da/38d7bff4d0277b594ec557f479d65272a893f1f2a716cad91efeb8680953/greenlet-3.3.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a687205fb22794e838f947e2194c0566d3812966b41c78709554aa883183fb62", size = 577113, upload-time = "2025-12-04T14:50:05.493Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f2/89c5eb0faddc3ff014f1c04467d67dee0d1d334ab81fadbf3744847f8a8a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4243050a88ba61842186cb9e63c7dfa677ec146160b0efd73b855a3d9c7fcf32", size = 590338, upload-time = "2025-12-04T14:57:41.136Z" }, - { url = "https://files.pythonhosted.org/packages/80/d7/db0a5085035d05134f8c089643da2b44cc9b80647c39e93129c5ef170d8f/greenlet-3.3.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:670d0f94cd302d81796e37299bcd04b95d62403883b24225c6b5271466612f45", size = 601098, upload-time = "2025-12-04T15:07:11.898Z" }, - { url = "https://files.pythonhosted.org/packages/dc/a6/e959a127b630a58e23529972dbc868c107f9d583b5a9f878fb858c46bc1a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb3a8ec3db4a3b0eb8a3c25436c2d49e3505821802074969db017b87bc6a948", size = 590206, upload-time = "2025-12-04T14:26:01.254Z" }, - { url = "https://files.pythonhosted.org/packages/48/60/29035719feb91798693023608447283b266b12efc576ed013dd9442364bb/greenlet-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2de5a0b09eab81fc6a382791b995b1ccf2b172a9fec934747a7a23d2ff291794", size = 1550668, upload-time = "2025-12-04T15:04:22.439Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5f/783a23754b691bfa86bd72c3033aa107490deac9b2ef190837b860996c9f/greenlet-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4449a736606bd30f27f8e1ff4678ee193bc47f6ca810d705981cfffd6ce0d8c5", size = 1615483, upload-time = "2025-12-04T14:27:28.083Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d5/c339b3b4bc8198b7caa4f2bd9fd685ac9f29795816d8db112da3d04175bb/greenlet-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:7652ee180d16d447a683c04e4c5f6441bae7ba7b17ffd9f6b3aff4605e9e6f71", size = 301164, upload-time = "2025-12-04T14:42:51.577Z" }, - { url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" }, - { url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" }, - { url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" }, - { url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" }, - { url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" }, - { url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" }, - { url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" }, - { url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964, upload-time = "2025-12-04T14:36:58.316Z" }, - { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, - { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, - { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" }, - { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, - { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, - { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, - { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" }, - { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, - { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, - { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, - { url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" }, - { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, - { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" }, - { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, - { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, - { url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" }, - { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, - { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, - { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, -] - -[[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/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, - { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, - { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, - { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, - { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, - { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, - { 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.optional-dependencies] -socks = [ - { name = "socksio" }, -] - -[[package]] -name = "identify" -version = "2.6.16" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, -] - -[[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 = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, -] - -[[package]] -name = "jiter" -version = "0.12.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" }, - { url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" }, - { url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" }, - { url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966, upload-time = "2025-11-09T20:47:06.508Z" }, - { url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047, upload-time = "2025-11-09T20:47:08.382Z" }, - { url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835, upload-time = "2025-11-09T20:47:09.81Z" }, - { url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587, upload-time = "2025-11-09T20:47:11.529Z" }, - { url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492, upload-time = "2025-11-09T20:47:12.993Z" }, - { url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046, upload-time = "2025-11-09T20:47:14.6Z" }, - { url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392, upload-time = "2025-11-09T20:47:16.011Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096, upload-time = "2025-11-09T20:47:17.344Z" }, - { url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899, upload-time = "2025-11-09T20:47:19.365Z" }, - { url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070, upload-time = "2025-11-09T20:47:20.75Z" }, - { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" }, - { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" }, - { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" }, - { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" }, - { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" }, - { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" }, - { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" }, - { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" }, - { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" }, - { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" }, - { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" }, - { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" }, - { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" }, - { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" }, - { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" }, - { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" }, - { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" }, - { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" }, - { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" }, - { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" }, - { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" }, - { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" }, - { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" }, - { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" }, - { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" }, - { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" }, - { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" }, - { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" }, - { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" }, - { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" }, - { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" }, - { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" }, - { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" }, - { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" }, - { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" }, - { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" }, - { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" }, - { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" }, - { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" }, - { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" }, - { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" }, - { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" }, - { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" }, - { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" }, - { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" }, - { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" }, - { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" }, - { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" }, - { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" }, - { url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" }, - { url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" }, - { url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" }, - { url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212, upload-time = "2025-11-09T20:49:15.643Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" }, - { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" }, - { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, -] - -[[package]] -name = "jsonpatch" -version = "1.33" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jsonpointer" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, -] - -[[package]] -name = "jsonpointer" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, -] - -[[package]] -name = "kiwisolver" -version = "1.4.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, - { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, - { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, - { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, - { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, - { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, - { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, - { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, - { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, - { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, - { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, - { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, - { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, - { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, - { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, - { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, - { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, - { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, - { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, - { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, - { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, - { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, - { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, - { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, - { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, - { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, - { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, - { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, - { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, - { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, - { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, - { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, - { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, - { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, - { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, - { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, - { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, - { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, - { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, - { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, - { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, - { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, - { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, - { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" }, - { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" }, - { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" }, - { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" }, - { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" }, - { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" }, - { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" }, - { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" }, - { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" }, - { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" }, - { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" }, - { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" }, - { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" }, - { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" }, - { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" }, - { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" }, - { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" }, - { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" }, - { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" }, - { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" }, - { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" }, - { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, - { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, - { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, - { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, - { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, - { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, - { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, -] - -[[package]] -name = "langchain-anthropic" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anthropic" }, - { name = "langchain-core" }, - { name = "pydantic" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0d/b6/ac5ee84e15bf79844c9c791f99a614c7ec7e1a63c2947e55977be01a81b4/langchain_anthropic-1.3.1.tar.gz", hash = "sha256:4f3d7a4a7729ab1aeaf62d32c87d4d227c1b5421668ca9e3734562b383470b07", size = 708940, upload-time = "2026-01-05T21:07:19.345Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/4f/7a5b32764addf4b757545b89899b9d76688176f19e4ee89868e3b8bbfd0f/langchain_anthropic-1.3.1-py3-none-any.whl", hash = "sha256:1fc28cf8037c30597ee6172fc2ff9e345efe8149a8c2a39897b1eebba2948322", size = 46328, upload-time = "2026-01-05T21:07:18.261Z" }, -] - -[[package]] -name = "langchain-core" -version = "1.2.11" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jsonpatch" }, - { name = "langsmith" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "pyyaml" }, - { name = "tenacity" }, - { name = "typing-extensions" }, - { name = "uuid-utils" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/12/17/1943cedfc118e04b8128e4c3e1dbf0fa0ea58eefddbb6198cfd699d19f01/langchain_core-1.2.11.tar.gz", hash = "sha256:f164bb36602dd74a3a50c1334fca75309ad5ed95767acdfdbb9fa95ce28a1e01", size = 831211, upload-time = "2026-02-10T20:35:28.35Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/30/1f80e3fc674353cad975ed5294353d42512535d2094ef032c06454c2c873/langchain_core-1.2.11-py3-none-any.whl", hash = "sha256:ae11ceb8dda60d0b9d09e763116e592f1683327c17be5b715f350fd29aee65d3", size = 500062, upload-time = "2026-02-10T20:35:26.698Z" }, -] - -[[package]] -name = "langchain-openai" -version = "1.1.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "langchain-core" }, - { name = "openai" }, - { name = "tiktoken" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/b7/30bfc4d1b658a9ee524bcce3b0b2ec9c45a11c853a13c4f0c9da9882784b/langchain_openai-1.1.7.tar.gz", hash = "sha256:f5ec31961ed24777548b63a5fe313548bc6e0eb9730d6552b8c6418765254c81", size = 1039134, upload-time = "2026-01-07T19:44:59.728Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/a1/50e7596aca775d8c3883eceeaf47489fac26c57c1abe243c00174f715a8a/langchain_openai-1.1.7-py3-none-any.whl", hash = "sha256:34e9cd686aac1a120d6472804422792bf8080a2103b5d21ee450c9e42d053815", size = 84753, upload-time = "2026-01-07T19:44:58.629Z" }, -] - -[[package]] -name = "langgraph" -version = "1.0.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "langchain-core" }, - { name = "langgraph-checkpoint" }, - { name = "langgraph-prebuilt" }, - { name = "langgraph-sdk" }, - { name = "pydantic" }, - { name = "xxhash" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/5b/f72655717c04e33d3b62f21b166dc063d192b53980e9e3be0e2a117f1c9f/langgraph-1.0.7.tar.gz", hash = "sha256:0cfdfee51e6e8cfe503ecc7367c73933437c505b03fa10a85c710975c8182d9a", size = 497098, upload-time = "2026-01-22T16:57:47.303Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/0e/fe80144e3e4048e5d19ccdb91ac547c1a7dc3da8dbd1443e210048194c14/langgraph-1.0.7-py3-none-any.whl", hash = "sha256:9d68e8f8dd8f3de2fec45f9a06de05766d9b075b78fb03171779893b7a52c4d2", size = 157353, upload-time = "2026-01-22T16:57:45.997Z" }, -] - -[[package]] -name = "langgraph-checkpoint" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "langchain-core" }, - { name = "ormsgpack" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/98/76/55a18c59dedf39688d72c4b06af73a5e3ea0d1a01bc867b88fbf0659f203/langgraph_checkpoint-4.0.0.tar.gz", hash = "sha256:814d1bd050fac029476558d8e68d87bce9009a0262d04a2c14b918255954a624", size = 137320, upload-time = "2026-01-12T20:30:26.38Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/de/ddd53b7032e623f3c7bcdab2b44e8bf635e468f62e10e5ff1946f62c9356/langgraph_checkpoint-4.0.0-py3-none-any.whl", hash = "sha256:3fa9b2635a7c5ac28b338f631abf6a030c3b508b7b9ce17c22611513b589c784", size = 46329, upload-time = "2026-01-12T20:30:25.2Z" }, -] - -[[package]] -name = "langgraph-prebuilt" -version = "1.0.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "langchain-core" }, - { name = "langgraph-checkpoint" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a7/59/711aecd1a50999456850dc328f3cad72b4372d8218838d8d5326f80cb76f/langgraph_prebuilt-1.0.7.tar.gz", hash = "sha256:38e097e06de810de4d0e028ffc0e432bb56d1fb417620fb1dfdc76c5e03e4bf9", size = 163692, upload-time = "2026-01-22T16:45:22.801Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/49/5e37abb3f38a17a3487634abc2a5da87c208cc1d14577eb8d7184b25c886/langgraph_prebuilt-1.0.7-py3-none-any.whl", hash = "sha256:e14923516504405bb5edc3977085bc9622c35476b50c1808544490e13871fe7c", size = 35324, upload-time = "2026-01-22T16:45:21.784Z" }, -] - -[[package]] -name = "langgraph-sdk" -version = "0.3.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "orjson" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c3/0f/ed0634c222eed48a31ba48eab6881f94ad690d65e44fe7ca838240a260c1/langgraph_sdk-0.3.3.tar.gz", hash = "sha256:c34c3dce3b6848755eb61f0c94369d1ba04aceeb1b76015db1ea7362c544fb26", size = 130589, upload-time = "2026-01-13T00:30:43.894Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/be/4ad511bacfdd854afb12974f407cb30010dceb982dc20c55491867b34526/langgraph_sdk-0.3.3-py3-none-any.whl", hash = "sha256:a52ebaf09d91143e55378bb2d0b033ed98f57f48c9ad35c8f81493b88705fc7b", size = 67021, upload-time = "2026-01-13T00:30:42.264Z" }, -] - -[[package]] -name = "langsmith" -version = "0.6.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "requests" }, - { name = "requests-toolbelt" }, - { name = "uuid-utils" }, - { name = "zstandard" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e7/85/9c7933052a997da1b85bc5c774f3865e9b1da1c8d71541ea133178b13229/langsmith-0.6.4.tar.gz", hash = "sha256:36f7223a01c218079fbb17da5e536ebbaf5c1468c028abe070aa3ae59bc99ec8", size = 919964, upload-time = "2026-01-15T20:02:28.873Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/66/0f/09a6637a7ba777eb307b7c80852d9ee26438e2bdafbad6fcc849ff9d9192/langsmith-0.6.4-py3-none-any.whl", hash = "sha256:ac4835860160be371042c7adbba3cb267bcf8d96a5ea976c33a8a4acad6c5486", size = 283503, upload-time = "2026-01-15T20:02:26.662Z" }, -] - -[[package]] -name = "librt" -version = "0.7.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" }, - { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" }, - { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" }, - { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" }, - { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" }, - { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" }, - { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" }, - { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" }, - { url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803, upload-time = "2026-01-14T12:55:04.996Z" }, - { url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080, upload-time = "2026-01-14T12:55:06.489Z" }, - { url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383, upload-time = "2026-01-14T12:55:07.49Z" }, - { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" }, - { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" }, - { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" }, - { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" }, - { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" }, - { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" }, - { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" }, - { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" }, - { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" }, - { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" }, - { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" }, - { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" }, - { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" }, - { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" }, - { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" }, - { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" }, - { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" }, - { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" }, - { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" }, - { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" }, - { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" }, - { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" }, - { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" }, - { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" }, - { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" }, - { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" }, - { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" }, - { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" }, - { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" }, - { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" }, - { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" }, - { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" }, - { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" }, - { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" }, - { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" }, - { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" }, - { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" }, - { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" }, - { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" }, -] - -[[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/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, - { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, - { 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 = "matplotlib" -version = "3.10.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "contourpy" }, - { name = "cycler" }, - { name = "fonttools" }, - { name = "kiwisolver" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "pillow" }, - { name = "pyparsing" }, - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, - { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, - { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, - { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, - { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, - { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, - { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, - { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, - { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, - { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, - { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, - { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, - { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, - { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, - { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, - { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, - { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, - { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, - { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, - { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, - { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, - { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, - { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, - { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, - { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, - { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, - { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, - { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, - { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, - { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, - { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, - { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, - { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, - { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, - { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, -] - -[[package]] -name = "mediapipe" -version = "0.10.32" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "absl-py" }, - { name = "flatbuffers" }, - { name = "matplotlib" }, - { name = "numpy" }, - { name = "opencv-contrib-python" }, - { name = "sounddevice" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/24/d9/ca1be234911c76ccd87a9e0d8210394153cb6ad9b5da65a231aff8769d6f/mediapipe-0.10.32-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b62178b7585e0bb8789075c43bbb3e352fbc4a8f765797fded509f86a098b29b", size = 19386286, upload-time = "2026-01-22T16:15:01.042Z" }, - { url = "https://files.pythonhosted.org/packages/e3/98/00cd8b2dcb563f2298655633e6611a791b2c1a7df1dae064b2b96084f1bf/mediapipe-0.10.32-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:4b0941fbbbce41862f13cb1850c4878c13dbc62cd5e81e74880051b7a20ce3b6", size = 10331072, upload-time = "2026-01-22T16:15:04.747Z" }, - { url = "https://files.pythonhosted.org/packages/fe/63/c9172142521fc334fa891be9ef6259a3790502041b395127d63755cb3f0b/mediapipe-0.10.32-py3-none-win_amd64.whl", hash = "sha256:438d98977a4e1b0f88070ad16537a328912f847ce9debf236da047b0b483cbfd", size = 10211344, upload-time = "2026-01-22T16:15:11.716Z" }, -] - -[[package]] -name = "momshell-backend" -version = "0.5.3" -source = { editable = "." } -dependencies = [ - { name = "aiosqlite" }, - { name = "bcrypt" }, - { name = "edge-tts" }, - { name = "email-validator" }, - { name = "fastapi" }, - { name = "greenlet" }, - { name = "httpx", extra = ["socks"] }, - { name = "jinja2" }, - { name = "langchain-anthropic" }, - { name = "langchain-core" }, - { name = "langchain-openai" }, - { name = "langgraph" }, - { name = "mediapipe" }, - { name = "numpy" }, - { name = "openai" }, - { name = "opencv-python" }, - { name = "passlib" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "python-dotenv" }, - { name = "python-jose", extra = ["cryptography"] }, - { name = "python-multipart" }, - { name = "sqlalchemy" }, - { name = "uvicorn", extra = ["standard"] }, - { name = "websockets" }, -] - -[package.dev-dependencies] -dev = [ - { name = "mypy" }, - { name = "pre-commit" }, - { name = "pytest" }, - { name = "ruff" }, -] - -[package.metadata] -requires-dist = [ - { name = "aiosqlite", specifier = ">=0.20.0" }, - { name = "bcrypt", specifier = ">=4.0.0,<5.1.0" }, - { name = "edge-tts", specifier = ">=6.1.0" }, - { name = "email-validator", specifier = ">=2.0.0" }, - { name = "fastapi", specifier = ">=0.115.0" }, - { name = "greenlet", specifier = ">=3.3.0" }, - { name = "httpx", extras = ["socks"], specifier = ">=0.28.1" }, - { name = "jinja2", specifier = ">=3.1.0" }, - { name = "langchain-anthropic", specifier = ">=0.3.0" }, - { name = "langchain-core", specifier = ">=0.3.0" }, - { name = "langchain-openai", specifier = ">=1.1.7" }, - { name = "langgraph", specifier = ">=0.2.0" }, - { name = "mediapipe", specifier = ">=0.10.0" }, - { name = "numpy", specifier = ">=1.24.0" }, - { name = "openai", specifier = ">=1.0.0" }, - { name = "opencv-python", specifier = ">=4.8.0" }, - { name = "passlib", specifier = ">=1.7.4" }, - { name = "pydantic", specifier = ">=2.0.0" }, - { name = "pydantic-settings", specifier = ">=2.0.0" }, - { name = "python-dotenv", specifier = ">=1.0.0" }, - { name = "python-jose", extras = ["cryptography"], specifier = ">=3.3.0" }, - { name = "python-multipart", specifier = ">=0.0.9" }, - { name = "sqlalchemy", specifier = ">=2.0.0" }, - { name = "uvicorn", extras = ["standard"], specifier = ">=0.30.0" }, - { name = "websockets", specifier = ">=12.0" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "mypy", specifier = ">=1.8.0" }, - { name = "pre-commit", specifier = ">=3.6.0" }, - { name = "pytest", specifier = ">=9.0.2" }, - { name = "ruff", specifier = ">=0.3.0" }, -] - -[[package]] -name = "multidict" -version = "6.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604, upload-time = "2025-10-06T14:48:54.277Z" }, - { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715, upload-time = "2025-10-06T14:48:55.445Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332, upload-time = "2025-10-06T14:48:56.706Z" }, - { url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212, upload-time = "2025-10-06T14:48:58.042Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671, upload-time = "2025-10-06T14:49:00.004Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491, upload-time = "2025-10-06T14:49:01.393Z" }, - { url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322, upload-time = "2025-10-06T14:49:02.745Z" }, - { url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694, upload-time = "2025-10-06T14:49:04.15Z" }, - { url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715, upload-time = "2025-10-06T14:49:05.967Z" }, - { url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189, upload-time = "2025-10-06T14:49:07.37Z" }, - { url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845, upload-time = "2025-10-06T14:49:08.759Z" }, - { url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374, upload-time = "2025-10-06T14:49:10.574Z" }, - { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345, upload-time = "2025-10-06T14:49:12.331Z" }, - { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940, upload-time = "2025-10-06T14:49:13.821Z" }, - { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229, upload-time = "2025-10-06T14:49:15.603Z" }, - { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308, upload-time = "2025-10-06T14:49:16.871Z" }, - { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037, upload-time = "2025-10-06T14:49:18.457Z" }, - { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023, upload-time = "2025-10-06T14:49:19.648Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" }, - { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" }, - { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" }, - { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" }, - { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" }, - { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" }, - { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" }, - { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" }, - { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" }, - { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" }, - { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" }, - { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" }, - { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" }, - { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" }, - { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" }, - { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" }, - { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" }, - { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" }, - { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" }, - { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" }, - { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" }, - { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" }, - { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" }, - { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" }, - { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" }, - { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" }, - { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" }, - { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" }, - { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" }, - { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" }, - { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" }, - { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" }, - { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" }, - { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" }, - { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" }, - { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" }, - { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" }, - { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" }, - { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" }, - { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" }, - { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" }, - { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" }, - { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" }, - { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" }, - { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" }, - { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" }, - { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, - { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, - { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" }, - { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" }, - { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" }, - { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084, upload-time = "2025-10-06T14:50:56.369Z" }, - { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667, upload-time = "2025-10-06T14:50:57.991Z" }, - { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590, upload-time = "2025-10-06T14:50:59.589Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112, upload-time = "2025-10-06T14:51:01.183Z" }, - { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194, upload-time = "2025-10-06T14:51:02.794Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510, upload-time = "2025-10-06T14:51:04.724Z" }, - { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395, upload-time = "2025-10-06T14:51:06.306Z" }, - { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520, upload-time = "2025-10-06T14:51:08.091Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479, upload-time = "2025-10-06T14:51:10.365Z" }, - { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" }, - { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" }, - { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" }, - { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" }, - { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" }, - { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" }, - { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" }, - { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" }, - { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324, upload-time = "2025-10-06T14:51:25.822Z" }, - { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" }, - { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" }, - { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" }, - { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" }, - { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" }, - { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" }, - { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" }, - { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272, upload-time = "2025-10-06T14:51:45.265Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" }, - { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" }, - { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" }, - { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, -] - -[[package]] -name = "mypy" -version = "1.19.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, - { name = "mypy-extensions" }, - { name = "pathspec" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, - { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, - { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, - { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, - { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, - { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, - { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, - { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, - { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, - { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, - { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, - { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, - { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, - { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, - { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, - { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, - { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, - { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, - { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, - { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, -] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, -] - -[[package]] -name = "nodeenv" -version = "1.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, -] - -[[package]] -name = "numpy" -version = "2.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/34/2b1bc18424f3ad9af577f6ce23600319968a70575bd7db31ce66731bbef9/numpy-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5", size = 16944563, upload-time = "2026-01-10T06:42:14.615Z" }, - { url = "https://files.pythonhosted.org/packages/2c/57/26e5f97d075aef3794045a6ca9eada6a4ed70eb9a40e7a4a93f9ac80d704/numpy-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425", size = 12645658, upload-time = "2026-01-10T06:42:17.298Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ba/80fc0b1e3cb2fd5c6143f00f42eb67762aa043eaa05ca924ecc3222a7849/numpy-2.4.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba", size = 5474132, upload-time = "2026-01-10T06:42:19.637Z" }, - { url = "https://files.pythonhosted.org/packages/40/ae/0a5b9a397f0e865ec171187c78d9b57e5588afc439a04ba9cab1ebb2c945/numpy-2.4.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501", size = 6804159, upload-time = "2026-01-10T06:42:21.44Z" }, - { url = "https://files.pythonhosted.org/packages/86/9c/841c15e691c7085caa6fd162f063eff494099c8327aeccd509d1ab1e36ab/numpy-2.4.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a", size = 14708058, upload-time = "2026-01-10T06:42:23.546Z" }, - { url = "https://files.pythonhosted.org/packages/5d/9d/7862db06743f489e6a502a3b93136d73aea27d97b2cf91504f70a27501d6/numpy-2.4.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509", size = 16651501, upload-time = "2026-01-10T06:42:25.909Z" }, - { url = "https://files.pythonhosted.org/packages/a6/9c/6fc34ebcbd4015c6e5f0c0ce38264010ce8a546cb6beacb457b84a75dfc8/numpy-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc", size = 16492627, upload-time = "2026-01-10T06:42:28.938Z" }, - { url = "https://files.pythonhosted.org/packages/aa/63/2494a8597502dacda439f61b3c0db4da59928150e62be0e99395c3ad23c5/numpy-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82", size = 18585052, upload-time = "2026-01-10T06:42:31.312Z" }, - { url = "https://files.pythonhosted.org/packages/6a/93/098e1162ae7522fc9b618d6272b77404c4656c72432ecee3abc029aa3de0/numpy-2.4.1-cp311-cp311-win32.whl", hash = "sha256:0f118ce6b972080ba0758c6087c3617b5ba243d806268623dc34216d69099ba0", size = 6236575, upload-time = "2026-01-10T06:42:33.872Z" }, - { url = "https://files.pythonhosted.org/packages/8c/de/f5e79650d23d9e12f38a7bc6b03ea0835b9575494f8ec94c11c6e773b1b1/numpy-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:18e14c4d09d55eef39a6ab5b08406e84bc6869c1e34eef45564804f90b7e0574", size = 12604479, upload-time = "2026-01-10T06:42:35.778Z" }, - { url = "https://files.pythonhosted.org/packages/dd/65/e1097a7047cff12ce3369bd003811516b20ba1078dbdec135e1cd7c16c56/numpy-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:6461de5113088b399d655d45c3897fa188766415d0f568f175ab071c8873bd73", size = 10578325, upload-time = "2026-01-10T06:42:38.518Z" }, - { url = "https://files.pythonhosted.org/packages/78/7f/ec53e32bf10c813604edf07a3682616bd931d026fcde7b6d13195dfb684a/numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2", size = 16656888, upload-time = "2026-01-10T06:42:40.913Z" }, - { url = "https://files.pythonhosted.org/packages/b8/e0/1f9585d7dae8f14864e948fd7fa86c6cb72dee2676ca2748e63b1c5acfe0/numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8", size = 12373956, upload-time = "2026-01-10T06:42:43.091Z" }, - { url = "https://files.pythonhosted.org/packages/8e/43/9762e88909ff2326f5e7536fa8cb3c49fb03a7d92705f23e6e7f553d9cb3/numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a", size = 5202567, upload-time = "2026-01-10T06:42:45.107Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ee/34b7930eb61e79feb4478800a4b95b46566969d837546aa7c034c742ef98/numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0", size = 6549459, upload-time = "2026-01-10T06:42:48.152Z" }, - { url = "https://files.pythonhosted.org/packages/79/e3/5f115fae982565771be994867c89bcd8d7208dbfe9469185497d70de5ddf/numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c", size = 14404859, upload-time = "2026-01-10T06:42:49.947Z" }, - { url = "https://files.pythonhosted.org/packages/d9/7d/9c8a781c88933725445a859cac5d01b5871588a15969ee6aeb618ba99eee/numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02", size = 16371419, upload-time = "2026-01-10T06:42:52.409Z" }, - { url = "https://files.pythonhosted.org/packages/a6/d2/8aa084818554543f17cf4162c42f162acbd3bb42688aefdba6628a859f77/numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162", size = 16182131, upload-time = "2026-01-10T06:42:54.694Z" }, - { url = "https://files.pythonhosted.org/packages/60/db/0425216684297c58a8df35f3284ef56ec4a043e6d283f8a59c53562caf1b/numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9", size = 18295342, upload-time = "2026-01-10T06:42:56.991Z" }, - { url = "https://files.pythonhosted.org/packages/31/4c/14cb9d86240bd8c386c881bafbe43f001284b7cce3bc01623ac9475da163/numpy-2.4.1-cp312-cp312-win32.whl", hash = "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f", size = 5959015, upload-time = "2026-01-10T06:42:59.631Z" }, - { url = "https://files.pythonhosted.org/packages/51/cf/52a703dbeb0c65807540d29699fef5fda073434ff61846a564d5c296420f/numpy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87", size = 12310730, upload-time = "2026-01-10T06:43:01.627Z" }, - { url = "https://files.pythonhosted.org/packages/69/80/a828b2d0ade5e74a9fe0f4e0a17c30fdc26232ad2bc8c9f8b3197cf7cf18/numpy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8", size = 10312166, upload-time = "2026-01-10T06:43:03.673Z" }, - { url = "https://files.pythonhosted.org/packages/04/68/732d4b7811c00775f3bd522a21e8dd5a23f77eb11acdeb663e4a4ebf0ef4/numpy-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b", size = 16652495, upload-time = "2026-01-10T06:43:06.283Z" }, - { url = "https://files.pythonhosted.org/packages/20/ca/857722353421a27f1465652b2c66813eeeccea9d76d5f7b74b99f298e60e/numpy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f", size = 12368657, upload-time = "2026-01-10T06:43:09.094Z" }, - { url = "https://files.pythonhosted.org/packages/81/0d/2377c917513449cc6240031a79d30eb9a163d32a91e79e0da47c43f2c0c8/numpy-2.4.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9", size = 5197256, upload-time = "2026-01-10T06:43:13.634Z" }, - { url = "https://files.pythonhosted.org/packages/17/39/569452228de3f5de9064ac75137082c6214be1f5c532016549a7923ab4b5/numpy-2.4.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e", size = 6545212, upload-time = "2026-01-10T06:43:15.661Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a4/77333f4d1e4dac4395385482557aeecf4826e6ff517e32ca48e1dafbe42a/numpy-2.4.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5", size = 14402871, upload-time = "2026-01-10T06:43:17.324Z" }, - { url = "https://files.pythonhosted.org/packages/ba/87/d341e519956273b39d8d47969dd1eaa1af740615394fe67d06f1efa68773/numpy-2.4.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8", size = 16359305, upload-time = "2026-01-10T06:43:19.376Z" }, - { url = "https://files.pythonhosted.org/packages/32/91/789132c6666288eaa20ae8066bb99eba1939362e8f1a534949a215246e97/numpy-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c", size = 16181909, upload-time = "2026-01-10T06:43:21.808Z" }, - { url = "https://files.pythonhosted.org/packages/cf/b8/090b8bd27b82a844bb22ff8fdf7935cb1980b48d6e439ae116f53cdc2143/numpy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2", size = 18284380, upload-time = "2026-01-10T06:43:23.957Z" }, - { url = "https://files.pythonhosted.org/packages/67/78/722b62bd31842ff029412271556a1a27a98f45359dea78b1548a3a9996aa/numpy-2.4.1-cp313-cp313-win32.whl", hash = "sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d", size = 5957089, upload-time = "2026-01-10T06:43:27.535Z" }, - { url = "https://files.pythonhosted.org/packages/da/a6/cf32198b0b6e18d4fbfa9a21a992a7fca535b9bb2b0cdd217d4a3445b5ca/numpy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb", size = 12307230, upload-time = "2026-01-10T06:43:29.298Z" }, - { url = "https://files.pythonhosted.org/packages/44/6c/534d692bfb7d0afe30611320c5fb713659dcb5104d7cc182aff2aea092f5/numpy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5", size = 10313125, upload-time = "2026-01-10T06:43:31.782Z" }, - { url = "https://files.pythonhosted.org/packages/da/a1/354583ac5c4caa566de6ddfbc42744409b515039e085fab6e0ff942e0df5/numpy-2.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7", size = 12496156, upload-time = "2026-01-10T06:43:34.237Z" }, - { url = "https://files.pythonhosted.org/packages/51/b0/42807c6e8cce58c00127b1dc24d365305189991f2a7917aa694a109c8d7d/numpy-2.4.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d", size = 5324663, upload-time = "2026-01-10T06:43:36.211Z" }, - { url = "https://files.pythonhosted.org/packages/fe/55/7a621694010d92375ed82f312b2f28017694ed784775269115323e37f5e2/numpy-2.4.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15", size = 6645224, upload-time = "2026-01-10T06:43:37.884Z" }, - { url = "https://files.pythonhosted.org/packages/50/96/9fa8635ed9d7c847d87e30c834f7109fac5e88549d79ef3324ab5c20919f/numpy-2.4.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9", size = 14462352, upload-time = "2026-01-10T06:43:39.479Z" }, - { url = "https://files.pythonhosted.org/packages/03/d1/8cf62d8bb2062da4fb82dd5d49e47c923f9c0738032f054e0a75342faba7/numpy-2.4.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2", size = 16407279, upload-time = "2026-01-10T06:43:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/86/1c/95c86e17c6b0b31ce6ef219da00f71113b220bcb14938c8d9a05cee0ff53/numpy-2.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505", size = 16248316, upload-time = "2026-01-10T06:43:44.121Z" }, - { url = "https://files.pythonhosted.org/packages/30/b4/e7f5ff8697274c9d0fa82398b6a372a27e5cef069b37df6355ccb1f1db1a/numpy-2.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2", size = 18329884, upload-time = "2026-01-10T06:43:46.613Z" }, - { url = "https://files.pythonhosted.org/packages/37/a4/b073f3e9d77f9aec8debe8ca7f9f6a09e888ad1ba7488f0c3b36a94c03ac/numpy-2.4.1-cp313-cp313t-win32.whl", hash = "sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4", size = 6081138, upload-time = "2026-01-10T06:43:48.854Z" }, - { url = "https://files.pythonhosted.org/packages/16/16/af42337b53844e67752a092481ab869c0523bc95c4e5c98e4dac4e9581ac/numpy-2.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510", size = 12447478, upload-time = "2026-01-10T06:43:50.476Z" }, - { url = "https://files.pythonhosted.org/packages/6c/f8/fa85b2eac68ec631d0b631abc448552cb17d39afd17ec53dcbcc3537681a/numpy-2.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261", size = 10382981, upload-time = "2026-01-10T06:43:52.575Z" }, - { url = "https://files.pythonhosted.org/packages/1b/a7/ef08d25698e0e4b4efbad8d55251d20fe2a15f6d9aa7c9b30cd03c165e6f/numpy-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc", size = 16652046, upload-time = "2026-01-10T06:43:54.797Z" }, - { url = "https://files.pythonhosted.org/packages/8f/39/e378b3e3ca13477e5ac70293ec027c438d1927f18637e396fe90b1addd72/numpy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3", size = 12378858, upload-time = "2026-01-10T06:43:57.099Z" }, - { url = "https://files.pythonhosted.org/packages/c3/74/7ec6154f0006910ed1fdbb7591cf4432307033102b8a22041599935f8969/numpy-2.4.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220", size = 5207417, upload-time = "2026-01-10T06:43:59.037Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b7/053ac11820d84e42f8feea5cb81cc4fcd1091499b45b1ed8c7415b1bf831/numpy-2.4.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee", size = 6542643, upload-time = "2026-01-10T06:44:01.852Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c4/2e7908915c0e32ca636b92e4e4a3bdec4cb1e7eb0f8aedf1ed3c68a0d8cd/numpy-2.4.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556", size = 14418963, upload-time = "2026-01-10T06:44:04.047Z" }, - { url = "https://files.pythonhosted.org/packages/eb/c0/3ed5083d94e7ffd7c404e54619c088e11f2e1939a9544f5397f4adb1b8ba/numpy-2.4.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844", size = 16363811, upload-time = "2026-01-10T06:44:06.207Z" }, - { url = "https://files.pythonhosted.org/packages/0e/68/42b66f1852bf525050a67315a4fb94586ab7e9eaa541b1bef530fab0c5dd/numpy-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3", size = 16197643, upload-time = "2026-01-10T06:44:08.33Z" }, - { url = "https://files.pythonhosted.org/packages/d2/40/e8714fc933d85f82c6bfc7b998a0649ad9769a32f3494ba86598aaf18a48/numpy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205", size = 18289601, upload-time = "2026-01-10T06:44:10.841Z" }, - { url = "https://files.pythonhosted.org/packages/80/9a/0d44b468cad50315127e884802351723daca7cf1c98d102929468c81d439/numpy-2.4.1-cp314-cp314-win32.whl", hash = "sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745", size = 6005722, upload-time = "2026-01-10T06:44:13.332Z" }, - { url = "https://files.pythonhosted.org/packages/7e/bb/c6513edcce5a831810e2dddc0d3452ce84d208af92405a0c2e58fd8e7881/numpy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d", size = 12438590, upload-time = "2026-01-10T06:44:15.006Z" }, - { url = "https://files.pythonhosted.org/packages/e9/da/a598d5cb260780cf4d255102deba35c1d072dc028c4547832f45dd3323a8/numpy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df", size = 10596180, upload-time = "2026-01-10T06:44:17.386Z" }, - { url = "https://files.pythonhosted.org/packages/de/bc/ea3f2c96fcb382311827231f911723aeff596364eb6e1b6d1d91128aa29b/numpy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f", size = 12498774, upload-time = "2026-01-10T06:44:19.467Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ab/ef9d939fe4a812648c7a712610b2ca6140b0853c5efea361301006c02ae5/numpy-2.4.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0", size = 5327274, upload-time = "2026-01-10T06:44:23.189Z" }, - { url = "https://files.pythonhosted.org/packages/bd/31/d381368e2a95c3b08b8cf7faac6004849e960f4a042d920337f71cef0cae/numpy-2.4.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c", size = 6648306, upload-time = "2026-01-10T06:44:25.012Z" }, - { url = "https://files.pythonhosted.org/packages/c8/e5/0989b44ade47430be6323d05c23207636d67d7362a1796ccbccac6773dd2/numpy-2.4.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93", size = 14464653, upload-time = "2026-01-10T06:44:26.706Z" }, - { url = "https://files.pythonhosted.org/packages/10/a7/cfbe475c35371cae1358e61f20c5f075badc18c4797ab4354140e1d283cf/numpy-2.4.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42", size = 16405144, upload-time = "2026-01-10T06:44:29.378Z" }, - { url = "https://files.pythonhosted.org/packages/f8/a3/0c63fe66b534888fa5177cc7cef061541064dbe2b4b60dcc60ffaf0d2157/numpy-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01", size = 16247425, upload-time = "2026-01-10T06:44:31.721Z" }, - { url = "https://files.pythonhosted.org/packages/6b/2b/55d980cfa2c93bd40ff4c290bf824d792bd41d2fe3487b07707559071760/numpy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b", size = 18330053, upload-time = "2026-01-10T06:44:34.617Z" }, - { url = "https://files.pythonhosted.org/packages/23/12/8b5fc6b9c487a09a7957188e0943c9ff08432c65e34567cabc1623b03a51/numpy-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a", size = 6152482, upload-time = "2026-01-10T06:44:36.798Z" }, - { url = "https://files.pythonhosted.org/packages/00/a5/9f8ca5856b8940492fc24fbe13c1bc34d65ddf4079097cf9e53164d094e1/numpy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2", size = 12627117, upload-time = "2026-01-10T06:44:38.828Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0d/eca3d962f9eef265f01a8e0d20085c6dd1f443cbffc11b6dede81fd82356/numpy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295", size = 10667121, upload-time = "2026-01-10T06:44:41.644Z" }, - { url = "https://files.pythonhosted.org/packages/1e/48/d86f97919e79314a1cdee4c832178763e6e98e623e123d0bada19e92c15a/numpy-2.4.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1", size = 16822202, upload-time = "2026-01-10T06:44:43.738Z" }, - { url = "https://files.pythonhosted.org/packages/51/e9/1e62a7f77e0f37dcfb0ad6a9744e65df00242b6ea37dfafb55debcbf5b55/numpy-2.4.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344", size = 12569985, upload-time = "2026-01-10T06:44:45.945Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7e/914d54f0c801342306fdcdce3e994a56476f1b818c46c47fc21ae968088c/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e", size = 5398484, upload-time = "2026-01-10T06:44:48.012Z" }, - { url = "https://files.pythonhosted.org/packages/1c/d8/9570b68584e293a33474e7b5a77ca404f1dcc655e40050a600dee81d27fb/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426", size = 6713216, upload-time = "2026-01-10T06:44:49.725Z" }, - { url = "https://files.pythonhosted.org/packages/33/9b/9dd6e2db8d49eb24f86acaaa5258e5f4c8ed38209a4ee9de2d1a0ca25045/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696", size = 14538937, upload-time = "2026-01-10T06:44:51.498Z" }, - { url = "https://files.pythonhosted.org/packages/53/87/d5bd995b0f798a37105b876350d346eea5838bd8f77ea3d7a48392f3812b/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be", size = 16479830, upload-time = "2026-01-10T06:44:53.931Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c7/b801bf98514b6ae6475e941ac05c58e6411dd863ea92916bfd6d510b08c1/numpy-2.4.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4f1b68ff47680c2925f8063402a693ede215f0257f02596b1318ecdfb1d79e33", size = 12492579, upload-time = "2026-01-10T06:44:57.094Z" }, -] - -[[package]] -name = "openai" -version = "2.15.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "httpx" }, - { name = "jiter" }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "tqdm" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/94/f4/4690ecb5d70023ce6bfcfeabfe717020f654bde59a775058ec6ac4692463/openai-2.15.0.tar.gz", hash = "sha256:42eb8cbb407d84770633f31bf727d4ffb4138711c670565a41663d9439174fba", size = 627383, upload-time = "2026-01-09T22:10:08.603Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/df/c306f7375d42bafb379934c2df4c2fa3964656c8c782bac75ee10c102818/openai-2.15.0-py3-none-any.whl", hash = "sha256:6ae23b932cd7230f7244e52954daa6602716d6b9bf235401a107af731baea6c3", size = 1067879, upload-time = "2026-01-09T22:10:06.446Z" }, -] - -[[package]] -name = "opencv-contrib-python" -version = "4.13.0.90" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2d/8a/6d5723ef4551cd6abe9b4c5cbb51bc48ab452b057d48d58d641c27f83c34/opencv_contrib_python-4.13.0.90.tar.gz", hash = "sha256:177d75b048021df8a2632cb5998b3827a30143f88540be2791cac5497e8592dd", size = 150983550, upload-time = "2026-01-18T13:55:20.9Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/31/10a5be5c3f07b066232a0a3c796c705d8c92e9137c30aa041044e974dbe4/opencv_contrib_python-4.13.0.90-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:e000908d262f479042677ef91475cecb1801341ea985af5cb3b9dcde4bb78029", size = 51810049, upload-time = "2026-01-18T08:16:22.443Z" }, - { url = "https://files.pythonhosted.org/packages/e8/29/00c03495a46b22a330c87af0507f4bd8dffd914efc90e492353a0509c659/opencv_contrib_python-4.13.0.90-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:8e7c74869e329636727298f3cab858f015160b2980f4726b538744732d4ab526", size = 38829919, upload-time = "2026-01-18T08:17:48.752Z" }, - { url = "https://files.pythonhosted.org/packages/99/4e/32185a05284e4a804b9fb10aebc816c9d818dddce86488afcf882e1b29d6/opencv_contrib_python-4.13.0.90-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3b95f67bd2539309459057fe6d239603754994155957cee0ce3b82e0f0d8d8a5", size = 53100146, upload-time = "2026-01-18T08:18:22.985Z" }, - { url = "https://files.pythonhosted.org/packages/39/f6/9c3f12778cc7cde48747539bd121e1829e1fed24af6c87fc4144deb8929a/opencv_contrib_python-4.13.0.90-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8ab59de69d1eee39787aa496cfdb2e6e6685733c54c97d416d58e70261836f58", size = 76590796, upload-time = "2026-01-18T08:19:17.471Z" }, - { url = "https://files.pythonhosted.org/packages/01/b2/f53ca77ca5d97a06fc15664bbe813798872ab5d2e4bd639ca0cb35e8c4d5/opencv_contrib_python-4.13.0.90-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c9268888a1592875cb1119f2dc444ff8581a92c27dec65cbe694551b2952d834", size = 52543760, upload-time = "2026-01-18T08:19:58.587Z" }, - { url = "https://files.pythonhosted.org/packages/a9/3d/00071f3a395611a13efca22e3ee65aab25b8bf54128ae5080d8361cbb673/opencv_contrib_python-4.13.0.90-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c693f1fb7a25eae73eb9bc1c2fdbc08ad3df51c31589f1f9a8377f2a4368b1c", size = 79150484, upload-time = "2026-01-18T08:20:48.248Z" }, - { url = "https://files.pythonhosted.org/packages/78/0f/32d766d139aaa56485099a8e2b514578af608c083e01f29050130a411d7f/opencv_contrib_python-4.13.0.90-cp37-abi3-win32.whl", hash = "sha256:334c10e5b191cac919206f797f9b84a2809ab39db20458b993edf8d204379341", size = 36829547, upload-time = "2026-01-18T08:21:22.909Z" }, - { url = "https://files.pythonhosted.org/packages/de/f8/c94c0ef4b525fc672f3692384aa6d971292b02de8fbf880d9e158890c226/opencv_contrib_python-4.13.0.90-cp37-abi3-win_amd64.whl", hash = "sha256:2f7ac460620f5a03924be96d9770c076e2807366a03941bdf38b29431305bfd4", size = 46485754, upload-time = "2026-01-18T08:21:56.426Z" }, -] - -[[package]] -name = "opencv-python" -version = "4.13.0.90" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/d7/133d5756aef78090f4d8dd4895793aed24942dec6064a15375cfac9175fc/opencv_python-4.13.0.90-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:58803f8b05b51d8a785e2306d83b44173b32536f980342f3bc76d8c122b5938d", size = 46020278, upload-time = "2026-01-18T08:57:42.539Z" }, - { url = "https://files.pythonhosted.org/packages/7b/65/3b8cdbe13fa2436695d00e1d8c1ddf5edb4050a93436f34ed867233d1960/opencv_python-4.13.0.90-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:a5354e8b161409fce7710ba4c1cfe88b7bb460d97f705dc4e714a1636616f87d", size = 32568376, upload-time = "2026-01-18T08:58:47.19Z" }, - { url = "https://files.pythonhosted.org/packages/34/ff/e4d7c165e678563f49505d3d2811fcc16011e929cd00bc4b0070c7ee82b0/opencv_python-4.13.0.90-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d557cbf0c7818081c9acf56585b68e781af4f00638971f75eaa3de70904a6314", size = 47685110, upload-time = "2026-01-18T08:59:58.045Z" }, - { url = "https://files.pythonhosted.org/packages/cf/02/d9b73dbce28712204e85ae4c1e179505e9a771f95b33743a97e170caedde/opencv_python-4.13.0.90-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9911581e37b24169e4842069ff01d6645ea2bc4af7e10a022d9ebe340fd035ec", size = 70460479, upload-time = "2026-01-18T09:01:16.377Z" }, - { url = "https://files.pythonhosted.org/packages/fc/1c/87fa71968beb71481ed359e21772061ceff7c9b45a61b3e7daa71e5b0b66/opencv_python-4.13.0.90-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1150b8f1947761b848bbfa9c96ceba8877743ffef157c08a04af6f7717ddd709", size = 46707819, upload-time = "2026-01-18T09:02:48.049Z" }, - { url = "https://files.pythonhosted.org/packages/af/16/915a94e5b537c328fa3e96b769c7d4eed3b67d1be978e0af658a3d3faed8/opencv_python-4.13.0.90-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:d6716f16149b04eea52f953b8ca983d60dd9cd4872c1fd5113f6e2fcebb90e93", size = 72926629, upload-time = "2026-01-18T09:04:29.23Z" }, - { url = "https://files.pythonhosted.org/packages/bf/84/9c63c84be013943dd4c5fff36157f1ec0ec894b69a2fc3026fd4e3c9280a/opencv_python-4.13.0.90-cp37-abi3-win32.whl", hash = "sha256:458a00f2ba47a877eca385be3e7bcc45e6d30a4361d107ce73c1800f516dab09", size = 30932151, upload-time = "2026-01-18T09:05:22.181Z" }, - { url = "https://files.pythonhosted.org/packages/13/de/291cbb17f44242ed6bfd3450fc2535d6bd298115c0ccd6f01cd51d4a11d7/opencv_python-4.13.0.90-cp37-abi3-win_amd64.whl", hash = "sha256:526bde4c33a86808a751e2bb57bf4921beb49794621810971926c472897f6433", size = 40211706, upload-time = "2026-01-18T09:06:06.749Z" }, -] - -[[package]] -name = "orjson" -version = "3.11.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/b8/333fdb27840f3bf04022d21b654a35f58e15407183aeb16f3b41aa053446/orjson-3.11.5.tar.gz", hash = "sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5", size = 5972347, upload-time = "2025-12-06T15:55:39.458Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/68/6b3659daec3a81aed5ab47700adb1a577c76a5452d35b91c88efee89987f/orjson-3.11.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9c8494625ad60a923af6b2b0bd74107146efe9b55099e20d7740d995f338fcd8", size = 245318, upload-time = "2025-12-06T15:54:02.355Z" }, - { url = "https://files.pythonhosted.org/packages/e9/00/92db122261425f61803ccf0830699ea5567439d966cbc35856fe711bfe6b/orjson-3.11.5-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:7bb2ce0b82bc9fd1168a513ddae7a857994b780b2945a8c51db4ab1c4b751ebc", size = 129491, upload-time = "2025-12-06T15:54:03.877Z" }, - { url = "https://files.pythonhosted.org/packages/94/4f/ffdcb18356518809d944e1e1f77589845c278a1ebbb5a8297dfefcc4b4cb/orjson-3.11.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67394d3becd50b954c4ecd24ac90b5051ee7c903d167459f93e77fc6f5b4c968", size = 132167, upload-time = "2025-12-06T15:54:04.944Z" }, - { url = "https://files.pythonhosted.org/packages/97/c6/0a8caff96f4503f4f7dd44e40e90f4d14acf80d3b7a97cb88747bb712d3e/orjson-3.11.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:298d2451f375e5f17b897794bcc3e7b821c0f32b4788b9bcae47ada24d7f3cf7", size = 130516, upload-time = "2025-12-06T15:54:06.274Z" }, - { url = "https://files.pythonhosted.org/packages/4d/63/43d4dc9bd9954bff7052f700fdb501067f6fb134a003ddcea2a0bb3854ed/orjson-3.11.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa5e4244063db8e1d87e0f54c3f7522f14b2dc937e65d5241ef0076a096409fd", size = 135695, upload-time = "2025-12-06T15:54:07.702Z" }, - { url = "https://files.pythonhosted.org/packages/87/6f/27e2e76d110919cb7fcb72b26166ee676480a701bcf8fc53ac5d0edce32f/orjson-3.11.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1db2088b490761976c1b2e956d5d4e6409f3732e9d79cfa69f876c5248d1baf9", size = 139664, upload-time = "2025-12-06T15:54:08.828Z" }, - { url = "https://files.pythonhosted.org/packages/d4/f8/5966153a5f1be49b5fbb8ca619a529fde7bc71aa0a376f2bb83fed248bcd/orjson-3.11.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2ed66358f32c24e10ceea518e16eb3549e34f33a9d51f99ce23b0251776a1ef", size = 137289, upload-time = "2025-12-06T15:54:09.898Z" }, - { url = "https://files.pythonhosted.org/packages/a7/34/8acb12ff0299385c8bbcbb19fbe40030f23f15a6de57a9c587ebf71483fb/orjson-3.11.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2021afda46c1ed64d74b555065dbd4c2558d510d8cec5ea6a53001b3e5e82a9", size = 138784, upload-time = "2025-12-06T15:54:11.022Z" }, - { url = "https://files.pythonhosted.org/packages/ee/27/910421ea6e34a527f73d8f4ee7bdffa48357ff79c7b8d6eb6f7b82dd1176/orjson-3.11.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b42ffbed9128e547a1647a3e50bc88ab28ae9daa61713962e0d3dd35e820c125", size = 141322, upload-time = "2025-12-06T15:54:12.427Z" }, - { url = "https://files.pythonhosted.org/packages/87/a3/4b703edd1a05555d4bb1753d6ce44e1a05b7a6d7c164d5b332c795c63d70/orjson-3.11.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8d5f16195bb671a5dd3d1dbea758918bada8f6cc27de72bd64adfbd748770814", size = 413612, upload-time = "2025-12-06T15:54:13.858Z" }, - { url = "https://files.pythonhosted.org/packages/1b/36/034177f11d7eeea16d3d2c42a1883b0373978e08bc9dad387f5074c786d8/orjson-3.11.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c0e5d9f7a0227df2927d343a6e3859bebf9208b427c79bd31949abcc2fa32fa5", size = 150993, upload-time = "2025-12-06T15:54:15.189Z" }, - { url = "https://files.pythonhosted.org/packages/44/2f/ea8b24ee046a50a7d141c0227c4496b1180b215e728e3b640684f0ea448d/orjson-3.11.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23d04c4543e78f724c4dfe656b3791b5f98e4c9253e13b2636f1af5d90e4a880", size = 141774, upload-time = "2025-12-06T15:54:16.451Z" }, - { url = "https://files.pythonhosted.org/packages/8a/12/cc440554bf8200eb23348a5744a575a342497b65261cd65ef3b28332510a/orjson-3.11.5-cp311-cp311-win32.whl", hash = "sha256:c404603df4865f8e0afe981aa3c4b62b406e6d06049564d58934860b62b7f91d", size = 135109, upload-time = "2025-12-06T15:54:17.73Z" }, - { url = "https://files.pythonhosted.org/packages/a3/83/e0c5aa06ba73a6760134b169f11fb970caa1525fa4461f94d76e692299d9/orjson-3.11.5-cp311-cp311-win_amd64.whl", hash = "sha256:9645ef655735a74da4990c24ffbd6894828fbfa117bc97c1edd98c282ecb52e1", size = 133193, upload-time = "2025-12-06T15:54:19.426Z" }, - { url = "https://files.pythonhosted.org/packages/cb/35/5b77eaebc60d735e832c5b1a20b155667645d123f09d471db0a78280fb49/orjson-3.11.5-cp311-cp311-win_arm64.whl", hash = "sha256:1cbf2735722623fcdee8e712cbaaab9e372bbcb0c7924ad711b261c2eccf4a5c", size = 126830, upload-time = "2025-12-06T15:54:20.836Z" }, - { url = "https://files.pythonhosted.org/packages/ef/a4/8052a029029b096a78955eadd68ab594ce2197e24ec50e6b6d2ab3f4e33b/orjson-3.11.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:334e5b4bff9ad101237c2d799d9fd45737752929753bf4faf4b207335a416b7d", size = 245347, upload-time = "2025-12-06T15:54:22.061Z" }, - { url = "https://files.pythonhosted.org/packages/64/67/574a7732bd9d9d79ac620c8790b4cfe0717a3d5a6eb2b539e6e8995e24a0/orjson-3.11.5-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:ff770589960a86eae279f5d8aa536196ebda8273a2a07db2a54e82b93bc86626", size = 129435, upload-time = "2025-12-06T15:54:23.615Z" }, - { url = "https://files.pythonhosted.org/packages/52/8d/544e77d7a29d90cf4d9eecd0ae801c688e7f3d1adfa2ebae5e1e94d38ab9/orjson-3.11.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed24250e55efbcb0b35bed7caaec8cedf858ab2f9f2201f17b8938c618c8ca6f", size = 132074, upload-time = "2025-12-06T15:54:24.694Z" }, - { url = "https://files.pythonhosted.org/packages/6e/57/b9f5b5b6fbff9c26f77e785baf56ae8460ef74acdb3eae4931c25b8f5ba9/orjson-3.11.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a66d7769e98a08a12a139049aac2f0ca3adae989817f8c43337455fbc7669b85", size = 130520, upload-time = "2025-12-06T15:54:26.185Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6d/d34970bf9eb33f9ec7c979a262cad86076814859e54eb9a059a52f6dc13d/orjson-3.11.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86cfc555bfd5794d24c6a1903e558b50644e5e68e6471d66502ce5cb5fdef3f9", size = 136209, upload-time = "2025-12-06T15:54:27.264Z" }, - { url = "https://files.pythonhosted.org/packages/e7/39/bc373b63cc0e117a105ea12e57280f83ae52fdee426890d57412432d63b3/orjson-3.11.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a230065027bc2a025e944f9d4714976a81e7ecfa940923283bca7bbc1f10f626", size = 139837, upload-time = "2025-12-06T15:54:28.75Z" }, - { url = "https://files.pythonhosted.org/packages/cb/aa/7c4818c8d7d324da220f4f1af55c343956003aa4d1ce1857bdc1d396ba69/orjson-3.11.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b29d36b60e606df01959c4b982729c8845c69d1963f88686608be9ced96dbfaa", size = 137307, upload-time = "2025-12-06T15:54:29.856Z" }, - { url = "https://files.pythonhosted.org/packages/46/bf/0993b5a056759ba65145effe3a79dd5a939d4a070eaa5da2ee3180fbb13f/orjson-3.11.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c74099c6b230d4261fdc3169d50efc09abf38ace1a42ea2f9994b1d79153d477", size = 139020, upload-time = "2025-12-06T15:54:31.024Z" }, - { url = "https://files.pythonhosted.org/packages/65/e8/83a6c95db3039e504eda60fc388f9faedbb4f6472f5aba7084e06552d9aa/orjson-3.11.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e697d06ad57dd0c7a737771d470eedc18e68dfdefcdd3b7de7f33dfda5b6212e", size = 141099, upload-time = "2025-12-06T15:54:32.196Z" }, - { url = "https://files.pythonhosted.org/packages/b9/b4/24fdc024abfce31c2f6812973b0a693688037ece5dc64b7a60c1ce69e2f2/orjson-3.11.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e08ca8a6c851e95aaecc32bc44a5aa75d0ad26af8cdac7c77e4ed93acf3d5b69", size = 413540, upload-time = "2025-12-06T15:54:33.361Z" }, - { url = "https://files.pythonhosted.org/packages/d9/37/01c0ec95d55ed0c11e4cae3e10427e479bba40c77312b63e1f9665e0737d/orjson-3.11.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e8b5f96c05fce7d0218df3fdfeb962d6b8cfff7e3e20264306b46dd8b217c0f3", size = 151530, upload-time = "2025-12-06T15:54:34.6Z" }, - { url = "https://files.pythonhosted.org/packages/f9/d4/f9ebc57182705bb4bbe63f5bbe14af43722a2533135e1d2fb7affa0c355d/orjson-3.11.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddbfdb5099b3e6ba6d6ea818f61997bb66de14b411357d24c4612cf1ebad08ca", size = 141863, upload-time = "2025-12-06T15:54:35.801Z" }, - { url = "https://files.pythonhosted.org/packages/0d/04/02102b8d19fdcb009d72d622bb5781e8f3fae1646bf3e18c53d1bc8115b5/orjson-3.11.5-cp312-cp312-win32.whl", hash = "sha256:9172578c4eb09dbfcf1657d43198de59b6cef4054de385365060ed50c458ac98", size = 135255, upload-time = "2025-12-06T15:54:37.209Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fb/f05646c43d5450492cb387de5549f6de90a71001682c17882d9f66476af5/orjson-3.11.5-cp312-cp312-win_amd64.whl", hash = "sha256:2b91126e7b470ff2e75746f6f6ee32b9ab67b7a93c8ba1d15d3a0caaf16ec875", size = 133252, upload-time = "2025-12-06T15:54:38.401Z" }, - { url = "https://files.pythonhosted.org/packages/dc/a6/7b8c0b26ba18c793533ac1cd145e131e46fcf43952aa94c109b5b913c1f0/orjson-3.11.5-cp312-cp312-win_arm64.whl", hash = "sha256:acbc5fac7e06777555b0722b8ad5f574739e99ffe99467ed63da98f97f9ca0fe", size = 126777, upload-time = "2025-12-06T15:54:39.515Z" }, - { url = "https://files.pythonhosted.org/packages/10/43/61a77040ce59f1569edf38f0b9faadc90c8cf7e9bec2e0df51d0132c6bb7/orjson-3.11.5-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3b01799262081a4c47c035dd77c1301d40f568f77cc7ec1bb7db5d63b0a01629", size = 245271, upload-time = "2025-12-06T15:54:40.878Z" }, - { url = "https://files.pythonhosted.org/packages/55/f9/0f79be617388227866d50edd2fd320cb8fb94dc1501184bb1620981a0aba/orjson-3.11.5-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:61de247948108484779f57a9f406e4c84d636fa5a59e411e6352484985e8a7c3", size = 129422, upload-time = "2025-12-06T15:54:42.403Z" }, - { url = "https://files.pythonhosted.org/packages/77/42/f1bf1549b432d4a78bfa95735b79b5dac75b65b5bb815bba86ad406ead0a/orjson-3.11.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:894aea2e63d4f24a7f04a1908307c738d0dce992e9249e744b8f4e8dd9197f39", size = 132060, upload-time = "2025-12-06T15:54:43.531Z" }, - { url = "https://files.pythonhosted.org/packages/25/49/825aa6b929f1a6ed244c78acd7b22c1481fd7e5fda047dc8bf4c1a807eb6/orjson-3.11.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ddc21521598dbe369d83d4d40338e23d4101dad21dae0e79fa20465dbace019f", size = 130391, upload-time = "2025-12-06T15:54:45.059Z" }, - { url = "https://files.pythonhosted.org/packages/42/ec/de55391858b49e16e1aa8f0bbbb7e5997b7345d8e984a2dec3746d13065b/orjson-3.11.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cce16ae2f5fb2c53c3eafdd1706cb7b6530a67cc1c17abe8ec747f5cd7c0c51", size = 135964, upload-time = "2025-12-06T15:54:46.576Z" }, - { url = "https://files.pythonhosted.org/packages/1c/40/820bc63121d2d28818556a2d0a09384a9f0262407cf9fa305e091a8048df/orjson-3.11.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e46c762d9f0e1cfb4ccc8515de7f349abbc95b59cb5a2bd68df5973fdef913f8", size = 139817, upload-time = "2025-12-06T15:54:48.084Z" }, - { url = "https://files.pythonhosted.org/packages/09/c7/3a445ca9a84a0d59d26365fd8898ff52bdfcdcb825bcc6519830371d2364/orjson-3.11.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7345c759276b798ccd6d77a87136029e71e66a8bbf2d2755cbdde1d82e78706", size = 137336, upload-time = "2025-12-06T15:54:49.426Z" }, - { url = "https://files.pythonhosted.org/packages/9a/b3/dc0d3771f2e5d1f13368f56b339c6782f955c6a20b50465a91acb79fe961/orjson-3.11.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75bc2e59e6a2ac1dd28901d07115abdebc4563b5b07dd612bf64260a201b1c7f", size = 138993, upload-time = "2025-12-06T15:54:50.939Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a2/65267e959de6abe23444659b6e19c888f242bf7725ff927e2292776f6b89/orjson-3.11.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:54aae9b654554c3b4edd61896b978568c6daa16af96fa4681c9b5babd469f863", size = 141070, upload-time = "2025-12-06T15:54:52.414Z" }, - { url = "https://files.pythonhosted.org/packages/63/c9/da44a321b288727a322c6ab17e1754195708786a04f4f9d2220a5076a649/orjson-3.11.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4bdd8d164a871c4ec773f9de0f6fe8769c2d6727879c37a9666ba4183b7f8228", size = 413505, upload-time = "2025-12-06T15:54:53.67Z" }, - { url = "https://files.pythonhosted.org/packages/7f/17/68dc14fa7000eefb3d4d6d7326a190c99bb65e319f02747ef3ebf2452f12/orjson-3.11.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a261fef929bcf98a60713bf5e95ad067cea16ae345d9a35034e73c3990e927d2", size = 151342, upload-time = "2025-12-06T15:54:55.113Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c5/ccee774b67225bed630a57478529fc026eda33d94fe4c0eac8fe58d4aa52/orjson-3.11.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c028a394c766693c5c9909dec76b24f37e6a1b91999e8d0c0d5feecbe93c3e05", size = 141823, upload-time = "2025-12-06T15:54:56.331Z" }, - { url = "https://files.pythonhosted.org/packages/67/80/5d00e4155d0cd7390ae2087130637671da713959bb558db9bac5e6f6b042/orjson-3.11.5-cp313-cp313-win32.whl", hash = "sha256:2cc79aaad1dfabe1bd2d50ee09814a1253164b3da4c00a78c458d82d04b3bdef", size = 135236, upload-time = "2025-12-06T15:54:57.507Z" }, - { url = "https://files.pythonhosted.org/packages/95/fe/792cc06a84808dbdc20ac6eab6811c53091b42f8e51ecebf14b540e9cfe4/orjson-3.11.5-cp313-cp313-win_amd64.whl", hash = "sha256:ff7877d376add4e16b274e35a3f58b7f37b362abf4aa31863dadacdd20e3a583", size = 133167, upload-time = "2025-12-06T15:54:58.71Z" }, - { url = "https://files.pythonhosted.org/packages/46/2c/d158bd8b50e3b1cfdcf406a7e463f6ffe3f0d167b99634717acdaf5e299f/orjson-3.11.5-cp313-cp313-win_arm64.whl", hash = "sha256:59ac72ea775c88b163ba8d21b0177628bd015c5dd060647bbab6e22da3aad287", size = 126712, upload-time = "2025-12-06T15:54:59.892Z" }, - { url = "https://files.pythonhosted.org/packages/c2/60/77d7b839e317ead7bb225d55bb50f7ea75f47afc489c81199befc5435b50/orjson-3.11.5-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e446a8ea0a4c366ceafc7d97067bfd55292969143b57e3c846d87fc701e797a0", size = 245252, upload-time = "2025-12-06T15:55:01.127Z" }, - { url = "https://files.pythonhosted.org/packages/f1/aa/d4639163b400f8044cef0fb9aa51b0337be0da3a27187a20d1166e742370/orjson-3.11.5-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:53deb5addae9c22bbe3739298f5f2196afa881ea75944e7720681c7080909a81", size = 129419, upload-time = "2025-12-06T15:55:02.723Z" }, - { url = "https://files.pythonhosted.org/packages/30/94/9eabf94f2e11c671111139edf5ec410d2f21e6feee717804f7e8872d883f/orjson-3.11.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd00d49d6063d2b8791da5d4f9d20539c5951f965e45ccf4e96d33505ce68f", size = 132050, upload-time = "2025-12-06T15:55:03.918Z" }, - { url = "https://files.pythonhosted.org/packages/3d/c8/ca10f5c5322f341ea9a9f1097e140be17a88f88d1cfdd29df522970d9744/orjson-3.11.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fd15f9fc8c203aeceff4fda211157fad114dde66e92e24097b3647a08f4ee9e", size = 130370, upload-time = "2025-12-06T15:55:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/25/d4/e96824476d361ee2edd5c6290ceb8d7edf88d81148a6ce172fc00278ca7f/orjson-3.11.5-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df95000fbe6777bf9820ae82ab7578e8662051bb5f83d71a28992f539d2cda7", size = 136012, upload-time = "2025-12-06T15:55:06.402Z" }, - { url = "https://files.pythonhosted.org/packages/85/8e/9bc3423308c425c588903f2d103cfcfe2539e07a25d6522900645a6f257f/orjson-3.11.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a8d676748fca47ade5bc3da7430ed7767afe51b2f8100e3cd65e151c0eaceb", size = 139809, upload-time = "2025-12-06T15:55:07.656Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3c/b404e94e0b02a232b957c54643ce68d0268dacb67ac33ffdee24008c8b27/orjson-3.11.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa0f513be38b40234c77975e68805506cad5d57b3dfd8fe3baa7f4f4051e15b4", size = 137332, upload-time = "2025-12-06T15:55:08.961Z" }, - { url = "https://files.pythonhosted.org/packages/51/30/cc2d69d5ce0ad9b84811cdf4a0cd5362ac27205a921da524ff42f26d65e0/orjson-3.11.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1863e75b92891f553b7922ce4ee10ed06db061e104f2b7815de80cdcb135ad", size = 138983, upload-time = "2025-12-06T15:55:10.595Z" }, - { url = "https://files.pythonhosted.org/packages/0e/87/de3223944a3e297d4707d2fe3b1ffb71437550e165eaf0ca8bbe43ccbcb1/orjson-3.11.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4be86b58e9ea262617b8ca6251a2f0d63cc132a6da4b5fcc8e0a4128782c829", size = 141069, upload-time = "2025-12-06T15:55:11.832Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/81d5087ae74be33bcae3ff2d80f5ccaa4a8fedc6d39bf65a427a95b8977f/orjson-3.11.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b923c1c13fa02084eb38c9c065afd860a5cff58026813319a06949c3af5732ac", size = 413491, upload-time = "2025-12-06T15:55:13.314Z" }, - { url = "https://files.pythonhosted.org/packages/d0/6f/f6058c21e2fc1efaf918986dbc2da5cd38044f1a2d4b7b91ad17c4acf786/orjson-3.11.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1b6bd351202b2cd987f35a13b5e16471cf4d952b42a73c391cc537974c43ef6d", size = 151375, upload-time = "2025-12-06T15:55:14.715Z" }, - { url = "https://files.pythonhosted.org/packages/54/92/c6921f17d45e110892899a7a563a925b2273d929959ce2ad89e2525b885b/orjson-3.11.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb150d529637d541e6af06bbe3d02f5498d628b7f98267ff87647584293ab439", size = 141850, upload-time = "2025-12-06T15:55:15.94Z" }, - { url = "https://files.pythonhosted.org/packages/88/86/cdecb0140a05e1a477b81f24739da93b25070ee01ce7f7242f44a6437594/orjson-3.11.5-cp314-cp314-win32.whl", hash = "sha256:9cc1e55c884921434a84a0c3dd2699eb9f92e7b441d7f53f3941079ec6ce7499", size = 135278, upload-time = "2025-12-06T15:55:17.202Z" }, - { url = "https://files.pythonhosted.org/packages/e4/97/b638d69b1e947d24f6109216997e38922d54dcdcdb1b11c18d7efd2d3c59/orjson-3.11.5-cp314-cp314-win_amd64.whl", hash = "sha256:a4f3cb2d874e03bc7767c8f88adaa1a9a05cecea3712649c3b58589ec7317310", size = 133170, upload-time = "2025-12-06T15:55:18.468Z" }, - { url = "https://files.pythonhosted.org/packages/8f/dd/f4fff4a6fe601b4f8f3ba3aa6da8ac33d17d124491a3b804c662a70e1636/orjson-3.11.5-cp314-cp314-win_arm64.whl", hash = "sha256:38b22f476c351f9a1c43e5b07d8b5a02eb24a6ab8e75f700f7d479d4568346a5", size = 126713, upload-time = "2025-12-06T15:55:19.738Z" }, -] - -[[package]] -name = "ormsgpack" -version = "1.12.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/12/0c/f1761e21486942ab9bb6feaebc610fa074f7c5e496e6962dea5873348077/ormsgpack-1.12.2.tar.gz", hash = "sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33", size = 39031, upload-time = "2026-01-18T20:55:28.023Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/08/8b68f24b18e69d92238aa8f258218e6dfeacf4381d9d07ab8df303f524a9/ormsgpack-1.12.2-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bd5f4bf04c37888e864f08e740c5a573c4017f6fd6e99fa944c5c935fabf2dd9", size = 378266, upload-time = "2026-01-18T20:55:59.876Z" }, - { url = "https://files.pythonhosted.org/packages/0d/24/29fc13044ecb7c153523ae0a1972269fcd613650d1fa1a9cec1044c6b666/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d5b28b3570e9fed9a5a76528fc7230c3c76333bc214798958e58e9b79cc18a", size = 203035, upload-time = "2026-01-18T20:55:30.59Z" }, - { url = "https://files.pythonhosted.org/packages/ad/c2/00169fb25dd8f9213f5e8a549dfb73e4d592009ebc85fbbcd3e1dcac575b/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3708693412c28f3538fb5a65da93787b6bbab3484f6bc6e935bfb77a62400ae5", size = 210539, upload-time = "2026-01-18T20:55:48.569Z" }, - { url = "https://files.pythonhosted.org/packages/1b/33/543627f323ff3c73091f51d6a20db28a1a33531af30873ea90c5ac95a9b5/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43013a3f3e2e902e1d05e72c0f1aeb5bedbb8e09240b51e26792a3c89267e181", size = 212401, upload-time = "2026-01-18T20:56:10.101Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5d/f70e2c3da414f46186659d24745483757bcc9adccb481a6eb93e2b729301/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7c8b1667a72cbba74f0ae7ecf3105a5e01304620ed14528b2cb4320679d2869b", size = 387082, upload-time = "2026-01-18T20:56:12.047Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d6/06e8dc920c7903e051f30934d874d4afccc9bb1c09dcaf0bc03a7de4b343/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:df6961442140193e517303d0b5d7bc2e20e69a879c2d774316125350c4a76b92", size = 482346, upload-time = "2026-01-18T20:56:05.152Z" }, - { url = "https://files.pythonhosted.org/packages/66/c4/f337ac0905eed9c393ef990c54565cd33644918e0a8031fe48c098c71dbf/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6a4c34ddef109647c769d69be65fa1de7a6022b02ad45546a69b3216573eb4a", size = 425181, upload-time = "2026-01-18T20:55:37.83Z" }, - { url = "https://files.pythonhosted.org/packages/78/29/6d5758fabef3babdf4bbbc453738cc7de9cd3334e4c38dd5737e27b85653/ormsgpack-1.12.2-cp311-cp311-win_amd64.whl", hash = "sha256:73670ed0375ecc303858e3613f407628dd1fca18fe6ac57b7b7ce66cc7bb006c", size = 117182, upload-time = "2026-01-18T20:55:31.472Z" }, - { url = "https://files.pythonhosted.org/packages/c4/57/17a15549233c37e7fd054c48fe9207492e06b026dbd872b826a0b5f833b6/ormsgpack-1.12.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2be829954434e33601ae5da328cccce3266b098927ca7a30246a0baec2ce7bd", size = 111464, upload-time = "2026-01-18T20:55:38.811Z" }, - { url = "https://files.pythonhosted.org/packages/4c/36/16c4b1921c308a92cef3bf6663226ae283395aa0ff6e154f925c32e91ff5/ormsgpack-1.12.2-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7a29d09b64b9694b588ff2f80e9826bdceb3a2b91523c5beae1fab27d5c940e7", size = 378618, upload-time = "2026-01-18T20:55:50.835Z" }, - { url = "https://files.pythonhosted.org/packages/c0/68/468de634079615abf66ed13bb5c34ff71da237213f29294363beeeca5306/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b39e629fd2e1c5b2f46f99778450b59454d1f901bc507963168985e79f09c5d", size = 203186, upload-time = "2026-01-18T20:56:11.163Z" }, - { url = "https://files.pythonhosted.org/packages/73/a9/d756e01961442688b7939bacd87ce13bfad7d26ce24f910f6028178b2cc8/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:958dcb270d30a7cb633a45ee62b9444433fa571a752d2ca484efdac07480876e", size = 210738, upload-time = "2026-01-18T20:56:09.181Z" }, - { url = "https://files.pythonhosted.org/packages/7b/ba/795b1036888542c9113269a3f5690ab53dd2258c6fb17676ac4bd44fcf94/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d379d72b6c5e964851c77cfedfb386e474adee4fd39791c2c5d9efb53505cc", size = 212569, upload-time = "2026-01-18T20:56:06.135Z" }, - { url = "https://files.pythonhosted.org/packages/6c/aa/bff73c57497b9e0cba8837c7e4bcab584b1a6dbc91a5dd5526784a5030c8/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8463a3fc5f09832e67bdb0e2fda6d518dc4281b133166146a67f54c08496442e", size = 387166, upload-time = "2026-01-18T20:55:36.738Z" }, - { url = "https://files.pythonhosted.org/packages/d3/cf/f8283cba44bcb7b14f97b6274d449db276b3a86589bdb363169b51bc12de/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eddffb77eff0bad4e67547d67a130604e7e2dfbb7b0cde0796045be4090f35c6", size = 482498, upload-time = "2026-01-18T20:55:29.626Z" }, - { url = "https://files.pythonhosted.org/packages/05/be/71e37b852d723dfcbe952ad04178c030df60d6b78eba26bfd14c9a40575e/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd", size = 425518, upload-time = "2026-01-18T20:55:49.556Z" }, - { url = "https://files.pythonhosted.org/packages/7a/0c/9803aa883d18c7ef197213cd2cbf73ba76472a11fe100fb7dab2884edf48/ormsgpack-1.12.2-cp312-cp312-win_amd64.whl", hash = "sha256:d024b40828f1dde5654faebd0d824f9cc29ad46891f626272dd5bfd7af2333a4", size = 117462, upload-time = "2026-01-18T20:55:47.726Z" }, - { url = "https://files.pythonhosted.org/packages/c8/9e/029e898298b2cc662f10d7a15652a53e3b525b1e7f07e21fef8536a09bb8/ormsgpack-1.12.2-cp312-cp312-win_arm64.whl", hash = "sha256:da538c542bac7d1c8f3f2a937863dba36f013108ce63e55745941dda4b75dbb6", size = 111559, upload-time = "2026-01-18T20:55:54.273Z" }, - { url = "https://files.pythonhosted.org/packages/eb/29/bb0eba3288c0449efbb013e9c6f58aea79cf5cb9ee1921f8865f04c1a9d7/ormsgpack-1.12.2-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5ea60cb5f210b1cfbad8c002948d73447508e629ec375acb82910e3efa8ff355", size = 378661, upload-time = "2026-01-18T20:55:57.765Z" }, - { url = "https://files.pythonhosted.org/packages/6e/31/5efa31346affdac489acade2926989e019e8ca98129658a183e3add7af5e/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1", size = 203194, upload-time = "2026-01-18T20:56:08.252Z" }, - { url = "https://files.pythonhosted.org/packages/eb/56/d0087278beef833187e0167f8527235ebe6f6ffc2a143e9de12a98b1ce87/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29a9f17a3dac6054c0dce7925e0f4995c727f7c41859adf9b5572180f640d172", size = 210778, upload-time = "2026-01-18T20:55:17.694Z" }, - { url = "https://files.pythonhosted.org/packages/1c/a2/072343e1413d9443e5a252a8eb591c2d5b1bffbe5e7bfc78c069361b92eb/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39c1bd2092880e413902910388be8715f70b9f15f20779d44e673033a6146f2d", size = 212592, upload-time = "2026-01-18T20:55:32.747Z" }, - { url = "https://files.pythonhosted.org/packages/a2/8b/a0da3b98a91d41187a63b02dda14267eefc2a74fcb43cc2701066cf1510e/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50b7249244382209877deedeee838aef1542f3d0fc28b8fe71ca9d7e1896a0d7", size = 387164, upload-time = "2026-01-18T20:55:40.853Z" }, - { url = "https://files.pythonhosted.org/packages/19/bb/6d226bc4cf9fc20d8eb1d976d027a3f7c3491e8f08289a2e76abe96a65f3/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5af04800d844451cf102a59c74a841324868d3f1625c296a06cc655c542a6685", size = 482516, upload-time = "2026-01-18T20:55:42.033Z" }, - { url = "https://files.pythonhosted.org/packages/fb/f1/bb2c7223398543dedb3dbf8bb93aaa737b387de61c5feaad6f908841b782/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cec70477d4371cd524534cd16472d8b9cc187e0e3043a8790545a9a9b296c258", size = 425539, upload-time = "2026-01-18T20:55:24.727Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e8/0fb45f57a2ada1fed374f7494c8cd55e2f88ccd0ab0a669aa3468716bf5f/ormsgpack-1.12.2-cp313-cp313-win_amd64.whl", hash = "sha256:21f4276caca5c03a818041d637e4019bc84f9d6ca8baa5ea03e5cc8bf56140e9", size = 117459, upload-time = "2026-01-18T20:55:56.876Z" }, - { url = "https://files.pythonhosted.org/packages/7a/d4/0cfeea1e960d550a131001a7f38a5132c7ae3ebde4c82af1f364ccc5d904/ormsgpack-1.12.2-cp313-cp313-win_arm64.whl", hash = "sha256:baca4b6773d20a82e36d6fd25f341064244f9f86a13dead95dd7d7f996f51709", size = 111577, upload-time = "2026-01-18T20:55:43.605Z" }, - { url = "https://files.pythonhosted.org/packages/94/16/24d18851334be09c25e87f74307c84950f18c324a4d3c0b41dabdbf19c29/ormsgpack-1.12.2-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bc68dd5915f4acf66ff2010ee47c8906dc1cf07399b16f4089f8c71733f6e36c", size = 378717, upload-time = "2026-01-18T20:55:26.164Z" }, - { url = "https://files.pythonhosted.org/packages/b5/a2/88b9b56f83adae8032ac6a6fa7f080c65b3baf9b6b64fd3d37bd202991d4/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46d084427b4132553940070ad95107266656cb646ea9da4975f85cb1a6676553", size = 203183, upload-time = "2026-01-18T20:55:18.815Z" }, - { url = "https://files.pythonhosted.org/packages/a9/80/43e4555963bf602e5bdc79cbc8debd8b6d5456c00d2504df9775e74b450b/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c010da16235806cf1d7bc4c96bf286bfa91c686853395a299b3ddb49499a3e13", size = 210814, upload-time = "2026-01-18T20:55:33.973Z" }, - { url = "https://files.pythonhosted.org/packages/78/e1/7cfbf28de8bca6efe7e525b329c31277d1b64ce08dcba723971c241a9d60/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18867233df592c997154ff942a6503df274b5ac1765215bceba7a231bea2745d", size = 212634, upload-time = "2026-01-18T20:55:28.634Z" }, - { url = "https://files.pythonhosted.org/packages/95/f8/30ae5716e88d792a4e879debee195653c26ddd3964c968594ddef0a3cc7e/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b009049086ddc6b8f80c76b3955df1aa22a5fbd7673c525cd63bf91f23122ede", size = 387139, upload-time = "2026-01-18T20:56:02.013Z" }, - { url = "https://files.pythonhosted.org/packages/dc/81/aee5b18a3e3a0e52f718b37ab4b8af6fae0d9d6a65103036a90c2a8ffb5d/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1dcc17d92b6390d4f18f937cf0b99054824a7815818012ddca925d6e01c2e49e", size = 482578, upload-time = "2026-01-18T20:55:35.117Z" }, - { url = "https://files.pythonhosted.org/packages/bd/17/71c9ba472d5d45f7546317f467a5fc941929cd68fb32796ca3d13dcbaec2/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f04b5e896d510b07c0ad733d7fce2d44b260c5e6c402d272128f8941984e4285", size = 425539, upload-time = "2026-01-18T20:56:04.009Z" }, - { url = "https://files.pythonhosted.org/packages/2e/a6/ac99cd7fe77e822fed5250ff4b86fa66dd4238937dd178d2299f10b69816/ormsgpack-1.12.2-cp314-cp314-win_amd64.whl", hash = "sha256:ae3aba7eed4ca7cb79fd3436eddd29140f17ea254b91604aa1eb19bfcedb990f", size = 117493, upload-time = "2026-01-18T20:56:07.343Z" }, - { url = "https://files.pythonhosted.org/packages/3a/67/339872846a1ae4592535385a1c1f93614138566d7af094200c9c3b45d1e5/ormsgpack-1.12.2-cp314-cp314-win_arm64.whl", hash = "sha256:118576ea6006893aea811b17429bfc561b4778fad393f5f538c84af70b01260c", size = 111579, upload-time = "2026-01-18T20:55:21.161Z" }, - { url = "https://files.pythonhosted.org/packages/49/c2/6feb972dc87285ad381749d3882d8aecbde9f6ecf908dd717d33d66df095/ormsgpack-1.12.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7121b3d355d3858781dc40dafe25a32ff8a8242b9d80c692fd548a4b1f7fd3c8", size = 378721, upload-time = "2026-01-18T20:55:52.12Z" }, - { url = "https://files.pythonhosted.org/packages/a3/9a/900a6b9b413e0f8a471cf07830f9cf65939af039a362204b36bd5b581d8b/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ee766d2e78251b7a63daf1cddfac36a73562d3ddef68cacfb41b2af64698033", size = 203170, upload-time = "2026-01-18T20:55:44.469Z" }, - { url = "https://files.pythonhosted.org/packages/87/4c/27a95466354606b256f24fad464d7c97ab62bce6cc529dd4673e1179b8fb/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292410a7d23de9b40444636b9b8f1e4e4b814af7f1ef476e44887e52a123f09d", size = 212816, upload-time = "2026-01-18T20:55:23.501Z" }, - { url = "https://files.pythonhosted.org/packages/73/cd/29cee6007bddf7a834e6cd6f536754c0535fcb939d384f0f37a38b1cddb8/ormsgpack-1.12.2-cp314-cp314t-win_amd64.whl", hash = "sha256:837dd316584485b72ef451d08dd3e96c4a11d12e4963aedb40e08f89685d8ec2", size = 117232, upload-time = "2026-01-18T20:55:45.448Z" }, -] - -[[package]] -name = "packaging" -version = "25.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, -] - -[[package]] -name = "passlib" -version = "1.7.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" }, -] - -[[package]] -name = "pathspec" -version = "1.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, -] - -[[package]] -name = "pillow" -version = "12.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3", size = 5304057, upload-time = "2026-01-02T09:10:46.627Z" }, - { url = "https://files.pythonhosted.org/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0", size = 4657811, upload-time = "2026-01-02T09:10:49.548Z" }, - { url = "https://files.pythonhosted.org/packages/72/64/f9189e44474610daf83da31145fa56710b627b5c4c0b9c235e34058f6b31/pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451", size = 6232243, upload-time = "2026-01-02T09:10:51.62Z" }, - { url = "https://files.pythonhosted.org/packages/ef/30/0df458009be6a4caca4ca2c52975e6275c387d4e5c95544e34138b41dc86/pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e", size = 8037872, upload-time = "2026-01-02T09:10:53.446Z" }, - { url = "https://files.pythonhosted.org/packages/e4/86/95845d4eda4f4f9557e25381d70876aa213560243ac1a6d619c46caaedd9/pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84", size = 6345398, upload-time = "2026-01-02T09:10:55.426Z" }, - { url = "https://files.pythonhosted.org/packages/5c/1f/8e66ab9be3aaf1435bc03edd1ebdf58ffcd17f7349c1d970cafe87af27d9/pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0", size = 7034667, upload-time = "2026-01-02T09:10:57.11Z" }, - { url = "https://files.pythonhosted.org/packages/f9/f6/683b83cb9b1db1fb52b87951b1c0b99bdcfceaa75febf11406c19f82cb5e/pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b", size = 6458743, upload-time = "2026-01-02T09:10:59.331Z" }, - { url = "https://files.pythonhosted.org/packages/9a/7d/de833d63622538c1d58ce5395e7c6cb7e7dce80decdd8bde4a484e095d9f/pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18", size = 7159342, upload-time = "2026-01-02T09:11:01.82Z" }, - { url = "https://files.pythonhosted.org/packages/8c/40/50d86571c9e5868c42b81fe7da0c76ca26373f3b95a8dd675425f4a92ec1/pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64", size = 6328655, upload-time = "2026-01-02T09:11:04.556Z" }, - { url = "https://files.pythonhosted.org/packages/6c/af/b1d7e301c4cd26cd45d4af884d9ee9b6fab893b0ad2450d4746d74a6968c/pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75", size = 7031469, upload-time = "2026-01-02T09:11:06.538Z" }, - { url = "https://files.pythonhosted.org/packages/48/36/d5716586d887fb2a810a4a61518a327a1e21c8b7134c89283af272efe84b/pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304", size = 2452515, upload-time = "2026-01-02T09:11:08.226Z" }, - { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" }, - { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" }, - { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" }, - { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" }, - { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" }, - { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" }, - { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" }, - { url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" }, - { url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" }, - { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" }, - { url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" }, - { url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" }, - { url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" }, - { url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" }, - { url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" }, - { url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" }, - { url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" }, - { url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" }, - { url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" }, - { url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" }, - { url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" }, - { url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551, upload-time = "2026-01-02T09:11:52.234Z" }, - { url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087, upload-time = "2026-01-02T09:11:54.822Z" }, - { url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470, upload-time = "2026-01-02T09:11:56.522Z" }, - { url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" }, - { url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" }, - { url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" }, - { url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" }, - { url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" }, - { url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" }, - { url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" }, - { url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" }, - { url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" }, - { url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" }, - { url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" }, - { url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" }, - { url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" }, - { url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" }, - { url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" }, - { url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" }, - { url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" }, - { url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" }, - { url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" }, - { url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" }, - { url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" }, - { url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" }, - { url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" }, - { url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" }, - { url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" }, - { url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" }, - { url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" }, - { url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" }, - { url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" }, - { url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" }, - { url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" }, - { url = "https://files.pythonhosted.org/packages/8b/bc/224b1d98cffd7164b14707c91aac83c07b047fbd8f58eba4066a3e53746a/pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377", size = 5228605, upload-time = "2026-01-02T09:13:14.084Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ca/49ca7769c4550107de049ed85208240ba0f330b3f2e316f24534795702ce/pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72", size = 4622245, upload-time = "2026-01-02T09:13:15.964Z" }, - { url = "https://files.pythonhosted.org/packages/73/48/fac807ce82e5955bcc2718642b94b1bd22a82a6d452aea31cbb678cddf12/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c", size = 5247593, upload-time = "2026-01-02T09:13:17.913Z" }, - { url = "https://files.pythonhosted.org/packages/d2/95/3e0742fe358c4664aed4fd05d5f5373dcdad0b27af52aa0972568541e3f4/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd", size = 6989008, upload-time = "2026-01-02T09:13:20.083Z" }, - { url = "https://files.pythonhosted.org/packages/5a/74/fe2ac378e4e202e56d50540d92e1ef4ff34ed687f3c60f6a121bcf99437e/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc", size = 5313824, upload-time = "2026-01-02T09:13:22.405Z" }, - { url = "https://files.pythonhosted.org/packages/f3/77/2a60dee1adee4e2655ac328dd05c02a955c1cd683b9f1b82ec3feb44727c/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a", size = 5963278, upload-time = "2026-01-02T09:13:24.706Z" }, - { url = "https://files.pythonhosted.org/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", size = 7029809, upload-time = "2026-01-02T09:13:26.541Z" }, -] - -[[package]] -name = "platformdirs" -version = "4.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, -] - -[[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 = "pre-commit" -version = "4.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cfgv" }, - { name = "identify" }, - { name = "nodeenv" }, - { name = "pyyaml" }, - { name = "virtualenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, -] - -[[package]] -name = "propcache" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, - { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, - { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, - { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, - { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, - { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, - { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, - { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, - { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, - { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, - { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, - { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, - { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, - { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, - { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, - { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, - { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, - { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, - { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, - { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, - { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, - { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, - { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, - { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, - { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, - { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, - { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, - { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, - { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, - { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, - { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, - { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, - { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, - { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, - { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, - { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, - { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, - { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, - { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, - { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, - { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, - { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, - { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, - { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, - { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, - { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, - { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, - { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, - { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, - { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, - { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, - { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, - { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, - { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, - { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, - { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, - { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, - { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, - { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, - { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, - { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, - { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, - { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, -] - -[[package]] -name = "pyasn1" -version = "0.6.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" }, -] - -[[package]] -name = "pycparser" -version = "3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, -] - -[[package]] -name = "pydantic" -version = "2.12.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, -] - -[[package]] -name = "pydantic-core" -version = "2.41.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, - { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, - { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, - { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, - { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, - { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, - { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, - { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, - { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, - { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, - { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, - { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, -] - -[[package]] -name = "pydantic-settings" -version = "2.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, -] - -[[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 = "pyparsing" -version = "3.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, -] - -[[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 = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - -[[package]] -name = "python-dotenv" -version = "1.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, -] - -[[package]] -name = "python-jose" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ecdsa" }, - { name = "pyasn1" }, - { name = "rsa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c6/77/3a1c9039db7124eb039772b935f2244fbb73fc8ee65b9acf2375da1c07bf/python_jose-3.5.0.tar.gz", hash = "sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b", size = 92726, upload-time = "2025-05-28T17:31:54.288Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771", size = 34624, upload-time = "2025-05-28T17:31:52.802Z" }, -] - -[package.optional-dependencies] -cryptography = [ - { name = "cryptography" }, -] - -[[package]] -name = "python-multipart" -version = "0.0.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, -] - -[[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/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, - { 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 = "regex" -version = "2026.1.15" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/c9/0c80c96eab96948363d270143138d671d5731c3a692b417629bf3492a9d6/regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a", size = 488168, upload-time = "2026-01-14T23:14:16.129Z" }, - { url = "https://files.pythonhosted.org/packages/17/f0/271c92f5389a552494c429e5cc38d76d1322eb142fb5db3c8ccc47751468/regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f", size = 290636, upload-time = "2026-01-14T23:14:17.715Z" }, - { url = "https://files.pythonhosted.org/packages/a0/f9/5f1fd077d106ca5655a0f9ff8f25a1ab55b92128b5713a91ed7134ff688e/regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1", size = 288496, upload-time = "2026-01-14T23:14:19.326Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e1/8f43b03a4968c748858ec77f746c286d81f896c2e437ccf050ebc5d3128c/regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b", size = 793503, upload-time = "2026-01-14T23:14:20.922Z" }, - { url = "https://files.pythonhosted.org/packages/8d/4e/a39a5e8edc5377a46a7c875c2f9a626ed3338cb3bb06931be461c3e1a34a/regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8", size = 860535, upload-time = "2026-01-14T23:14:22.405Z" }, - { url = "https://files.pythonhosted.org/packages/dc/1c/9dce667a32a9477f7a2869c1c767dc00727284a9fa3ff5c09a5c6c03575e/regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413", size = 907225, upload-time = "2026-01-14T23:14:23.897Z" }, - { url = "https://files.pythonhosted.org/packages/a4/3c/87ca0a02736d16b6262921425e84b48984e77d8e4e572c9072ce96e66c30/regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026", size = 800526, upload-time = "2026-01-14T23:14:26.039Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ff/647d5715aeea7c87bdcbd2f578f47b415f55c24e361e639fe8c0cc88878f/regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785", size = 773446, upload-time = "2026-01-14T23:14:28.109Z" }, - { url = "https://files.pythonhosted.org/packages/af/89/bf22cac25cb4ba0fe6bff52ebedbb65b77a179052a9d6037136ae93f42f4/regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e", size = 783051, upload-time = "2026-01-14T23:14:29.929Z" }, - { url = "https://files.pythonhosted.org/packages/1e/f4/6ed03e71dca6348a5188363a34f5e26ffd5db1404780288ff0d79513bce4/regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763", size = 854485, upload-time = "2026-01-14T23:14:31.366Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9a/8e8560bd78caded8eb137e3e47612430a05b9a772caf60876435192d670a/regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb", size = 762195, upload-time = "2026-01-14T23:14:32.802Z" }, - { url = "https://files.pythonhosted.org/packages/38/6b/61fc710f9aa8dfcd764fe27d37edfaa023b1a23305a0d84fccd5adb346ea/regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2", size = 845986, upload-time = "2026-01-14T23:14:34.898Z" }, - { url = "https://files.pythonhosted.org/packages/fd/2e/fbee4cb93f9d686901a7ca8d94285b80405e8c34fe4107f63ffcbfb56379/regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1", size = 788992, upload-time = "2026-01-14T23:14:37.116Z" }, - { url = "https://files.pythonhosted.org/packages/ed/14/3076348f3f586de64b1ab75a3fbabdaab7684af7f308ad43be7ef1849e55/regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569", size = 265893, upload-time = "2026-01-14T23:14:38.426Z" }, - { url = "https://files.pythonhosted.org/packages/0f/19/772cf8b5fc803f5c89ba85d8b1870a1ca580dc482aa030383a9289c82e44/regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7", size = 277840, upload-time = "2026-01-14T23:14:39.785Z" }, - { url = "https://files.pythonhosted.org/packages/78/84/d05f61142709474da3c0853222d91086d3e1372bcdab516c6fd8d80f3297/regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec", size = 270374, upload-time = "2026-01-14T23:14:41.592Z" }, - { url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" }, - { url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" }, - { url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" }, - { url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" }, - { url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" }, - { url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" }, - { url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" }, - { url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" }, - { url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" }, - { url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" }, - { url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" }, - { url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" }, - { url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" }, - { url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" }, - { url = "https://files.pythonhosted.org/packages/f8/2e/6870bb16e982669b674cce3ee9ff2d1d46ab80528ee6bcc20fb2292efb60/regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e", size = 489164, upload-time = "2026-01-14T23:15:13.962Z" }, - { url = "https://files.pythonhosted.org/packages/dc/67/9774542e203849b0286badf67199970a44ebdb0cc5fb739f06e47ada72f8/regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10", size = 291218, upload-time = "2026-01-14T23:15:15.647Z" }, - { url = "https://files.pythonhosted.org/packages/b2/87/b0cda79f22b8dee05f774922a214da109f9a4c0eca5da2c9d72d77ea062c/regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc", size = 288895, upload-time = "2026-01-14T23:15:17.788Z" }, - { url = "https://files.pythonhosted.org/packages/3b/6a/0041f0a2170d32be01ab981d6346c83a8934277d82c780d60b127331f264/regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599", size = 798680, upload-time = "2026-01-14T23:15:19.342Z" }, - { url = "https://files.pythonhosted.org/packages/58/de/30e1cfcdbe3e891324aa7568b7c968771f82190df5524fabc1138cb2d45a/regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae", size = 864210, upload-time = "2026-01-14T23:15:22.005Z" }, - { url = "https://files.pythonhosted.org/packages/64/44/4db2f5c5ca0ccd40ff052ae7b1e9731352fcdad946c2b812285a7505ca75/regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5", size = 912358, upload-time = "2026-01-14T23:15:24.569Z" }, - { url = "https://files.pythonhosted.org/packages/79/b6/e6a5665d43a7c42467138c8a2549be432bad22cbd206f5ec87162de74bd7/regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6", size = 803583, upload-time = "2026-01-14T23:15:26.526Z" }, - { url = "https://files.pythonhosted.org/packages/e7/53/7cd478222169d85d74d7437e74750005e993f52f335f7c04ff7adfda3310/regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788", size = 775782, upload-time = "2026-01-14T23:15:29.352Z" }, - { url = "https://files.pythonhosted.org/packages/ca/b5/75f9a9ee4b03a7c009fe60500fe550b45df94f0955ca29af16333ef557c5/regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714", size = 787978, upload-time = "2026-01-14T23:15:31.295Z" }, - { url = "https://files.pythonhosted.org/packages/72/b3/79821c826245bbe9ccbb54f6eadb7879c722fd3e0248c17bfc90bf54e123/regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d", size = 858550, upload-time = "2026-01-14T23:15:33.558Z" }, - { url = "https://files.pythonhosted.org/packages/4a/85/2ab5f77a1c465745bfbfcb3ad63178a58337ae8d5274315e2cc623a822fa/regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3", size = 763747, upload-time = "2026-01-14T23:15:35.206Z" }, - { url = "https://files.pythonhosted.org/packages/6d/84/c27df502d4bfe2873a3e3a7cf1bdb2b9cc10284d1a44797cf38bed790470/regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31", size = 850615, upload-time = "2026-01-14T23:15:37.523Z" }, - { url = "https://files.pythonhosted.org/packages/7d/b7/658a9782fb253680aa8ecb5ccbb51f69e088ed48142c46d9f0c99b46c575/regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3", size = 789951, upload-time = "2026-01-14T23:15:39.582Z" }, - { url = "https://files.pythonhosted.org/packages/fc/2a/5928af114441e059f15b2f63e188bd00c6529b3051c974ade7444b85fcda/regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f", size = 266275, upload-time = "2026-01-14T23:15:42.108Z" }, - { url = "https://files.pythonhosted.org/packages/4f/16/5bfbb89e435897bff28cf0352a992ca719d9e55ebf8b629203c96b6ce4f7/regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e", size = 277145, upload-time = "2026-01-14T23:15:44.244Z" }, - { url = "https://files.pythonhosted.org/packages/56/c1/a09ff7392ef4233296e821aec5f78c51be5e91ffde0d163059e50fd75835/regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337", size = 270411, upload-time = "2026-01-14T23:15:45.858Z" }, - { url = "https://files.pythonhosted.org/packages/3c/38/0cfd5a78e5c6db00e6782fdae70458f89850ce95baa5e8694ab91d89744f/regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be", size = 492068, upload-time = "2026-01-14T23:15:47.616Z" }, - { url = "https://files.pythonhosted.org/packages/50/72/6c86acff16cb7c959c4355826bbf06aad670682d07c8f3998d9ef4fee7cd/regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8", size = 292756, upload-time = "2026-01-14T23:15:49.307Z" }, - { url = "https://files.pythonhosted.org/packages/4e/58/df7fb69eadfe76526ddfce28abdc0af09ffe65f20c2c90932e89d705153f/regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd", size = 291114, upload-time = "2026-01-14T23:15:51.484Z" }, - { url = "https://files.pythonhosted.org/packages/ed/6c/a4011cd1cf96b90d2cdc7e156f91efbd26531e822a7fbb82a43c1016678e/regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a", size = 807524, upload-time = "2026-01-14T23:15:53.102Z" }, - { url = "https://files.pythonhosted.org/packages/1d/25/a53ffb73183f69c3e9f4355c4922b76d2840aee160af6af5fac229b6201d/regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93", size = 873455, upload-time = "2026-01-14T23:15:54.956Z" }, - { url = "https://files.pythonhosted.org/packages/66/0b/8b47fc2e8f97d9b4a851736f3890a5f786443aa8901061c55f24c955f45b/regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af", size = 915007, upload-time = "2026-01-14T23:15:57.041Z" }, - { url = "https://files.pythonhosted.org/packages/c2/fa/97de0d681e6d26fabe71968dbee06dd52819e9a22fdce5dac7256c31ed84/regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09", size = 812794, upload-time = "2026-01-14T23:15:58.916Z" }, - { url = "https://files.pythonhosted.org/packages/22/38/e752f94e860d429654aa2b1c51880bff8dfe8f084268258adf9151cf1f53/regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5", size = 781159, upload-time = "2026-01-14T23:16:00.817Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a7/d739ffaef33c378fc888302a018d7f81080393d96c476b058b8c64fd2b0d/regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794", size = 795558, upload-time = "2026-01-14T23:16:03.267Z" }, - { url = "https://files.pythonhosted.org/packages/3e/c4/542876f9a0ac576100fc73e9c75b779f5c31e3527576cfc9cb3009dcc58a/regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a", size = 868427, upload-time = "2026-01-14T23:16:05.646Z" }, - { url = "https://files.pythonhosted.org/packages/fc/0f/d5655bea5b22069e32ae85a947aa564912f23758e112cdb74212848a1a1b/regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80", size = 769939, upload-time = "2026-01-14T23:16:07.542Z" }, - { url = "https://files.pythonhosted.org/packages/20/06/7e18a4fa9d326daeda46d471a44ef94201c46eaa26dbbb780b5d92cbfdda/regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2", size = 854753, upload-time = "2026-01-14T23:16:10.395Z" }, - { url = "https://files.pythonhosted.org/packages/3b/67/dc8946ef3965e166f558ef3b47f492bc364e96a265eb4a2bb3ca765c8e46/regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60", size = 799559, upload-time = "2026-01-14T23:16:12.347Z" }, - { url = "https://files.pythonhosted.org/packages/a5/61/1bba81ff6d50c86c65d9fd84ce9699dd106438ee4cdb105bf60374ee8412/regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952", size = 268879, upload-time = "2026-01-14T23:16:14.049Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5e/cef7d4c5fb0ea3ac5c775fd37db5747f7378b29526cc83f572198924ff47/regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10", size = 280317, upload-time = "2026-01-14T23:16:15.718Z" }, - { url = "https://files.pythonhosted.org/packages/b4/52/4317f7a5988544e34ab57b4bde0f04944c4786128c933fb09825924d3e82/regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829", size = 271551, upload-time = "2026-01-14T23:16:17.533Z" }, - { url = "https://files.pythonhosted.org/packages/52/0a/47fa888ec7cbbc7d62c5f2a6a888878e76169170ead271a35239edd8f0e8/regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac", size = 489170, upload-time = "2026-01-14T23:16:19.835Z" }, - { url = "https://files.pythonhosted.org/packages/ac/c4/d000e9b7296c15737c9301708e9e7fbdea009f8e93541b6b43bdb8219646/regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6", size = 291146, upload-time = "2026-01-14T23:16:21.541Z" }, - { url = "https://files.pythonhosted.org/packages/f9/b6/921cc61982e538682bdf3bdf5b2c6ab6b34368da1f8e98a6c1ddc503c9cf/regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2", size = 288986, upload-time = "2026-01-14T23:16:23.381Z" }, - { url = "https://files.pythonhosted.org/packages/ca/33/eb7383dde0bbc93f4fb9d03453aab97e18ad4024ac7e26cef8d1f0a2cff0/regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846", size = 799098, upload-time = "2026-01-14T23:16:25.088Z" }, - { url = "https://files.pythonhosted.org/packages/27/56/b664dccae898fc8d8b4c23accd853f723bde0f026c747b6f6262b688029c/regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b", size = 864980, upload-time = "2026-01-14T23:16:27.297Z" }, - { url = "https://files.pythonhosted.org/packages/16/40/0999e064a170eddd237bae9ccfcd8f28b3aa98a38bf727a086425542a4fc/regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e", size = 911607, upload-time = "2026-01-14T23:16:29.235Z" }, - { url = "https://files.pythonhosted.org/packages/07/78/c77f644b68ab054e5a674fb4da40ff7bffb2c88df58afa82dbf86573092d/regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde", size = 803358, upload-time = "2026-01-14T23:16:31.369Z" }, - { url = "https://files.pythonhosted.org/packages/27/31/d4292ea8566eaa551fafc07797961c5963cf5235c797cc2ae19b85dfd04d/regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5", size = 775833, upload-time = "2026-01-14T23:16:33.141Z" }, - { url = "https://files.pythonhosted.org/packages/ce/b2/cff3bf2fea4133aa6fb0d1e370b37544d18c8350a2fa118c7e11d1db0e14/regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34", size = 788045, upload-time = "2026-01-14T23:16:35.005Z" }, - { url = "https://files.pythonhosted.org/packages/8d/99/2cb9b69045372ec877b6f5124bda4eb4253bc58b8fe5848c973f752bc52c/regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75", size = 859374, upload-time = "2026-01-14T23:16:36.919Z" }, - { url = "https://files.pythonhosted.org/packages/09/16/710b0a5abe8e077b1729a562d2f297224ad079f3a66dce46844c193416c8/regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e", size = 763940, upload-time = "2026-01-14T23:16:38.685Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d1/7585c8e744e40eb3d32f119191969b91de04c073fca98ec14299041f6e7e/regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160", size = 850112, upload-time = "2026-01-14T23:16:40.646Z" }, - { url = "https://files.pythonhosted.org/packages/af/d6/43e1dd85df86c49a347aa57c1f69d12c652c7b60e37ec162e3096194a278/regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1", size = 789586, upload-time = "2026-01-14T23:16:42.799Z" }, - { url = "https://files.pythonhosted.org/packages/93/38/77142422f631e013f316aaae83234c629555729a9fbc952b8a63ac91462a/regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1", size = 271691, upload-time = "2026-01-14T23:16:44.671Z" }, - { url = "https://files.pythonhosted.org/packages/4a/a9/ab16b4649524ca9e05213c1cdbb7faa85cc2aa90a0230d2f796cbaf22736/regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903", size = 280422, upload-time = "2026-01-14T23:16:46.607Z" }, - { url = "https://files.pythonhosted.org/packages/be/2a/20fd057bf3521cb4791f69f869635f73e0aaf2b9ad2d260f728144f9047c/regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705", size = 273467, upload-time = "2026-01-14T23:16:48.967Z" }, - { url = "https://files.pythonhosted.org/packages/ad/77/0b1e81857060b92b9cad239104c46507dd481b3ff1fa79f8e7f865aae38a/regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8", size = 492073, upload-time = "2026-01-14T23:16:51.154Z" }, - { url = "https://files.pythonhosted.org/packages/70/f3/f8302b0c208b22c1e4f423147e1913fd475ddd6230565b299925353de644/regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf", size = 292757, upload-time = "2026-01-14T23:16:53.08Z" }, - { url = "https://files.pythonhosted.org/packages/bf/f0/ef55de2460f3b4a6da9d9e7daacd0cb79d4ef75c64a2af316e68447f0df0/regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d", size = 291122, upload-time = "2026-01-14T23:16:55.383Z" }, - { url = "https://files.pythonhosted.org/packages/cf/55/bb8ccbacabbc3a11d863ee62a9f18b160a83084ea95cdfc5d207bfc3dd75/regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84", size = 807761, upload-time = "2026-01-14T23:16:57.251Z" }, - { url = "https://files.pythonhosted.org/packages/8f/84/f75d937f17f81e55679a0509e86176e29caa7298c38bd1db7ce9c0bf6075/regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df", size = 873538, upload-time = "2026-01-14T23:16:59.349Z" }, - { url = "https://files.pythonhosted.org/packages/b8/d9/0da86327df70349aa8d86390da91171bd3ca4f0e7c1d1d453a9c10344da3/regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434", size = 915066, upload-time = "2026-01-14T23:17:01.607Z" }, - { url = "https://files.pythonhosted.org/packages/2a/5e/f660fb23fc77baa2a61aa1f1fe3a4eea2bbb8a286ddec148030672e18834/regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a", size = 812938, upload-time = "2026-01-14T23:17:04.366Z" }, - { url = "https://files.pythonhosted.org/packages/69/33/a47a29bfecebbbfd1e5cd3f26b28020a97e4820f1c5148e66e3b7d4b4992/regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10", size = 781314, upload-time = "2026-01-14T23:17:06.378Z" }, - { url = "https://files.pythonhosted.org/packages/65/ec/7ec2bbfd4c3f4e494a24dec4c6943a668e2030426b1b8b949a6462d2c17b/regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac", size = 795652, upload-time = "2026-01-14T23:17:08.521Z" }, - { url = "https://files.pythonhosted.org/packages/46/79/a5d8651ae131fe27d7c521ad300aa7f1c7be1dbeee4d446498af5411b8a9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea", size = 868550, upload-time = "2026-01-14T23:17:10.573Z" }, - { url = "https://files.pythonhosted.org/packages/06/b7/25635d2809664b79f183070786a5552dd4e627e5aedb0065f4e3cf8ee37d/regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e", size = 769981, upload-time = "2026-01-14T23:17:12.871Z" }, - { url = "https://files.pythonhosted.org/packages/16/8b/fc3fcbb2393dcfa4a6c5ffad92dc498e842df4581ea9d14309fcd3c55fb9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521", size = 854780, upload-time = "2026-01-14T23:17:14.837Z" }, - { url = "https://files.pythonhosted.org/packages/d0/38/dde117c76c624713c8a2842530be9c93ca8b606c0f6102d86e8cd1ce8bea/regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db", size = 799778, upload-time = "2026-01-14T23:17:17.369Z" }, - { url = "https://files.pythonhosted.org/packages/e3/0d/3a6cfa9ae99606afb612d8fb7a66b245a9d5ff0f29bb347c8a30b6ad561b/regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e", size = 274667, upload-time = "2026-01-14T23:17:19.301Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b2/297293bb0742fd06b8d8e2572db41a855cdf1cae0bf009b1cb74fe07e196/regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf", size = 284386, upload-time = "2026-01-14T23:17:21.231Z" }, - { url = "https://files.pythonhosted.org/packages/95/e4/a3b9480c78cf8ee86626cb06f8d931d74d775897d44201ccb813097ae697/regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70", size = 274837, upload-time = "2026-01-14T23:17:23.146Z" }, -] - -[[package]] -name = "requests" -version = "2.32.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, -] - -[[package]] -name = "requests-toolbelt" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, -] - -[[package]] -name = "rsa" -version = "4.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, -] - -[[package]] -name = "ruff" -version = "0.14.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" }, - { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" }, - { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" }, - { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" }, - { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" }, - { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" }, - { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" }, - { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" }, - { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" }, - { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" }, - { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" }, - { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" }, - { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024, upload-time = "2026-01-15T20:14:43.647Z" }, - { url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887, upload-time = "2026-01-15T20:15:01.48Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, -] - -[[package]] -name = "socksio" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/48a7d9495be3d1c651198fd99dbb6ce190e2274d0f28b9051307bdec6b85/socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac", size = 19055, upload-time = "2020-04-17T15:50:34.664Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3", size = 12763, upload-time = "2020-04-17T15:50:31.878Z" }, -] - -[[package]] -name = "sounddevice" -version = "0.5.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a8/fd/a1c8500a0113d122f2343f827895de2083307bbcbb706da59078bcdc3b75/sounddevice-0.5.4.tar.gz", hash = "sha256:4286fc031606dccaaf5d0e5b0accdbe266a5bc66271b7ec5a985a7f4a9d09332", size = 143045, upload-time = "2026-01-21T21:09:27.671Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/a9/a9a176cf22c591dd34039ed046156306f6b5988812f86e62c5d4653ac909/sounddevice-0.5.4-py3-none-any.whl", hash = "sha256:f6f0a8fb835b677f326a26232d56d6e18c5c84aa985b275fb3701533d3e333b7", size = 32769, upload-time = "2026-01-21T21:09:19.778Z" }, - { url = "https://files.pythonhosted.org/packages/32/2b/891b11f3cbff1da4b17c311f0ce6d1c66c9fad46fd14be55a6b6f86f32ed/sounddevice-0.5.4-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl", hash = "sha256:e340ae585e2859d17544c70dda3c352741ff6ffddcd94d7d9386150eba9db935", size = 108515, upload-time = "2026-01-21T21:09:21.412Z" }, - { url = "https://files.pythonhosted.org/packages/a4/a5/34a9a43f0dbc547da0b01c03df50fb36c4023a77a166f18985b364f72528/sounddevice-0.5.4-py3-none-win32.whl", hash = "sha256:4fe3be129a02e902606ee4512f642bcdbf35e122fd8fce53154cee9dbc919bf9", size = 317725, upload-time = "2026-01-21T21:09:23.051Z" }, - { url = "https://files.pythonhosted.org/packages/21/ff/bade5521ed9598b98168a143ccb24f4041f85d3562eb2b9ab6fce9b60d30/sounddevice-0.5.4-py3-none-win_amd64.whl", hash = "sha256:7e1b530cc9f6a23d045d2577c088ec9817d1518b55c8515f783683a329cec358", size = 365283, upload-time = "2026-01-21T21:09:24.398Z" }, - { url = "https://files.pythonhosted.org/packages/15/8a/f6347d06ba647a2bced7945320db564b1d2dfe16bc1110a42f03945eb4e6/sounddevice-0.5.4-py3-none-win_arm64.whl", hash = "sha256:fcddcf99d7a533d0a3b1449c00d679ff075c1561038bdd05533a2fec3dc314e8", size = 317073, upload-time = "2026-01-21T21:09:25.944Z" }, -] - -[[package]] -name = "sqlalchemy" -version = "2.0.46" -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/06/aa/9ce0f3e7a9829ead5c8ce549392f33a12c4555a6c0609bb27d882e9c7ddf/sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7", size = 9865393, upload-time = "2026-01-21T18:03:45.119Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/ac/b42ad16800d0885105b59380ad69aad0cce5a65276e269ce2729a2343b6a/sqlalchemy-2.0.46-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:261c4b1f101b4a411154f1da2b76497d73abbfc42740029205d4d01fa1052684", size = 2154851, upload-time = "2026-01-21T18:27:30.54Z" }, - { url = "https://files.pythonhosted.org/packages/a0/60/d8710068cb79f64d002ebed62a7263c00c8fd95f4ebd4b5be8f7ca93f2bc/sqlalchemy-2.0.46-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:181903fe8c1b9082995325f1b2e84ac078b1189e2819380c2303a5f90e114a62", size = 3311241, upload-time = "2026-01-21T18:32:33.45Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/20c71487c7219ab3aa7421c7c62d93824c97c1460f2e8bb72404b0192d13/sqlalchemy-2.0.46-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:590be24e20e2424a4c3c1b0835e9405fa3d0af5823a1a9fc02e5dff56471515f", size = 3310741, upload-time = "2026-01-21T18:44:57.887Z" }, - { url = "https://files.pythonhosted.org/packages/65/80/d26d00b3b249ae000eee4db206fcfc564bf6ca5030e4747adf451f4b5108/sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7568fe771f974abadce52669ef3a03150ff03186d8eb82613bc8adc435a03f01", size = 3263116, upload-time = "2026-01-21T18:32:35.044Z" }, - { url = "https://files.pythonhosted.org/packages/da/ee/74dda7506640923821340541e8e45bd3edd8df78664f1f2e0aae8077192b/sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf7e1e78af38047e08836d33502c7a278915698b7c2145d045f780201679999", size = 3285327, upload-time = "2026-01-21T18:44:59.254Z" }, - { url = "https://files.pythonhosted.org/packages/9f/25/6dcf8abafff1389a21c7185364de145107b7394ecdcb05233815b236330d/sqlalchemy-2.0.46-cp311-cp311-win32.whl", hash = "sha256:9d80ea2ac519c364a7286e8d765d6cd08648f5b21ca855a8017d9871f075542d", size = 2114564, upload-time = "2026-01-21T18:33:15.85Z" }, - { url = "https://files.pythonhosted.org/packages/93/5f/e081490f8523adc0088f777e4ebad3cac21e498ec8a3d4067074e21447a1/sqlalchemy-2.0.46-cp311-cp311-win_amd64.whl", hash = "sha256:585af6afe518732d9ccd3aea33af2edaae4a7aa881af5d8f6f4fe3a368699597", size = 2139233, upload-time = "2026-01-21T18:33:17.528Z" }, - { url = "https://files.pythonhosted.org/packages/b6/35/d16bfa235c8b7caba3730bba43e20b1e376d2224f407c178fbf59559f23e/sqlalchemy-2.0.46-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a9a72b0da8387f15d5810f1facca8f879de9b85af8c645138cba61ea147968c", size = 2153405, upload-time = "2026-01-21T19:05:54.143Z" }, - { url = "https://files.pythonhosted.org/packages/06/6c/3192e24486749862f495ddc6584ed730c0c994a67550ec395d872a2ad650/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2347c3f0efc4de367ba00218e0ae5c4ba2306e47216ef80d6e31761ac97cb0b9", size = 3334702, upload-time = "2026-01-21T18:46:45.384Z" }, - { url = "https://files.pythonhosted.org/packages/ea/a2/b9f33c8d68a3747d972a0bb758c6b63691f8fb8a49014bc3379ba15d4274/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9094c8b3197db12aa6f05c51c05daaad0a92b8c9af5388569847b03b1007fb1b", size = 3347664, upload-time = "2026-01-21T18:40:09.979Z" }, - { url = "https://files.pythonhosted.org/packages/aa/d2/3e59e2a91eaec9db7e8dc6b37b91489b5caeb054f670f32c95bcba98940f/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37fee2164cf21417478b6a906adc1a91d69ae9aba8f9533e67ce882f4bb1de53", size = 3277372, upload-time = "2026-01-21T18:46:47.168Z" }, - { url = "https://files.pythonhosted.org/packages/dd/dd/67bc2e368b524e2192c3927b423798deda72c003e73a1e94c21e74b20a85/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b1e14b2f6965a685c7128bd315e27387205429c2e339eeec55cb75ca4ab0ea2e", size = 3312425, upload-time = "2026-01-21T18:40:11.548Z" }, - { url = "https://files.pythonhosted.org/packages/43/82/0ecd68e172bfe62247e96cb47867c2d68752566811a4e8c9d8f6e7c38a65/sqlalchemy-2.0.46-cp312-cp312-win32.whl", hash = "sha256:412f26bb4ba942d52016edc8d12fb15d91d3cd46b0047ba46e424213ad407bcb", size = 2113155, upload-time = "2026-01-21T18:42:49.748Z" }, - { url = "https://files.pythonhosted.org/packages/bc/2a/2821a45742073fc0331dc132552b30de68ba9563230853437cac54b2b53e/sqlalchemy-2.0.46-cp312-cp312-win_amd64.whl", hash = "sha256:ea3cd46b6713a10216323cda3333514944e510aa691c945334713fca6b5279ff", size = 2140078, upload-time = "2026-01-21T18:42:51.197Z" }, - { url = "https://files.pythonhosted.org/packages/b3/4b/fa7838fe20bb752810feed60e45625a9a8b0102c0c09971e2d1d95362992/sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00", size = 2150268, upload-time = "2026-01-21T19:05:56.621Z" }, - { url = "https://files.pythonhosted.org/packages/46/c1/b34dccd712e8ea846edf396e00973dda82d598cb93762e55e43e6835eba9/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2", size = 3276511, upload-time = "2026-01-21T18:46:49.022Z" }, - { url = "https://files.pythonhosted.org/packages/96/48/a04d9c94753e5d5d096c628c82a98c4793b9c08ca0e7155c3eb7d7db9f24/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee", size = 3292881, upload-time = "2026-01-21T18:40:13.089Z" }, - { url = "https://files.pythonhosted.org/packages/be/f4/06eda6e91476f90a7d8058f74311cb65a2fb68d988171aced81707189131/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad", size = 3224559, upload-time = "2026-01-21T18:46:50.974Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a2/d2af04095412ca6345ac22b33b89fe8d6f32a481e613ffcb2377d931d8d0/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e", size = 3262728, upload-time = "2026-01-21T18:40:14.883Z" }, - { url = "https://files.pythonhosted.org/packages/31/48/1980c7caa5978a3b8225b4d230e69a2a6538a3562b8b31cea679b6933c83/sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f", size = 2111295, upload-time = "2026-01-21T18:42:52.366Z" }, - { url = "https://files.pythonhosted.org/packages/2d/54/f8d65bbde3d877617c4720f3c9f60e99bb7266df0d5d78b6e25e7c149f35/sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef", size = 2137076, upload-time = "2026-01-21T18:42:53.924Z" }, - { url = "https://files.pythonhosted.org/packages/56/ba/9be4f97c7eb2b9d5544f2624adfc2853e796ed51d2bb8aec90bc94b7137e/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10", size = 3556533, upload-time = "2026-01-21T18:33:06.636Z" }, - { url = "https://files.pythonhosted.org/packages/20/a6/b1fc6634564dbb4415b7ed6419cdfeaadefd2c39cdab1e3aa07a5f2474c2/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764", size = 3523208, upload-time = "2026-01-21T18:45:08.436Z" }, - { url = "https://files.pythonhosted.org/packages/a1/d8/41e0bdfc0f930ff236f86fccd12962d8fa03713f17ed57332d38af6a3782/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b", size = 3464292, upload-time = "2026-01-21T18:33:08.208Z" }, - { url = "https://files.pythonhosted.org/packages/f0/8b/9dcbec62d95bea85f5ecad9b8d65b78cc30fb0ffceeb3597961f3712549b/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447", size = 3473497, upload-time = "2026-01-21T18:45:10.552Z" }, - { url = "https://files.pythonhosted.org/packages/e9/f8/5ecdfc73383ec496de038ed1614de9e740a82db9ad67e6e4514ebc0708a3/sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada", size = 2152079, upload-time = "2026-01-21T19:05:58.477Z" }, - { url = "https://files.pythonhosted.org/packages/e5/bf/eba3036be7663ce4d9c050bc3d63794dc29fbe01691f2bf5ccb64e048d20/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366", size = 3272216, upload-time = "2026-01-21T18:46:52.634Z" }, - { url = "https://files.pythonhosted.org/packages/05/45/1256fb597bb83b58a01ddb600c59fe6fdf0e5afe333f0456ed75c0f8d7bd/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d", size = 3277208, upload-time = "2026-01-21T18:40:16.38Z" }, - { url = "https://files.pythonhosted.org/packages/d9/a0/2053b39e4e63b5d7ceb3372cface0859a067c1ddbd575ea7e9985716f771/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e", size = 3221994, upload-time = "2026-01-21T18:46:54.622Z" }, - { url = "https://files.pythonhosted.org/packages/1e/87/97713497d9502553c68f105a1cb62786ba1ee91dea3852ae4067ed956a50/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf", size = 3243990, upload-time = "2026-01-21T18:40:18.253Z" }, - { url = "https://files.pythonhosted.org/packages/a8/87/5d1b23548f420ff823c236f8bea36b1a997250fd2f892e44a3838ca424f4/sqlalchemy-2.0.46-cp314-cp314-win32.whl", hash = "sha256:70ed2830b169a9960193f4d4322d22be5c0925357d82cbf485b3369893350908", size = 2114215, upload-time = "2026-01-21T18:42:55.232Z" }, - { url = "https://files.pythonhosted.org/packages/3a/20/555f39cbcf0c10cf452988b6a93c2a12495035f68b3dbd1a408531049d31/sqlalchemy-2.0.46-cp314-cp314-win_amd64.whl", hash = "sha256:3c32e993bc57be6d177f7d5d31edb93f30726d798ad86ff9066d75d9bf2e0b6b", size = 2139867, upload-time = "2026-01-21T18:42:56.474Z" }, - { url = "https://files.pythonhosted.org/packages/3e/f0/f96c8057c982d9d8a7a68f45d69c674bc6f78cad401099692fe16521640a/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa", size = 3561202, upload-time = "2026-01-21T18:33:10.337Z" }, - { url = "https://files.pythonhosted.org/packages/d7/53/3b37dda0a5b137f21ef608d8dfc77b08477bab0fe2ac9d3e0a66eaeab6fc/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863", size = 3526296, upload-time = "2026-01-21T18:45:12.657Z" }, - { url = "https://files.pythonhosted.org/packages/33/75/f28622ba6dde79cd545055ea7bd4062dc934e0621f7b3be2891f8563f8de/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede", size = 3470008, upload-time = "2026-01-21T18:33:11.725Z" }, - { url = "https://files.pythonhosted.org/packages/a9/42/4afecbbc38d5e99b18acef446453c76eec6fbd03db0a457a12a056836e22/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330", size = 3476137, upload-time = "2026-01-21T18:45:15.001Z" }, - { url = "https://files.pythonhosted.org/packages/fc/a1/9c4efa03300926601c19c18582531b45aededfb961ab3c3585f1e24f120b/sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e", size = 1937882, upload-time = "2026-01-21T18:22:10.456Z" }, -] - -[[package]] -name = "starlette" -version = "0.50.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, -] - -[[package]] -name = "tabulate" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, -] - -[[package]] -name = "tenacity" -version = "9.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, -] - -[[package]] -name = "tiktoken" -version = "0.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "regex" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, - { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, - { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, - { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, - { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, - { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, - { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, - { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, - { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, - { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, - { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, - { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, - { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, - { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, - { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, - { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, - { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, - { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, - { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, - { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, - { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, - { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, - { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, - { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, - { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, - { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, - { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, - { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, - { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, - { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, - { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, - { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, - { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, -] - -[[package]] -name = "tqdm" -version = "4.67.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, -] - -[[package]] -name = "urllib3" -version = "2.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, -] - -[[package]] -name = "uuid-utils" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/7c/3a926e847516e67bc6838634f2e54e24381105b4e80f9338dc35cca0086b/uuid_utils-0.14.0.tar.gz", hash = "sha256:fc5bac21e9933ea6c590433c11aa54aaca599f690c08069e364eb13a12f670b4", size = 22072, upload-time = "2026-01-20T20:37:15.729Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/42/42d003f4a99ddc901eef2fd41acb3694163835e037fb6dde79ad68a72342/uuid_utils-0.14.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f6695c0bed8b18a904321e115afe73b34444bc8451d0ce3244a1ec3b84deb0e5", size = 601786, upload-time = "2026-01-20T20:37:09.843Z" }, - { url = "https://files.pythonhosted.org/packages/96/e6/775dfb91f74b18f7207e3201eb31ee666d286579990dc69dd50db2d92813/uuid_utils-0.14.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4f0a730bbf2d8bb2c11b93e1005e91769f2f533fa1125ed1f00fd15b6fcc732b", size = 303943, upload-time = "2026-01-20T20:37:18.767Z" }, - { url = "https://files.pythonhosted.org/packages/17/82/ea5f5e85560b08a1f30cdc65f75e76494dc7aba9773f679e7eaa27370229/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40ce3fd1a4fdedae618fc3edc8faf91897012469169d600133470f49fd699ed3", size = 340467, upload-time = "2026-01-20T20:37:11.794Z" }, - { url = "https://files.pythonhosted.org/packages/ca/33/54b06415767f4569882e99b6470c6c8eeb97422686a6d432464f9967fd91/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:09ae4a98416a440e78f7d9543d11b11cae4bab538b7ed94ec5da5221481748f2", size = 346333, upload-time = "2026-01-20T20:37:12.818Z" }, - { url = "https://files.pythonhosted.org/packages/cb/10/a6bce636b8f95e65dc84bf4a58ce8205b8e0a2a300a38cdbc83a3f763d27/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:971e8c26b90d8ae727e7f2ac3ee23e265971d448b3672882f2eb44828b2b8c3e", size = 470859, upload-time = "2026-01-20T20:37:01.512Z" }, - { url = "https://files.pythonhosted.org/packages/8a/27/84121c51ea72f013f0e03d0886bcdfa96b31c9b83c98300a7bd5cc4fa191/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5cde1fa82804a8f9d2907b7aec2009d440062c63f04abbdb825fce717a5e860", size = 341988, upload-time = "2026-01-20T20:37:22.881Z" }, - { url = "https://files.pythonhosted.org/packages/90/a4/01c1c7af5e6a44f20b40183e8dac37d6ed83e7dc9e8df85370a15959b804/uuid_utils-0.14.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7343862a2359e0bd48a7f3dfb5105877a1728677818bb694d9f40703264a2db", size = 365784, upload-time = "2026-01-20T20:37:10.808Z" }, - { url = "https://files.pythonhosted.org/packages/04/f0/65ee43ec617b8b6b1bf2a5aecd56a069a08cca3d9340c1de86024331bde3/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c51e4818fdb08ccec12dc7083a01f49507b4608770a0ab22368001685d59381b", size = 523750, upload-time = "2026-01-20T20:37:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/95/d3/6bf503e3f135a5dfe705a65e6f89f19bccd55ac3fb16cb5d3ec5ba5388b8/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:181bbcccb6f93d80a8504b5bd47b311a1c31395139596edbc47b154b0685b533", size = 615818, upload-time = "2026-01-20T20:37:21.816Z" }, - { url = "https://files.pythonhosted.org/packages/df/6c/99937dd78d07f73bba831c8dc9469dfe4696539eba2fc269ae1b92752f9e/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:5c8ae96101c3524ba8dbf762b6f05e9e9d896544786c503a727c5bf5cb9af1a7", size = 580831, upload-time = "2026-01-20T20:37:19.691Z" }, - { url = "https://files.pythonhosted.org/packages/44/fa/bbc9e2c25abd09a293b9b097a0d8fc16acd6a92854f0ec080f1ea7ad8bb3/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00ac3c6edfdaff7e1eed041f4800ae09a3361287be780d7610a90fdcde9befdc", size = 546333, upload-time = "2026-01-20T20:37:03.117Z" }, - { url = "https://files.pythonhosted.org/packages/e7/9b/e5e99b324b1b5f0c62882230455786df0bc66f67eff3b452447e703f45d2/uuid_utils-0.14.0-cp39-abi3-win32.whl", hash = "sha256:ec2fd80adf8e0e6589d40699e6f6df94c93edcc16dd999be0438dd007c77b151", size = 177319, upload-time = "2026-01-20T20:37:04.208Z" }, - { url = "https://files.pythonhosted.org/packages/d3/28/2c7d417ea483b6ff7820c948678fdf2ac98899dc7e43bb15852faa95acaf/uuid_utils-0.14.0-cp39-abi3-win_amd64.whl", hash = "sha256:efe881eb43a5504fad922644cb93d725fd8a6a6d949bd5a4b4b7d1a1587c7fd1", size = 182566, upload-time = "2026-01-20T20:37:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/b8/86/49e4bdda28e962fbd7266684171ee29b3d92019116971d58783e51770745/uuid_utils-0.14.0-cp39-abi3-win_arm64.whl", hash = "sha256:32b372b8fd4ebd44d3a219e093fe981af4afdeda2994ee7db208ab065cfcd080", size = 182809, upload-time = "2026-01-20T20:37:05.139Z" }, - { url = "https://files.pythonhosted.org/packages/f1/03/1f1146e32e94d1f260dfabc81e1649102083303fb4ad549775c943425d9a/uuid_utils-0.14.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:762e8d67992ac4d2454e24a141a1c82142b5bde10409818c62adbe9924ebc86d", size = 587430, upload-time = "2026-01-20T20:37:24.998Z" }, - { url = "https://files.pythonhosted.org/packages/87/ba/d5a7469362594d885fd9219fe9e851efbe65101d3ef1ef25ea321d7ce841/uuid_utils-0.14.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:40be5bf0b13aa849d9062abc86c198be6a25ff35316ce0b89fc25f3bac6d525e", size = 298106, upload-time = "2026-01-20T20:37:23.896Z" }, - { url = "https://files.pythonhosted.org/packages/8a/11/3dafb2a5502586f59fd49e93f5802cd5face82921b3a0f3abb5f357cb879/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:191a90a6f3940d1b7322b6e6cceff4dd533c943659e0a15f788674407856a515", size = 333423, upload-time = "2026-01-20T20:37:17.828Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f2/c8987663f0cdcf4d717a36d85b5db2a5589df0a4e129aa10f16f4380ef48/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4aa4525f4ad82f9d9c842f9a3703f1539c1808affbaec07bb1b842f6b8b96aa5", size = 338659, upload-time = "2026-01-20T20:37:14.286Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c8/929d81665d83f0b2ffaecb8e66c3091a50f62c7cb5b65e678bd75a96684e/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdbd82ff20147461caefc375551595ecf77ebb384e46267f128aca45a0f2cdfc", size = 467029, upload-time = "2026-01-20T20:37:08.277Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a0/27d7daa1bfed7163f4ccaf52d7d2f4ad7bb1002a85b45077938b91ee584f/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff57e8a5d540006ce73cf0841a643d445afe78ba12e75ac53a95ca2924a56be", size = 333298, upload-time = "2026-01-20T20:37:07.271Z" }, - { url = "https://files.pythonhosted.org/packages/63/d4/acad86ce012b42ce18a12f31ee2aa3cbeeb98664f865f05f68c882945913/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3fd9112ca96978361201e669729784f26c71fecc9c13a7f8a07162c31bd4d1e2", size = 359217, upload-time = "2026-01-20T20:36:59.687Z" }, -] - -[[package]] -name = "uvicorn" -version = "0.40.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, -] - -[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/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, - { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, - { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, - { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, - { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, - { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, - { 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 = "virtualenv" -version = "20.36.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "distlib" }, - { name = "filelock" }, - { name = "platformdirs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, -] - -[[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/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, - { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, - { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, - { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, - { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, - { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, - { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, - { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, - { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, - { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, - { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, - { 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" }, - { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, - { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, - { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, -] - -[[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/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, - { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, - { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, - { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, - { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, - { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, - { 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/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, - { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, - { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, - { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, - { 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" }, -] - -[[package]] -name = "xxhash" -version = "3.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844, upload-time = "2025-10-02T14:34:14.037Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" }, - { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665, upload-time = "2025-10-02T14:34:16.541Z" }, - { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" }, - { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" }, - { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" }, - { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" }, - { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654, upload-time = "2025-10-02T14:34:25.644Z" }, - { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" }, - { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" }, - { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574, upload-time = "2025-10-02T14:34:31.028Z" }, - { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481, upload-time = "2025-10-02T14:34:32.062Z" }, - { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861, upload-time = "2025-10-02T14:34:33.555Z" }, - { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, - { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, - { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, - { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, - { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, - { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, - { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, - { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" }, - { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, - { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, - { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, - { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, - { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, - { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, - { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" }, - { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" }, - { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, - { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, - { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, - { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, - { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, - { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" }, - { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, - { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, - { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, - { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, - { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" }, - { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, - { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, - { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, - { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" }, - { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, - { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, - { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, - { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, - { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, - { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, - { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" }, - { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" }, - { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" }, - { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" }, - { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" }, - { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" }, - { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" }, - { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" }, - { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" }, - { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" }, - { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" }, - { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" }, - { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" }, - { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" }, - { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" }, - { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" }, - { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" }, - { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" }, - { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" }, - { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" }, - { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, - { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, - { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, - { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" }, - { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" }, - { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" }, -] - -[[package]] -name = "yarl" -version = "1.22.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "multidict" }, - { name = "propcache" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, - { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, - { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, - { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, - { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, - { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, - { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, - { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, - { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, - { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, - { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, - { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, - { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, - { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, - { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, - { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, - { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, - { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, - { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, - { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, - { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, - { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, - { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, - { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, - { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, - { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, - { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, - { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, - { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, - { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, - { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, - { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, - { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, - { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, - { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, - { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, - { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, - { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, - { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, - { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, - { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, - { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, - { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, - { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, - { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, - { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, - { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, - { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, - { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, - { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, - { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, - { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, - { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, - { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, - { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, - { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, - { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, - { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, - { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, - { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, - { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, - { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, - { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, - { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, - { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, - { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, - { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, - { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, - { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, - { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, - { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, - { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, - { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, - { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, - { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, - { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, - { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, - { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, - { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, - { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, - { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, - { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, - { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, - { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, -] - -[[package]] -name = "zstandard" -version = "0.25.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, - { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, - { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" }, - { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, - { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, - { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, - { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, - { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, - { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, - { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, - { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, - { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, - { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" }, - { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" }, - { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, - { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, - { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, - { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, - { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, - { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, - { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, - { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, - { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, - { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, - { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, - { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, - { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, - { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, - { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, - { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, - { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, - { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, - { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, - { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, - { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, - { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, - { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, - { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, - { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, - { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, - { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, - { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, - { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, - { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, - { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, - { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, - { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, - { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, - { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, - { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, - { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, - { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, - { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, - { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, - { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, - { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, - { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, -] diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 28b8f37a..a908f696 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -1,51 +1,34 @@ -# Docker Compose for multi-container deployment -# Run from deploy/ directory: docker compose up -d - services: - nginx: - image: nginx:alpine + app: + build: + context: .. + dockerfile: Dockerfile + container_name: momshell + restart: unless-stopped + env_file: ../.env ports: - - "7860:80" - volumes: - - ./nginx.conf:/etc/nginx/nginx.conf:ro + - "80:80" depends_on: - - backend - - frontend - restart: unless-stopped + postgres: + condition: service_healthy - backend: - build: - context: ../backend - dockerfile: Dockerfile - expose: - - "8000" + postgres: + image: postgres:16-alpine + container_name: momshell-postgres + restart: unless-stopped environment: - - PYTHONUNBUFFERED=1 - - PORT=8000 - env_file: - - ../.env + POSTGRES_USER: ${POSTGRES_USER:-momshell} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-momshell} + POSTGRES_DB: ${POSTGRES_DB:-momshell} volumes: - - momshell_data:/app/data - restart: unless-stopped - healthcheck: - test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] - interval: 30s - timeout: 10s - retries: 3 - - frontend: - build: - context: ../frontend - dockerfile: Dockerfile - args: - - NEXT_PUBLIC_API_URL= + - pgdata:/var/lib/postgresql/data expose: - - "7860" - environment: - - NEXT_PUBLIC_API_URL=http://backend:8000 - depends_on: - - backend - restart: unless-stopped + - "5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-momshell}"] + interval: 5s + timeout: 3s + retries: 5 volumes: - momshell_data: + pgdata: diff --git a/deploy/entrypoint.sh b/deploy/entrypoint.sh new file mode 100755 index 00000000..e080c9a8 --- /dev/null +++ b/deploy/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -e + +# Start backend in background +/app/server & + +# Start nginx in foreground +exec nginx -g "daemon off;" diff --git a/deploy/nginx.conf b/deploy/nginx.conf deleted file mode 100644 index d334244a..00000000 --- a/deploy/nginx.conf +++ /dev/null @@ -1,63 +0,0 @@ -events { - worker_connections 1024; -} - -http { - upstream backend { - server backend:8000; - } - - upstream frontend { - server frontend:7860; - } - - server { - listen 80; - server_name _; - - client_max_body_size 50M; - - # API requests -> backend - location /api/ { - proxy_pass http://backend/api/; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 86400; - } - - # WebSocket for real-time features - location /ws/ { - proxy_pass http://backend/ws/; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_read_timeout 86400; - } - - # Health check endpoint - location /health { - proxy_pass http://backend/health; - proxy_set_header Host $host; - } - - # All other requests -> frontend - location / { - proxy_pass http://frontend; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - } -} diff --git a/docs/README.md b/docs/README.md index 1d1cde59..0379779c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,39 +1,17 @@ -# MomShell Documentation - -Welcome to the MomShell documentation. This index will help you find the information you need. - -## Quick Links +# Documentation | Document | Description | |----------|-------------| -| [Features](features.md) | Detailed feature descriptions and use cases | -| [Getting Started](getting-started.md) | Quick start guide for new users | -| [Development](development.md) | Development environment setup | -| [Deployment](deployment.md) | Docker deployment guide | +| [Getting Started](getting-started.md) | Quick setup guide | +| [Development](development.md) | Development workflow and commands | | [Configuration](configuration.md) | Environment variables reference | -| [Architecture](architecture.md) | Technical architecture overview | - -## For New Users - -1. **[Getting Started](getting-started.md)** - Install and run MomShell in minutes -2. **[Features](features.md)** - Explore what MomShell can do for you - -## For Developers - -1. **[Development](development.md)** - Set up your development environment -2. **[Architecture](architecture.md)** - Understand the codebase structure -3. **[Contributing](../CONTRIBUTING.md)** - Contribution guidelines - -## For Deployment - -1. **[Deployment](deployment.md)** - Deploy with Docker -2. **[Configuration](configuration.md)** - Configure environment variables +| [Architecture](architecture.md) | Technical design and project structure | +| [Deployment](deployment.md) | Docker and production deployment | +| [Features](features.md) | Module descriptions | -## Project +## Contributing -- **[Changelog](../CHANGELOG.md)** - Version history -- **[Code of Conduct](../CODE_OF_CONDUCT.md)** - Community guidelines -- **[Security](../SECURITY.md)** - Security policy +See [CONTRIBUTING.md](../CONTRIBUTING.md) for code standards and workflow. --- diff --git a/docs/architecture.md b/docs/architecture.md index a0d295a2..2f291cdb 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -6,168 +6,106 @@ Technical architecture overview of MomShell. ### Backend -| Technology | Purpose | -| ----------------------- | ----------------------------------------- | -| **FastAPI** | High-performance async web framework | -| **MediaPipe** | Real-time pose detection (33 landmarks) | -| **LangGraph** | Workflow orchestration for coaching logic | -| **Edge TTS** | Microsoft neural voice synthesis | -| **SQLite + SQLAlchemy** | Lightweight async database | +| Technology | Purpose | +|------------|---------| +| **Go 1.23** | Backend language | +| **Gin** | HTTP framework | +| **GORM** | ORM (PostgreSQL) | +| **JWT (golang-jwt)** | Authentication | +| **OpenAI SDK** | LLM integration | +| **go:embed** | Embedded admin panel | ### Frontend -| Technology | Purpose | -| ---------------- | ------------------------------- | -| **Next.js 14** | React framework with App Router | -| **TypeScript** | Type-safe development | -| **Tailwind CSS** | Utility-first styling | +| Technology | Purpose | +|------------|---------| +| **Vue 3** | UI framework | +| **Vite** | Build tool | +| **TypeScript** | Type safety | +| **Pinia** | State management | +| **Axios** | HTTP client | ## Project Structure ``` MomShell/ -├── backend/ # FastAPI backend -│ ├── app/ -│ │ ├── api/v1/ # REST + WebSocket routes -│ │ ├── core/ # Configuration and database -│ │ ├── models/ # ML models dir (gitignored) -│ │ ├── schemas/ # Pydantic schemas (coach) -│ │ ├── services/ # Business logic -│ │ │ ├── auth/ # JWT, OAuth authentication -│ │ │ ├── chat/ # Soul Companion service -│ │ │ ├── coach/ # Recovery Coach service -│ │ │ │ ├── analysis/ # Pose analysis & scoring -│ │ │ │ ├── exercises/ # Exercise library -│ │ │ │ ├── feedback/ # LLM feedback & TTS -│ │ │ │ ├── pose/ # MediaPipe detection -│ │ │ │ ├── progress/ # Progress tracking -│ │ │ │ └── workflow/ # LangGraph workflow -│ │ │ ├── community/ # Sisterhood Bond service -│ │ │ │ ├── moderation/ # Content moderation -│ │ │ │ ├── router/ # Community API routes -│ │ │ │ └── schemas/ # Community schemas -│ │ │ └── guardian/ # Guardian Partner service -│ │ ├── static/ # Static assets -│ │ │ ├── css/ -│ │ │ └── js/ -│ │ └── templates/ # HTML templates -│ ├── data/ # Database storage (gitignored) -│ ├── models/ # ML models (gitignored) -│ ├── scripts/ # CLI scripts (create_admin, etc.) -│ └── tests/ # Backend tests +├── backend/ # Go backend +│ ├── cmd/server/main.go # Entry point & dependency wiring +│ ├── internal/ +│ │ ├── admin/ # Embedded admin panel (go:embed HTML) +│ │ ├── config/ # Environment config loader +│ │ ├── database/ # DB connection & auto-migration +│ │ ├── dto/ # Request/response data transfer objects +│ │ ├── handler/ # HTTP handlers (Gin) +│ │ ├── middleware/ # Auth, CORS, recovery middleware +│ │ ├── model/ # GORM models (User, Question, Answer, etc.) +│ │ ├── repository/ # Data access layer +│ │ ├── router/ # Route registration +│ │ └── service/ # Business logic +│ └── pkg/ +│ ├── jwt/ # JWT generation & validation +│ ├── openai/ # OpenAI-compatible client +│ └── password/ # bcrypt hashing │ -├── frontend/ # Next.js frontend -│ ├── app/ # App router pages -│ │ ├── auth/ # Auth pages -│ │ │ ├── login/ -│ │ │ ├── register/ -│ │ │ ├── forgot-password/ -│ │ │ └── reset-password/ -│ │ ├── chat/ # Soul Companion -│ │ ├── coach/ # Recovery Coach -│ │ ├── community/ # Sisterhood Bond -│ │ │ ├── admin/ # Admin pages (certification review) -│ │ │ ├── certification/ # Professional certification -│ │ │ ├── collections/ # Shell Picks -│ │ │ ├── my-posts/ # My questions -│ │ │ ├── my-replies/ # My answers -│ │ │ └── profile/ # User profile -│ │ └── guardian/ # Guardian Partner -│ ├── components/ # React components -│ │ ├── auth/ -│ │ ├── coach/ -│ │ ├── community/ -│ │ ├── guardian/ -│ │ └── home/ -│ ├── contexts/ # React contexts (AuthContext) -│ ├── hooks/ # Custom hooks -│ ├── lib/ # Utilities & API clients -│ │ └── api/ # API client modules -│ ├── public/ # Public assets -│ └── types/ # TypeScript definitions -│ -├── deploy/ # Deployment configs -│ ├── docker-compose.yml -│ └── nginx.conf +├── frontend/ # Vue 3 frontend +│ └── src/ +│ ├── components/ +│ │ ├── overlay/ # Auth, chat, community, profile panels +│ │ └── scene/ # Beach scene layers (sky, ocean, sand, etc.) +│ ├── composables/ # Vue composables (animation, parallax, waves) +│ ├── constants/ # Scene configuration +│ ├── lib/ # API client, auth utilities +│ ├── stores/ # Pinia stores (auth, UI) +│ └── styles/ # CSS │ +├── deploy/ # Docker Compose + Nginx config ├── docs/ # Documentation -│ ├── README.md # Documentation index -│ ├── features.md # Feature descriptions -│ ├── getting-started.md # Quick start guide -│ ├── development.md # Development guide -│ ├── deployment.md # Docker deployment -│ ├── configuration.md # Environment variables -│ └── architecture.md # This file -│ -├── scripts/ # Project-level scripts -│ └── dev-setup.sh # Development setup script -│ -├── .github/ # GitHub configs -│ ├── ISSUE_TEMPLATE/ # Issue templates -│ └── workflows/ # CI/CD workflows -│ +├── scripts/dev-setup.sh # Development setup script ├── .env.example # Environment template -├── Dockerfile # Combined container (ModelScope) -├── Makefile # Build commands -├── CONTRIBUTING.md # Contribution guidelines -└── README.md # Project README +├── Makefile # Build & dev commands +└── .pre-commit-config.yaml # Git hooks ``` -## Architecture Highlights - -### Real-time Pose Detection +## Architecture Layers -- Uses MediaPipe Pose Landmarker with VIDEO mode for tracking -- LITE model by default for better performance (configurable via `MEDIAPIPE_MODEL`) -- Client-side skeleton rendering for minimal latency -- WebSocket protocol for real-time bidirectional communication +``` +Handler (HTTP) → Service (Business Logic) → Repository (Data Access) → GORM → PostgreSQL +``` -### Non-blocking Feedback Pipeline +- **Handler**: Parses HTTP requests, validates input, calls service, returns JSON +- **Service**: Business rules, authorization checks, cross-cutting concerns +- **Repository**: Database queries via GORM, no business logic +- **Model**: GORM structs with table mappings and relationships +- **DTO**: Request/response types, decoupled from models -```mermaid -flowchart TD - A[Video Frame] --> B[Pose Detection] - B --> C[Analysis] - C -.-> D[Background Process] - D --> E[LLM Feedback] - E --> F[TTS Synthesis] - F --> G[Audio Response] -``` +## Key Design Decisions -- LLM feedback generation runs in background -- TTS synthesis is fire-and-forget -- No blocking of the frame processing pipeline +### Embedded Admin Panel -### WebSocket Protocol +The admin panel is a single HTML file (`internal/admin/admin.html`) using Tailwind CSS CDN and Alpine.js. It's embedded into the Go binary via `go:embed`, requiring no separate frontend build or deployment. -- Client sends video frames -- Server returns keypoints -- Skeleton drawn on client side for smooth 20+ FPS experience +### Authentication -### Data Flow +- JWT access tokens (30 min) + refresh tokens (7 days) +- Tokens extracted from `Authorization: Bearer`, `X-Access-Token` header, or `access_token` cookie +- Admin role verified per-request in handler via `authService.GetUserByID` -```mermaid -flowchart TB - subgraph Frontend[Frontend - Next.js] - UI[Web App] - end +### Content Moderation - subgraph Backend[Backend - FastAPI] - Auth - Chat - Community - Coach - Guardian - end +- Keyword-based detection with categories (pseudoscience, violence, self-harm, spam) +- Crisis keywords trigger auto-rejection +- Results: Passed / Rejected / NeedManualReview - subgraph External[External Services] - DB[(SQLite)] - LLM[ModelScope] - Search[Firecrawl] - end +## Data Flow - Frontend <-->|REST / WebSocket| Backend - Backend <-->|SQL / HTTP API| External +``` +Frontend (Vue 3 / Vite) + ↕ REST API (JSON) +Backend (Go / Gin) + ↕ GORM +PostgreSQL + ↕ HTTP +OpenAI-compatible LLM ``` --- diff --git a/docs/configuration.md b/docs/configuration.md index 214f214d..5f0661bf 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,127 +1,67 @@ # Configuration -Environment variables for configuring MomShell. +Environment variables for configuring MomShell. Copy `.env.example` to `.env` and edit. -## Environment Variables - -### Core Settings - -| Variable | Description | Required | Default | -|----------|-------------|----------|---------| -| `APP_NAME` | Application name | No | `MomShell` | -| `DEBUG` | Enable debug mode | No | `false` | -| `HOST` | Server host | No | `0.0.0.0` | -| `PORT` | Server port | No | `8000` (local) / `7860` (Docker) | - -### Database +## Database | Variable | Description | Required | Default | |----------|-------------|----------|---------| -| `DATABASE_URL` | Database connection URL | No | `sqlite+aiosqlite:///./data/momshell.db` | +| `DATABASE_URL` | PostgreSQL connection string | **Yes** | — | -Supported formats: -- SQLite (local): `sqlite+aiosqlite:///./data/momshell.db` -- SQLite (Docker): `sqlite+aiosqlite:////mnt/workspace/momshell.db` -- PostgreSQL: `postgresql+asyncpg://user:pass@host:5432/dbname` +Format: `postgres://user:password@host:5432/dbname?sslmode=disable` -### AI Services (ModelScope) +## JWT Authentication | Variable | Description | Required | Default | |----------|-------------|----------|---------| -| `MODELSCOPE_KEY` | ModelScope API key | **Yes** | - | -| `MODELSCOPE_MODEL` | Model for chat and feedback | No | `Qwen/Qwen2.5-72B-Instruct` | -| `MODELSCOPE_IMAGE_MODEL` | Model for image generation | No | - | +| `JWT_SECRET_KEY` | Secret key for JWT signing | **Yes** | — | +| `JWT_ACCESS_TOKEN_EXPIRE_MINUTES` | Access token lifetime | No | `30` | +| `JWT_REFRESH_TOKEN_EXPIRE_DAYS` | Refresh token lifetime | No | `7` | -Get your API key from: https://modelscope.cn/ +The setup script auto-generates `JWT_SECRET_KEY`. Change it manually for production. -### MediaPipe (Pose Detection) +## OpenAI Compatible API | Variable | Description | Required | Default | |----------|-------------|----------|---------| -| `MEDIAPIPE_MODEL` | Pose model type (`lite` or `full`) | No | `lite` | -| `POSE_MODEL_COMPLEXITY` | Model complexity (0-2) | No | `1` | -| `MIN_DETECTION_CONFIDENCE` | Detection confidence threshold | No | `0.5` | -| `MIN_TRACKING_CONFIDENCE` | Tracking confidence threshold | No | `0.3` | - -### TTS (Text-to-Speech) - -| Variable | Description | Required | Default | -|----------|-------------|----------|---------| -| `TTS_VOICE` | Microsoft Edge TTS voice | No | `zh-CN-XiaoxiaoNeural` | -| `TTS_RATE` | Speech rate adjustment | No | `-10%` | - -Common Chinese voices: -- `zh-CN-XiaoxiaoNeural` (default, female) -- `zh-CN-YunxiNeural` (male) -- `zh-CN-YunyangNeural` (male, news style) +| `OPENAI_API_KEY` | API key for LLM service | **Yes** | — | +| `OPENAI_BASE_URL` | API base URL | No | `https://api-inference.modelscope.cn/v1` | +| `OPENAI_MODEL` | Model name | No | `Qwen/Qwen2.5-72B-Instruct` | -See [Edge TTS documentation](https://github.com/rany2/edge-tts) for all available voices. +Any OpenAI-compatible API is supported (ModelScope, OpenAI, local Ollama, etc.). -### Safety Thresholds +## Server | Variable | Description | Required | Default | |----------|-------------|----------|---------| -| `MAX_DEVIATION_ANGLE` | Maximum allowed pose deviation (degrees) | No | `30.0` | -| `FATIGUE_DETECTION_THRESHOLD` | Fatigue detection sensitivity | No | `0.7` | -| `REST_PROMPT_INTERVAL` | Interval between rest prompts (seconds) | No | `300` | +| `PORT` | HTTP server port | No | `8000` | -### JWT Authentication +## Frontend | Variable | Description | Required | Default | |----------|-------------|----------|---------| -| `JWT_SECRET_KEY` | Secret key for JWT signing | **Yes** (production) | `your-secret-key-change-in-production` | -| `JWT_ALGORITHM` | JWT signing algorithm | No | `HS256` | -| `JWT_ACCESS_TOKEN_EXPIRE_MINUTES` | Access token expiration | No | `30` | -| `JWT_REFRESH_TOKEN_EXPIRE_DAYS` | Refresh token expiration | No | `7` | +| `VITE_API_BASE_URL` | Backend API URL for frontend | No | `http://localhost:8000` | -**Important**: Change `JWT_SECRET_KEY` to a secure random value in production. +Leave empty in production when using Nginx reverse proxy. -### Web Search (Firecrawl) +## Initial Admin Account | Variable | Description | Required | Default | |----------|-------------|----------|---------| -| `FIRECRAWL_API_KEY` | Firecrawl API key for web search | No | - | - -Enables web search for fact-checked responses. Get your API key from: https://www.firecrawl.dev/ - -### Initial Admin Account - -| Variable | Description | Required | Default | -|----------|-------------|----------|---------| -| `ADMIN_USERNAME` | Admin username | No | - | -| `ADMIN_EMAIL` | Admin email | No | - | -| `ADMIN_PASSWORD` | Admin password | No | - | - -Set these to automatically create an admin account on first startup. - -## Setup - -1. Copy the example file: - -```bash -cp .env.example .env -``` - -2. Edit `.env` and fill in required values: +| `ADMIN_USERNAME` | Admin username | No | — | +| `ADMIN_EMAIL` | Admin email | No | — | +| `ADMIN_PASSWORD` | Admin password | No | — | -```bash -# Required -MODELSCOPE_KEY=your_modelscope_api_key_here +Set all three to auto-create an admin on first startup. Skipped if the username or email already exists. -# Required for production -JWT_SECRET_KEY=your_secure_random_secret_key +## Runtime Configuration -# Optional - customize as needed -MODELSCOPE_MODEL=Qwen/Qwen2.5-72B-Instruct -TTS_VOICE=zh-CN-XiaoxiaoNeural -``` +The following can also be changed at runtime via the admin panel (`/admin` → System Config): -## Important Notes +- `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `OPENAI_MODEL` +- `JWT_ACCESS_TOKEN_EXPIRE_MINUTES`, `JWT_REFRESH_TOKEN_EXPIRE_DAYS` -- **Do not use quotes** around values in `.env` - Docker `--env-file` includes quotes literally -- `MODELSCOPE_KEY` is required for Soul Companion and Recovery Coach features -- For Docker deployment, leave `PORT` unset or commented out to use the default 7860 -- Always change `JWT_SECRET_KEY` in production environments +Read-only in admin panel (requires restart): `DATABASE_URL`, `JWT_ALGORITHM`, `PORT`. --- diff --git a/docs/deployment.md b/docs/deployment.md index b1a461f3..f415349f 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,11 +1,7 @@ # Docker Deployment -Deploy MomShell with Docker for production environments. - ## Prerequisites -### Install Docker - ```bash # Arch Linux sudo pacman -S docker docker-compose @@ -13,7 +9,6 @@ sudo pacman -S docker docker-compose # Ubuntu/Debian sudo apt install docker.io docker-compose -# Start Docker daemon sudo systemctl start docker sudo systemctl enable docker ``` @@ -23,84 +18,86 @@ sudo systemctl enable docker ```bash # 1. Configure environment cp .env.example .env -# Edit .env, fill in MODELSCOPE_KEY (required) +# Edit .env: uncomment the Docker DATABASE_URL line (postgres host) +# Set JWT_SECRET_KEY, OPENAI_API_KEY, etc. -# 2. Start services -cd deploy -docker compose up -d --build +# 2. Start all services +make docker-up -# 3. Access the application -open http://localhost:7860 +# 3. Access +# App: http://localhost +# Admin panel: http://localhost/admin ``` -## Docker Compose Deployment +## Architecture -The recommended deployment method using `deploy/docker-compose.yml`: +A single Docker image contains both frontend and backend: -```bash -cd deploy +``` +Browser → Nginx (:80) + ├── / → static files (Vue SPA) + ├── /api/ → proxy → Go backend (:8000) + └── /admin → proxy → Go backend (:8000) +``` -# Start all services -docker compose up -d --build +`docker-compose.yml` runs two containers: -# View logs -docker compose logs -f +| Service | Image | Port | +|---------|-------|------| +| **app** | Nginx + Go binary (single image) | 80 (exposed) | +| **postgres** | PostgreSQL 16 | 5432 (internal) | -# Stop services -docker compose down -``` +The root `Dockerfile` uses multi-stage builds: +1. **frontend-builder** — Node 24, `npm ci && npm run build` → `dist/` +2. **backend-builder** — Go 1.23, `go build` → binary +3. **final** — Nginx Alpine + Go binary + entrypoint script ## Make Commands -From the project root: - ```bash -make docker-up # Build and start all containers -make docker-down # Stop all containers -make docker-logs # View container logs -make docker-build-backend # Build backend image only -make docker-build-frontend # Build frontend image only +make docker-build # Build Docker image +make docker-up # Start all services (app + postgres) +make docker-down # Stop all services +make docker-logs # View logs ``` -## Single Container Deployment - -For platforms like ModelScope or simpler deployments: +## Standalone Run ```bash -# Build combined image +# Build docker build -t momshell . -# Run container -docker run -d -p 7860:7860 --env-file .env momshell - -# Or use Make -make docker-build +# Run (requires external PostgreSQL) +docker run -d -p 80:80 --env-file .env momshell ``` -**Note**: Ensure `PORT` is commented out in `.env` (or not set) so the Dockerfile's `PORT=7860` takes effect. +## Configuration -## Container Architecture +Key environment variables for deployment: -### Multi-Container (docker-compose) +| Variable | Description | +|----------|-------------| +| `DATABASE_URL` | `postgres://momshell:momshell@postgres:5432/momshell?sslmode=disable` | +| `POSTGRES_USER` | PostgreSQL container user (default: momshell) | +| `POSTGRES_PASSWORD` | PostgreSQL container password (default: momshell) | +| `POSTGRES_DB` | PostgreSQL container database (default: momshell) | +| `JWT_SECRET_KEY` | Secure random secret | +| `OPENAI_API_KEY` | LLM API key | +| `PORT` | Backend server port (default: 8000, internal) | -- **backend**: FastAPI application -- **frontend**: Next.js application -- **nginx**: Reverse proxy +`VITE_API_BASE_URL` is not needed in Docker — Nginx proxies `/api/` to the backend within the same container. -### Single Container +See [Configuration](configuration.md) for the full reference. -- Combined backend and frontend -- Suitable for platforms with single-container requirements -- Uses port 7860 by default +## Data Persistence -## Configuration +PostgreSQL data is stored in a Docker named volume `pgdata`. To reset: -See [Configuration](configuration.md) for environment variables. - -Key variables for Docker: -- `MODELSCOPE_KEY` - Required for AI services -- `PORT` - Container port (default: 7860 for Docker) -- `DATABASE_URL` - Database connection string +```bash +make docker-down +docker volume rm deploy_pgdata +make docker-up +``` --- diff --git a/docs/development.md b/docs/development.md index d63c2bce..f00321f4 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,18 +1,18 @@ # Development Guide -Set up your development environment for contributing to MomShell. - ## Prerequisites -- [uv](https://docs.astral.sh/uv/) - Python package manager -- [nvm](https://github.com/nvm-sh/nvm) - Node.js version manager +- Go 1.23+ +- Node.js 24+ +- PostgreSQL - Git +- pre-commit (optional, for git hooks) -See [Getting Started](getting-started.md) for installation instructions. +See [Getting Started](getting-started.md) for installation links. ## Setup -### Automated Setup (Recommended) +### Automated (Recommended) ```bash git clone https://github.com/koishi510/MomShell.git @@ -20,37 +20,16 @@ cd MomShell ./scripts/dev-setup.sh ``` -### Manual Setup - -1. **Environment variables** - -```bash -# Backend environment (API keys, database, etc.) -cp .env.example .env -# Edit .env and fill in your API keys - -# Frontend environment (required for local development) -cp frontend/.env.example frontend/.env.local -``` - -> **Note:** In Docker deployment, the frontend uses relative paths via Nginx. But for local development with separate frontend/backend servers, the frontend needs `NEXT_PUBLIC_API_URL` to point to the backend at `http://localhost:8000`. - -2. **Backend dependencies** +### Manual ```bash -cd backend -uv sync -cd .. -``` +cp .env.example .env # Edit with your config -3. **Frontend dependencies** +cd backend && go mod download && cd .. +cd frontend && npm install && cd .. -```bash -cd frontend -nvm install -nvm use -npm install -cd .. +# Optional: install git hooks +pre-commit install ``` ## Running Locally @@ -58,51 +37,36 @@ cd .. ### Using Make ```bash -# Start backend and frontend in separate terminals -make dev-backend # Terminal 1 -make dev-frontend # Terminal 2 - -# Or use tmux to start both -make dev-tmux +make dev-backend # Terminal 1 — Go server on :8000 +make dev-frontend # Terminal 2 — Vite dev server on :5173 +make dev-tmux # Or both in tmux ``` ### Manual Commands -**Backend (FastAPI)** - ```bash -cd backend -uv run uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 -``` - -**Frontend (Next.js)** +# Backend +cd backend && go run cmd/server/main.go -```bash -cd frontend -npm run dev +# Frontend +cd frontend && npx vite ``` ## Common Commands ```bash -# Backend -make lint # Run linters (ruff) -make test # Run backend tests +make lint # go vet + eslint +make format # go fmt +make typecheck # go build + vue-tsc +make check # lint + typecheck -# Frontend -cd frontend -npm run lint # ESLint -npm run build # Production build +make build-backend # Build Go binary +make build-frontend # Vite production build ``` ## Contributing -Please read the full [Contributing Guide](../CONTRIBUTING.md) for: - -- Code standards and quality checks -- Branch management and PR process -- Commit message conventions -- Testing requirements +See [CONTRIBUTING.md](../CONTRIBUTING.md) for code standards, commit conventions, and PR workflow. --- diff --git a/docs/features.md b/docs/features.md index ffa1e3be..b8a2eb38 100644 --- a/docs/features.md +++ b/docs/features.md @@ -1,123 +1,32 @@ # Features -MomShell provides five integrated modules designed to support new mothers through the postpartum recovery journey. +MomShell provides integrated modules designed to support new mothers through postpartum recovery. ## Soul Companion -An AI-powered emotional support companion that understands the unique challenges of new motherhood. +AI-powered emotional support companion. -### What It Does - -- **Empathetic Conversations**: Warm, validating, non-judgmental dialogue designed specifically for postpartum emotional support -- **Memory & Personalization**: Remembers your conversations for a personalized experience -- **Fact-Checked Responses**: Integrates web search (Firecrawl) for medical and factual questions to reduce AI hallucinations -- **Healing Atmosphere**: Visual ambient effects and calming UI design - -### Use Cases - -- When you need someone to talk to at 3am during a feeding -- When you're feeling overwhelmed and need validation -- When you have questions about postpartum recovery -- When you just need a moment of calm connection - ---- +- **Empathetic conversations** designed for postpartum emotional support +- **Conversation memory** for personalized experience across sessions +- **Content moderation** with crisis keyword detection ## Sisterhood Bond -A supportive community connecting new mothers with each other and verified healthcare professionals. - -### What It Does - -- **Dual-Channel System**: - - **Professional Channel**: Advice from verified doctors, therapists, and nurses - - **Experience Channel**: Stories and tips from fellow mothers -- **Verified Professionals**: Healthcare providers with credential verification -- **Engagement Features**: Q&A with likes, collections, and content moderation -- **Daily Resonance**: Curated topics and Shell Picks collections - -### Use Cases - -- Ask questions and get answers from verified healthcare professionals -- Share your experiences and learn from other mothers -- Find solidarity in shared challenges -- Discover curated content relevant to your stage of recovery - ---- - -## Recovery Coach - -AI-powered postpartum exercise coaching with real-time pose detection and voice feedback. - -### What It Does - -- **Real-Time Pose Detection**: Uses MediaPipe with 33 body landmarks for accurate movement tracking -- **Postpartum-Specific Exercises**: 9 exercises across 5 categories: - - Breathing exercises - - Pelvic floor strengthening - - Diastasis recti recovery - - Posture correction - - Strength building -- **Voice Guidance**: LLM-powered feedback with gentle, encouraging tone via Edge TTS -- **Progress Tracking**: Achievements, streaks, and strength metrics -- **Safety Monitoring**: Fatigue detection with automatic rest prompts - -### Use Cases - -- Guided postpartum exercise sessions at home -- Safe return to physical activity with proper form guidance -- Track your recovery progress over time -- Exercise at your own pace with personalized encouragement - ---- - -## Guardian Partner - -A gamified system to engage partners in the postpartum recovery journey. - -### What It Does - -- **Partner Binding**: Connect with your partner via invite codes -- **Daily Status Recording**: Track mood, energy, health conditions, feeding, and sleep -- **Smart Suggestions**: Partners receive personalized suggestions based on the mother's status -- **Task System**: Three difficulty levels with point rewards -- **Level Progression**: Advance from Intern → Trainee → Regular → Gold -- **Time Recorder**: Capture and preserve baby milestone photos - -### Use Cases - -- Help partners understand how to support the new mother -- Gamify the division of responsibilities -- Track and celebrate partner involvement -- Preserve precious moments with milestone photos - ---- - -## Echo Domain - -A meditative space where mothers reconnect with their pre-motherhood identity, while partners observe and support through a symbolic glass window. - -### What It Does - -- **Mom Mode (The Origin)**: - - **Identity Tags**: Record personal preferences across music, sounds, literature, and youth memories - - **Meditation Sessions**: Guided breathing with 4-4-6 rhythm (inhale-hold-exhale) and matched scenes/audio - - **Youth Memoirs**: AI-generated nostalgic stories based on your identity tags - - **Scene & Audio Matching**: Personalized visuals and ambient sounds based on your preferences +Community Q&A connecting mothers with verified healthcare professionals. -- **Partner Mode (The Guardian)**: - - **Window Clarity**: Glass window that clears as mom meditates (0-100% clarity) - - **Blurred View**: See mom's meditation status through a frosted glass effect - - **Memory Injection**: Prepare heartwarming memories that unlock at clarity thresholds - - **Revealed Memories**: Memories automatically appear as clarity increases +- **Dual channels**: Professional advice and peer experience sharing +- **Verified professionals**: Doctors, therapists, and nurses with credential verification +- **Engagement**: Q&A, likes, collections, comments +- **Content moderation**: Keyword-based filtering with manual review queue -- **Dual-Color Nebula UI**: Beautiful split-screen with warm amber (mom) and cool indigo (partner) themes +## Admin Panel -### Use Cases +Embedded management interface at `/admin`. -- Reconnect with your identity beyond motherhood during quiet meditation -- Find peace through personalized scenes and sounds that resonate with your memories -- Allow your partner to participate in your healing journey in a supportive, non-intrusive way -- Receive surprise memories from your partner as rewards for self-care +- **Dashboard**: User statistics, content counts, role distribution +- **User management**: Search, filter, paginate, create, edit (role/status), delete +- **Config management**: View and edit runtime configuration (API keys, token expiration) +- **Single-file UI**: Tailwind CSS + Alpine.js, embedded via `go:embed` --- diff --git a/docs/getting-started.md b/docs/getting-started.md index 03339127..97cfac1b 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,59 +1,34 @@ # Getting Started -Get MomShell up and running in minutes. - ## Prerequisites -Before you begin, ensure you have: - -- [uv](https://docs.astral.sh/uv/) - Python package manager -- [nvm](https://github.com/nvm-sh/nvm) - Node.js version manager - -### Install uv - -```bash -# Linux / macOS -curl -LsSf https://astral.sh/uv/install.sh | sh - -# Windows (PowerShell) -powershell -c "irm https://astral.sh/uv/install.ps1 | iex" -``` - -### Install nvm - -```bash -# Linux / macOS -curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash - -# Windows: use nvm-windows -# https://github.com/coreybutler/nvm-windows -``` +- [Go 1.23+](https://go.dev/dl/) +- [Node.js 24+](https://nodejs.org/) (or via [nvm](https://github.com/nvm-sh/nvm)) +- [PostgreSQL](https://www.postgresql.org/) +- Git ## Quick Install ```bash -# Clone the repository git clone https://github.com/koishi510/MomShell.git cd MomShell - -# Run the setup script (recommended) ./scripts/dev-setup.sh ``` -The setup script automatically: -- Checks prerequisites -- Creates `.env` from template -- Installs backend/frontend dependencies -- Sets up Git hooks +The setup script: +- Checks prerequisites (Go, Node, npm, git) +- Creates `.env` from template with auto-generated JWT secret +- Downloads Go dependencies +- Installs npm packages +- Installs pre-commit hooks ## Start the Application ```bash -# Using Make (recommended) - run in separate terminals -make dev-backend # Terminal 1 -make dev-frontend # Terminal 2 +make dev-backend # Terminal 1 — http://localhost:8000 +make dev-frontend # Terminal 2 — http://localhost:5173 -# Or use tmux to start both +# Or use tmux make dev-tmux ``` @@ -61,21 +36,21 @@ make dev-tmux | Service | URL | |---------|-----| -| Frontend | http://localhost:3000 | +| Frontend | http://localhost:5173 | | Backend API | http://localhost:8000 | -| API Docs | http://localhost:8000/docs | +| Admin Panel | http://localhost:8000/admin | ## Create Admin Account -To manage certifications and other admin tasks: +Set these in `.env` before first startup: -```bash -cd backend -uv run python -m scripts.create_admin [nickname] - -# Example -uv run python -m scripts.create_admin admin admin@example.com mypassword Admin ``` +ADMIN_USERNAME=admin +ADMIN_EMAIL=admin@example.com +ADMIN_PASSWORD=your_secure_password +``` + +Or create additional admins via the admin panel after login. ## Next Steps diff --git a/frontend/.dockerignore b/frontend/.dockerignore deleted file mode 100644 index ab4a0be8..00000000 --- a/frontend/.dockerignore +++ /dev/null @@ -1,44 +0,0 @@ -# Dependencies -node_modules - -# Build output -.next -out -build -dist - -# Git -.git -.gitignore - -# IDE -.idea -.vscode -*.swp -*.swo - -# Environment -.env -.env.* -!.env.example - -# TypeScript build info -*.tsbuildinfo - -# Testing -coverage -.nyc_output - -# Debug logs -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Docker -Dockerfile -.dockerignore - -# Misc -*.md -.eslintcache -.turbo diff --git a/frontend/.env.example b/frontend/.env.example deleted file mode 100644 index 7c300b42..00000000 --- a/frontend/.env.example +++ /dev/null @@ -1,8 +0,0 @@ -# Frontend Environment Variables -# Copy this file to .env.local for local development: -# cp .env.example .env.local - -# Backend API URL -# - Local development: http://localhost:8000 -# - Docker/Production: Leave empty (uses relative path via Nginx) -NEXT_PUBLIC_API_URL=http://localhost:8000 diff --git a/frontend/.nvmrc b/frontend/.nvmrc index 2bd5a0a9..a45fd52c 100644 --- a/frontend/.nvmrc +++ b/frontend/.nvmrc @@ -1 +1 @@ -22 +24 diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 50753189..bfce20b3 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,56 +1,14 @@ -# syntax=docker/dockerfile:1 - -FROM node:22-alpine AS base - -# ============================================================================= -# Dependencies stage -# ============================================================================= -FROM base AS deps - -RUN apk add --no-cache libc6-compat - +FROM node:24-alpine AS builder WORKDIR /app - COPY package.json package-lock.json ./ - RUN npm ci - -# ============================================================================= -# Build stage -# ============================================================================= -FROM base AS builder - -WORKDIR /app - -COPY --from=deps /app/node_modules ./node_modules COPY . . - -# Build-time environment variables -ARG NEXT_PUBLIC_API_URL= -ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL -ENV NEXT_TELEMETRY_DISABLED=1 - +ARG VITE_API_BASE_URL= +ENV VITE_API_BASE_URL=$VITE_API_BASE_URL RUN npm run build -# ============================================================================= -# Production stage - serve static export -# ============================================================================= -FROM base AS runner - -WORKDIR /app - -ENV NODE_ENV=production - -RUN addgroup --system --gid 1001 nodejs && \ - adduser --system --uid 1001 nextjs && \ - npm install -g serve - -# Copy static export output -COPY --from=builder --chown=nextjs:nodejs /app/out ./out - -USER nextjs - -EXPOSE 7860 - -# Serve static files -CMD ["serve", "-s", "out", "-l", "7860"] +FROM nginx:alpine +COPY --from=builder /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/Makefile b/frontend/Makefile new file mode 100644 index 00000000..000b9146 --- /dev/null +++ b/frontend/Makefile @@ -0,0 +1,69 @@ +.PHONY: dev build preview lint typecheck check install clean docker-build docker-run help + +CYAN := \033[36m +GREEN := \033[32m +RESET := \033[0m + +##@ Development + +dev: ## Start Vite dev server + @echo "$(CYAN)Starting dev server on http://localhost:5173$(RESET)" + npx vite + +preview: build ## Preview production build locally + @echo "$(CYAN)Starting preview server...$(RESET)" + npx vite preview + +##@ Code Quality + +lint: ## Run ESLint + @echo "$(CYAN)Linting...$(RESET)" + npx eslint . + @echo "$(GREEN)Lint passed$(RESET)" + +typecheck: ## Run vue-tsc type checker + @echo "$(CYAN)Type checking...$(RESET)" + npx vue-tsc --noEmit -p tsconfig.node.json + @echo "$(GREEN)Typecheck passed$(RESET)" + +check: lint typecheck ## Run all checks (lint + typecheck) + @echo "$(GREEN)All checks passed$(RESET)" + +##@ Build + +install: ## Install npm dependencies + @echo "$(CYAN)Installing dependencies...$(RESET)" + npm install + @echo "$(GREEN)Dependencies installed$(RESET)" + +build: ## Build for production + @echo "$(CYAN)Building...$(RESET)" + npm run build + @echo "$(GREEN)Build output in dist/$(RESET)" + +##@ Docker + +docker-build: ## Build Docker image + @echo "$(CYAN)Building Docker image...$(RESET)" + docker build -t momshell-frontend . + @echo "$(GREEN)Image built: momshell-frontend$(RESET)" + +docker-run: ## Run Docker container + @echo "$(CYAN)Starting container on http://localhost:80$(RESET)" + docker run --rm -p 80:80 momshell-frontend + +##@ Cleanup + +clean: ## Remove build output and caches + @echo "$(CYAN)Cleaning...$(RESET)" + rm -rf dist node_modules/.cache + @echo "$(GREEN)Cleaned$(RESET)" + +##@ Help + +help: ## Show this help + @awk 'BEGIN {FS = ":.*##"; printf "\n$(CYAN)Usage:$(RESET)\n make $(GREEN)$(RESET)\n"} \ + /^[a-zA-Z_-]+:.*?##/ { printf " $(GREEN)%-16s$(RESET) %s\n", $$1, $$2 } \ + /^##@/ { printf "\n$(CYAN)%s$(RESET)\n", substr($$0, 5) }' $(MAKEFILE_LIST) + +.DEFAULT_GOAL := help diff --git a/frontend/app/auth/forgot-password/page.tsx b/frontend/app/auth/forgot-password/page.tsx deleted file mode 100644 index ed133c01..00000000 --- a/frontend/app/auth/forgot-password/page.tsx +++ /dev/null @@ -1,114 +0,0 @@ -"use client"; - -import { useState } from "react"; -import Link from "next/link"; -import { forgotPassword } from "../../../lib/auth"; -import { getErrorMessage } from "../../../lib/apiClient"; - -export default function ForgotPasswordPage() { - const [email, setEmail] = useState(""); - const [error, setError] = useState(""); - const [success, setSuccess] = useState(false); - const [isLoading, setIsLoading] = useState(false); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(""); - setIsLoading(true); - - try { - await forgotPassword(email); - setSuccess(true); - } catch (err) { - setError(getErrorMessage(err)); - } finally { - setIsLoading(false); - } - }; - - return ( -
      -
      -
      -

      MomShell

      -

      产后康复助手

      -
      - -
      -

      - 忘记密码 -

      -

      - 输入您的注册邮箱,我们将发送重置密码的链接 -

      - - {error && ( -
      - {error} -
      - )} - - {success ? ( -
      -
      -

      邮件已发送

      -

      - 如果该邮箱已注册,您将收到重置密码的邮件 -

      -
      - - 返回登录 - -
      - ) : ( -
      -
      - - setEmail(e.target.value)} - required - className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-pink-500 focus:border-transparent outline-none transition" - placeholder="输入注册邮箱" - /> -
      - - -
      - )} - -
      - - 返回登录 - -
      -
      - -
      - - 返回首页 - -
      -
      -
      - ); -} diff --git a/frontend/app/auth/login/page.tsx b/frontend/app/auth/login/page.tsx deleted file mode 100644 index 289eb215..00000000 --- a/frontend/app/auth/login/page.tsx +++ /dev/null @@ -1,192 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useRouter } from "next/navigation"; -import Link from "next/link"; -import { useAuth } from "../../../contexts/AuthContext"; -import { getErrorMessage } from "../../../lib/apiClient"; -import SliderCaptcha from "../../../components/auth/SliderCaptcha"; - -export default function LoginPage() { - const router = useRouter(); - const { login, isLoading: authLoading } = useAuth(); - - const [login_, setLogin] = useState(""); - const [password, setPassword] = useState(""); - const [showPassword, setShowPassword] = useState(false); - const [rememberMe, setRememberMe] = useState(false); - const [captchaToken, setCaptchaToken] = useState(null); - const [error, setError] = useState(""); - const [isLoading, setIsLoading] = useState(false); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(""); - - if (!captchaToken) { - setError("请完成滑块验证"); - return; - } - - setIsLoading(true); - - try { - await login({ login: login_, password }, rememberMe); - router.push("/"); - } catch (err) { - setError(getErrorMessage(err)); - } finally { - setIsLoading(false); - } - }; - - if (authLoading) { - return ( -
      -
      ...
      -
      - ); - } - - return ( -
      -
      -
      -

      MomShell

      -

      产后康复助手

      -
      - -
      -

      登录

      - - {error && ( -
      - {error} -
      - )} - -
      -
      - - setLogin(e.target.value)} - required - className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-pink-500 focus:border-transparent outline-none transition" - placeholder="输入用户名或邮箱" - /> -
      - -
      - -
      - setPassword(e.target.value)} - required - className="w-full px-4 py-3 pr-12 border border-gray-300 rounded-lg focus:ring-2 focus:ring-pink-500 focus:border-transparent outline-none transition" - placeholder="输入密码" - /> - -
      -
      - - setCaptchaToken(token)} - onReset={() => setCaptchaToken(null)} - /> - -
      - - - - 忘记密码? - -
      - - - - -
      -

      - 还没有账号?{" "} - - 立即注册 - -

      -
      -
      - -
      - - 返回首页 - -
      -
      -
      - ); -} diff --git a/frontend/app/auth/register/page.tsx b/frontend/app/auth/register/page.tsx deleted file mode 100644 index 2fe20865..00000000 --- a/frontend/app/auth/register/page.tsx +++ /dev/null @@ -1,297 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useRouter } from "next/navigation"; -import Link from "next/link"; -import { useAuth } from "../../../contexts/AuthContext"; -import { getErrorMessage } from "../../../lib/apiClient"; -import SliderCaptcha from "../../../components/auth/SliderCaptcha"; - -export default function RegisterPage() { - const router = useRouter(); - const { register } = useAuth(); - - const [username, setUsername] = useState(""); - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); - const [showPassword, setShowPassword] = useState(false); - const [nickname, setNickname] = useState(""); - const [role, setRole] = useState<"mom" | "dad" | "family">("mom"); - const [captchaToken, setCaptchaToken] = useState(null); - const [error, setError] = useState(""); - const [isLoading, setIsLoading] = useState(false); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(""); - - if (!captchaToken) { - setError("请完成滑块验证"); - return; - } - - if (password !== confirmPassword) { - setError("两次输入的密码不一致"); - return; - } - - if (password.length < 6) { - setError("密码长度至少为6位"); - return; - } - - setIsLoading(true); - - try { - await register({ username, email, password, nickname, role }); - router.push("/auth/login?registered=true"); - } catch (err) { - setError(getErrorMessage(err)); - } finally { - setIsLoading(false); - } - }; - - return ( -
      -
      -
      -

      MomShell

      -

      产后康复助手

      -
      - -
      -

      注册

      - - {error && ( -
      - {error} -
      - )} - -
      -
      - - setUsername(e.target.value)} - required - minLength={3} - maxLength={50} - className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-pink-500 focus:border-transparent outline-none transition" - placeholder="3-50个字符" - /> -
      - -
      - - setEmail(e.target.value)} - required - className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-pink-500 focus:border-transparent outline-none transition" - placeholder="example@email.com" - /> -
      - -
      - - setNickname(e.target.value)} - required - maxLength={50} - className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-pink-500 focus:border-transparent outline-none transition" - placeholder="在社区中显示的名字" - /> -
      - -
      - -
      - {[ - { value: "mom", label: "妈妈" }, - { value: "dad", label: "爸爸" }, - { value: "family", label: "家属" }, - ].map((option) => ( - - ))} -
      -
      - -
      - -
      - setPassword(e.target.value)} - required - minLength={6} - className="w-full px-4 py-3 pr-12 border border-gray-300 rounded-lg focus:ring-2 focus:ring-pink-500 focus:border-transparent outline-none transition" - placeholder="至少6位" - /> - -
      -
      - -
      - -
      - setConfirmPassword(e.target.value)} - required - className="w-full px-4 py-3 pr-12 border border-gray-300 rounded-lg focus:ring-2 focus:ring-pink-500 focus:border-transparent outline-none transition" - placeholder="再次输入密码" - /> - -
      -
      - - setCaptchaToken(token)} - onReset={() => setCaptchaToken(null)} - /> - - - - -
      -

      - 已有账号?{" "} - - 立即登录 - -

      -
      -
      - -
      - - 返回首页 - -
      -
      -
      - ); -} diff --git a/frontend/app/auth/reset-password/page.tsx b/frontend/app/auth/reset-password/page.tsx deleted file mode 100644 index 9c8c8db3..00000000 --- a/frontend/app/auth/reset-password/page.tsx +++ /dev/null @@ -1,170 +0,0 @@ -"use client"; - -import { useState, useEffect, Suspense } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; -import Link from "next/link"; -import { resetPassword } from "../../../lib/auth"; -import { getErrorMessage } from "../../../lib/apiClient"; - -function ResetPasswordForm() { - const router = useRouter(); - const searchParams = useSearchParams(); - const token = searchParams.get("token"); - - const [password, setPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); - const [error, setError] = useState(""); - const [success, setSuccess] = useState(false); - const [isLoading, setIsLoading] = useState(false); - - useEffect(() => { - if (!token) { - setError("无效的重置链接"); - } - }, [token]); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(""); - - if (!token) { - setError("无效的重置链接"); - return; - } - - if (password !== confirmPassword) { - setError("两次输入的密码不一致"); - return; - } - - if (password.length < 6) { - setError("密码长度至少为6位"); - return; - } - - setIsLoading(true); - - try { - await resetPassword(token, password); - setSuccess(true); - } catch (err) { - setError(getErrorMessage(err)); - } finally { - setIsLoading(false); - } - }; - - return ( -
      -
      -
      -

      MomShell

      -

      产后康复助手

      -
      - -
      -

      - 重置密码 -

      - - {error && ( -
      - {error} -
      - )} - - {success ? ( -
      -
      -

      密码重置成功

      -

      请使用新密码登录

      -
      - -
      - ) : ( -
      -
      - - setPassword(e.target.value)} - required - minLength={6} - className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-pink-500 focus:border-transparent outline-none transition" - placeholder="至少6位" - /> -
      - -
      - - setConfirmPassword(e.target.value)} - required - className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-pink-500 focus:border-transparent outline-none transition" - placeholder="再次输入新密码" - /> -
      - - -
      - )} - -
      - - 返回登录 - -
      -
      - -
      - - 返回首页 - -
      -
      -
      - ); -} - -export default function ResetPasswordPage() { - return ( - -
      ...
      - - } - > - -
      - ); -} diff --git a/frontend/app/chat/page.tsx b/frontend/app/chat/page.tsx deleted file mode 100644 index d5b5c7ac..00000000 --- a/frontend/app/chat/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -// frontend/app/chat/page.tsx -/** - * Soul Companion chat page (requires authentication) - */ - -"use client"; - -import { AuthGuard } from "../../components/AuthGuard"; -import { CompanionInterface } from "../../components/CompanionInterface"; - -export default function ChatPage() { - return ( - - - - ); -} diff --git a/frontend/app/coach/page.tsx b/frontend/app/coach/page.tsx deleted file mode 100644 index 3070c584..00000000 --- a/frontend/app/coach/page.tsx +++ /dev/null @@ -1,1071 +0,0 @@ -// frontend/app/rehab/page.tsx -/** - * AI 康复教练页面 - 现代化重构版 - * 使用 Tailwind CSS + Framer Motion - */ - -"use client"; - -import { useState, useRef, useEffect, useCallback } from "react"; -import Link from "next/link"; -import { motion, AnimatePresence } from "framer-motion"; -import { - CoachBackground, - EnergyRing, - MetricLegend, - ExerciseCard, - SessionProgress, - SessionProgressOverlay, - AchievementBadge, - AchievementProgress, - PoseOverlay, - FeedbackPanel, - PhaseTransition, - ScorePopup, - type PoseOverlayHandle, - type Feedback, -} from "../../components/coach"; -import type { EnergyMetrics } from "../../types/coach"; -import { getUserId } from "../../lib/user"; -import { useAuth } from "../../contexts/AuthContext"; -import { getAccessToken } from "../../lib/auth"; -import { AuthGuard } from "../../components/AuthGuard"; - -// 动态获取API和WebSocket基础URL(支持同域部署) -const getApiBase = () => { - if (typeof window === "undefined") return ""; - return process.env.NEXT_PUBLIC_API_URL || ""; -}; - -const getWsBase = () => { - if (typeof window === "undefined") return ""; - const apiBase = getApiBase(); - if (apiBase) { - return apiBase.replace(/^http/, "ws"); - } - // 同域部署时,使用当前页面的协议和主机 - const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; - return `${protocol}//${window.location.host}`; -}; - -const FRAME_RATE = 20; - -// Phase names mapping -const PHASE_NAMES: Record = { - preparation: "准备", - inhale: "吸气", - exhale: "呼气", - hold: "保持", - release: "放松", - rest: "休息", -}; - -// Types -interface Exercise { - id: string; - name: string; - description: string; - category: string; - difficulty: string; - sets: number; - repetitions: number; - phases: { name: string; description: string; cues: string[] }[]; -} - -interface ProgressSummary { - total_sessions: number; - current_streak: number; - total_minutes: number; - strength_metrics: Record; -} - -interface Achievement { - id: string; - name: string; - icon: string; - is_earned: boolean; -} - -interface SessionSummary { - average_score: number; - completed_reps: number; - session_duration: number; - new_achievements?: Achievement[]; -} - -type ViewType = "exercises" | "progress" | "session"; - -// Category names for filter buttons -const categoryNames: Record = { - all: "全部", - breathing: "呼吸训练", - pelvic_floor: "盆底肌", - diastasis_recti: "腹直肌修复", - posture: "体态矫正", - strength: "力量训练", -}; - -// Achievement icons for session complete modal -const achievementIcons: Record = { - footprints: "👣", - fire: "🔥", - "calendar-check": "📅", - trophy: "🏆", - star: "⭐", - "check-circle": "✅", - medal: "🏅", - "trending-up": "📈", - award: "🎖️", -}; - -export default function RehabPage() { - // Auth state - use authenticated user ID when available - const { user, isAuthenticated } = useAuth(); - const effectiveUserId = isAuthenticated && user ? user.id : getUserId(); - - // View state - const [currentView, setCurrentView] = useState("exercises"); - const [selectedCategory, setSelectedCategory] = useState("all"); - - // Data state - const [exercises, setExercises] = useState([]); - const [isLoadingExercises, setIsLoadingExercises] = useState(true); - const [progressSummary, setProgressSummary] = - useState(null); - const [achievements, setAchievements] = useState([]); - - // Session state - const [selectedExercise, setSelectedExercise] = useState( - null, - ); - const [sessionState, setSessionState] = useState("preparing"); - const [sessionProgress, setSessionProgress] = useState({ - progress: 0, - currentSet: 1, - totalSets: 3, - currentRep: 1, - totalReps: 10, - currentPhase: "准备中", - }); - const [score, setScore] = useState(null); - const [prevScore, setPrevScore] = useState(null); - const [feedback, setFeedback] = useState(null); - const [sessionSummary, setSessionSummary] = useState( - null, - ); - const [showModal, setShowModal] = useState(false); - const [showPhaseTransition, setShowPhaseTransition] = useState(false); - const [showScorePopup, setShowScorePopup] = useState(false); - const [skeletonColor, setSkeletonColor] = useState< - "green" | "yellow" | "red" | "white" - >("white"); - - // Refs - const videoRef = useRef(null); - const poseOverlayRef = useRef(null); - const wsRef = useRef(null); - const frameIntervalRef = useRef(null); - const videoStreamRef = useRef(null); - const audioQueueRef = useRef([]); - const isPlayingAudioRef = useRef(false); - const currentAudioRef = useRef(null); - const processAudioQueueRef = useRef<() => void>(() => {}); - const hasFetchedExercisesRef = useRef(false); - - // Convert progress summary to EnergyMetrics - const energyMetrics: EnergyMetrics = { - core_strength: progressSummary?.strength_metrics?.core_strength?.value ?? 0, - pelvic_floor: progressSummary?.strength_metrics?.pelvic_floor?.value ?? 0, - posture: progressSummary?.strength_metrics?.posture?.value ?? 0, - flexibility: progressSummary?.strength_metrics?.flexibility?.value ?? 0, - }; - - // Fetch exercises - const fetchExercises = useCallback(async () => { - setIsLoadingExercises(true); - try { - const response = await fetch(`${getApiBase()}/api/v1/exercises`); - const data = await response.json(); - setExercises(data); - } catch (error) { - console.error("Failed to fetch exercises:", error); - } finally { - setIsLoadingExercises(false); - } - }, []); - - // Fetch progress - const fetchProgress = useCallback(async () => { - try { - const headers: Record = {}; - const token = getAccessToken(); - if (token) { - headers["Authorization"] = `Bearer ${token}`; - } - const response = await fetch( - `${getApiBase()}/api/v1/progress/${effectiveUserId}/summary`, - { headers }, - ); - const data = await response.json(); - setProgressSummary(data); - } catch (error) { - console.error("Failed to fetch progress:", error); - } - }, [effectiveUserId]); - - // Fetch achievements - const fetchAchievements = useCallback(async () => { - try { - const headers: Record = {}; - const token = getAccessToken(); - if (token) { - headers["Authorization"] = `Bearer ${token}`; - } - const response = await fetch( - `${getApiBase()}/api/v1/progress/${effectiveUserId}/achievements`, - { headers }, - ); - const data = await response.json(); - setAchievements(data); - } catch (error) { - console.error("Failed to fetch achievements:", error); - } - }, [effectiveUserId]); - - // Save progress to database (for authenticated users) - const saveProgress = useCallback(async () => { - if (!isAuthenticated) return; - try { - const token = getAccessToken(); - if (!token) return; - await fetch(`${getApiBase()}/api/v1/progress/${effectiveUserId}/save`, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - }, - }); - } catch (error) { - console.error("Failed to save progress:", error); - } - }, [effectiveUserId, isAuthenticated]); - - // Audio queue management - const processAudioQueue = useCallback(() => { - if (isPlayingAudioRef.current || audioQueueRef.current.length === 0) return; - - isPlayingAudioRef.current = true; - const base64Data = audioQueueRef.current.shift()!; - - try { - const audio = new Audio(`data:audio/mp3;base64,${base64Data}`); - currentAudioRef.current = audio; - - audio.onended = () => { - isPlayingAudioRef.current = false; - currentAudioRef.current = null; - setTimeout(() => processAudioQueueRef.current(), 500); - }; - - audio.onerror = () => { - isPlayingAudioRef.current = false; - currentAudioRef.current = null; - setTimeout(() => processAudioQueueRef.current(), 100); - }; - - audio.play().catch(() => { - isPlayingAudioRef.current = false; - currentAudioRef.current = null; - processAudioQueueRef.current(); - }); - } catch (error) { - console.error("Failed to create audio:", error); - isPlayingAudioRef.current = false; - processAudioQueueRef.current(); - } - }, []); - - useEffect(() => { - processAudioQueueRef.current = processAudioQueue; - }, [processAudioQueue]); - - const playAudio = useCallback( - (base64Data: string) => { - audioQueueRef.current.push(base64Data); - processAudioQueue(); - }, - [processAudioQueue], - ); - - const stopAllAudio = useCallback(() => { - audioQueueRef.current = []; - if (currentAudioRef.current) { - currentAudioRef.current.pause(); - currentAudioRef.current = null; - } - isPlayingAudioRef.current = false; - }, []); - - // Camera functions - const startCamera = useCallback(async () => { - // 1. 先清理旧流 - if (videoStreamRef.current) { - videoStreamRef.current.getTracks().forEach((track) => track.stop()); - videoStreamRef.current = null; - } - - try { - const stream = await navigator.mediaDevices.getUserMedia({ - video: { - width: { ideal: 480 }, - height: { ideal: 360 }, - facingMode: "user", - }, - audio: false, - }); - videoStreamRef.current = stream; - - // 2. 等待 video 元素挂载 - const video = videoRef.current; - if (!video) { - console.error("Video element not mounted"); - stream.getTracks().forEach((track) => track.stop()); - videoStreamRef.current = null; - return false; - } - - video.srcObject = stream; - - // 3. 显式调用 play() 并等待就绪 - await video.play(); - - // 4. 等待视频元数据加载完成 - await new Promise((resolve, reject) => { - if (video.videoWidth > 0) { - resolve(); - return; - } - - const timeout = setTimeout(() => { - reject(new Error("Video load timeout")); - }, 5000); - - video.onloadedmetadata = () => { - clearTimeout(timeout); - resolve(); - }; - - video.onerror = () => { - clearTimeout(timeout); - reject(new Error("Video load error")); - }; - }); - - return true; - } catch (error) { - console.error("Failed to start camera:", error); - // 清理失败的流 - if (videoStreamRef.current) { - videoStreamRef.current.getTracks().forEach((track) => track.stop()); - videoStreamRef.current = null; - } - alert("无法访问摄像头,请检查权限设置。"); - return false; - } - }, []); - - const stopCamera = useCallback(() => { - if (videoStreamRef.current) { - videoStreamRef.current.getTracks().forEach((track) => track.stop()); - videoStreamRef.current = null; - } - }, []); - - // Frame sending - const startFrameSending = useCallback(() => { - if (frameIntervalRef.current) return; - - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - const SEND_WIDTH = 480; - const SEND_HEIGHT = 360; - - let lastSendTime = 0; - const minInterval = 1000 / FRAME_RATE; - - const sendFrame = () => { - if ( - !wsRef.current || - wsRef.current.readyState !== WebSocket.OPEN || - !videoRef.current || - !videoRef.current.videoWidth || - !ctx - ) { - frameIntervalRef.current = requestAnimationFrame(sendFrame); - return; - } - - const now = performance.now(); - if (now - lastSendTime < minInterval) { - frameIntervalRef.current = requestAnimationFrame(sendFrame); - return; - } - lastSendTime = now; - - canvas.width = SEND_WIDTH; - canvas.height = SEND_HEIGHT; - ctx.drawImage(videoRef.current, 0, 0, SEND_WIDTH, SEND_HEIGHT); - - const dataUrl = canvas.toDataURL("image/jpeg", 0.5); - const base64 = dataUrl.split(",")[1]; - wsRef.current.send(JSON.stringify({ type: "frame", data: base64 })); - frameIntervalRef.current = requestAnimationFrame(sendFrame); - }; - - frameIntervalRef.current = requestAnimationFrame(sendFrame); - }, []); - - const stopFrameSending = useCallback(() => { - if (frameIntervalRef.current) { - cancelAnimationFrame(frameIntervalRef.current); - frameIntervalRef.current = null; - } - }, []); - - // WebSocket message handler - const handleWebSocketMessage = useCallback( - (data: any) => { - switch (data.type) { - case "ack": - console.log("Server acknowledged:", data.message); - break; - case "state": - if (data.data) { - const stateData = data.data; - if (stateData.progress) { - const newPhase = - PHASE_NAMES[stateData.progress.current_phase] || - stateData.progress.current_phase; - - // Check for phase transition - setSessionProgress((prev) => { - if (prev.currentPhase !== newPhase) { - setShowPhaseTransition(true); - setTimeout(() => setShowPhaseTransition(false), 1500); - } - return { - progress: stateData.progress.progress || 0, - currentSet: stateData.progress.current_set || 1, - totalSets: stateData.progress.total_sets || 3, - currentRep: stateData.progress.current_rep || 1, - totalReps: stateData.progress.total_reps || 10, - currentPhase: newPhase, - }; - }); - } - if (stateData.analysis) { - const newScore = Math.round(stateData.analysis.score); - setScore((prev) => { - if (prev !== null && Math.abs(newScore - prev) >= 5) { - setPrevScore(prev); - setShowScorePopup(true); - setTimeout(() => setShowScorePopup(false), 1000); - } - return newScore; - }); - } - if (stateData.session_state) { - setSessionState(stateData.session_state); - } - } - if (data.keypoints) { - const color = (data.skeleton_color || "white") as - | "green" - | "yellow" - | "red" - | "white"; - setSkeletonColor(color); - poseOverlayRef.current?.drawSkeleton(data.keypoints, color); - } - if (data.feedback) { - setFeedback(data.feedback); - if (data.feedback.audio) { - playAudio(data.feedback.audio); - } - } - break; - case "feedback": - setFeedback(data.feedback); - if (data.feedback?.audio) { - playAudio(data.feedback.audio); - } - break; - case "session_ended": - setSessionSummary(data.summary); - setShowModal(true); - stopFrameSending(); - poseOverlayRef.current?.clear(); - // Save progress to database for authenticated users - saveProgress(); - break; - case "error": - console.error("Server error:", data.message); - setFeedback({ text: data.message, type: "info" }); - break; - } - }, - [playAudio, stopFrameSending, saveProgress], - ); - - // WebSocket connection - const connectWebSocket = useCallback( - (exerciseId: string) => { - const sessionId = `session_${Date.now()}_${crypto.randomUUID().slice(0, 9)}`; - const wsUrl = `${getWsBase()}/api/v1/ws/coach/${sessionId}`; - const ws = new WebSocket(wsUrl); - - ws.onopen = () => { - console.log("WebSocket connected"); - ws.send( - JSON.stringify({ - type: "start", - exercise_id: exerciseId, - user_id: effectiveUserId, - use_llm: true, - }), - ); - }; - - ws.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - handleWebSocketMessage(data); - } catch (e) { - console.error("Error handling message:", e); - } - }; - - ws.onclose = (event) => { - console.log("WebSocket disconnected:", event.code, event.reason); - stopFrameSending(); - stopAllAudio(); - }; - - ws.onerror = (error) => { - console.error("WebSocket error:", error); - }; - - wsRef.current = ws; - }, - [handleWebSocketMessage, stopFrameSending, stopAllAudio, effectiveUserId], - ); - - // Control functions - const sendControl = useCallback((action: string) => { - if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { - wsRef.current.send(JSON.stringify({ type: "control", action })); - } - }, []); - - // Start session - const startSession = useCallback( - async (exercise: Exercise) => { - setSelectedExercise(exercise); - setCurrentView("session"); - setSessionState("preparing"); - setScore(null); - setPrevScore(null); - setFeedback(null); - setSessionProgress({ - progress: 0, - currentSet: 1, - totalSets: exercise.sets, - currentRep: 1, - totalReps: exercise.repetitions, - currentPhase: "准备中", - }); - - // 等待 video 元素真正挂载(最多 2 秒) - const videoMounted = await new Promise((resolve) => { - const startTime = Date.now(); - const checkVideo = () => { - if (videoRef.current) { - resolve(true); - } else if (Date.now() - startTime > 2000) { - resolve(false); - } else { - requestAnimationFrame(checkVideo); - } - }; - checkVideo(); - }); - - if (!videoMounted) { - console.error("Video element did not mount in time"); - setCurrentView("exercises"); - return; - } - - const cameraStarted = await startCamera(); - if (!cameraStarted) { - setCurrentView("exercises"); - return; - } - - connectWebSocket(exercise.id); - }, - [startCamera, connectWebSocket], - ); - - // Begin exercise - const beginExercise = useCallback(() => { - if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { - wsRef.current.send(JSON.stringify({ type: "begin" })); - startFrameSending(); - } - }, [startFrameSending]); - - // End session - const endSession = useCallback(() => { - sendControl("end"); - stopFrameSending(); - stopCamera(); - stopAllAudio(); - poseOverlayRef.current?.clear(); - - if (wsRef.current) { - wsRef.current.close(); - wsRef.current = null; - } - - setCurrentView("exercises"); // 返回动作选择界面 - }, [sendControl, stopFrameSending, stopCamera, stopAllAudio]); - - // Close modal - const closeModal = useCallback(() => { - setShowModal(false); - setCurrentView("exercises"); - fetchProgress(); - fetchAchievements(); - }, [fetchProgress, fetchAchievements]); - - // Change view - const changeView = useCallback( - (view: ViewType) => { - setCurrentView(view); - if (view === "progress") { - fetchProgress(); - fetchAchievements(); - } - }, - [fetchProgress, fetchAchievements], - ); - - // Filter exercises - const filteredExercises = - selectedCategory === "all" - ? exercises - : exercises.filter((e) => e.category === selectedCategory); - - // Format duration - const formatDuration = (seconds: number) => { - const mins = Math.floor(seconds / 60); - const secs = Math.round(seconds % 60); - return `${mins}:${secs.toString().padStart(2, "0")}`; - }; - - // Initialize - useEffect(() => { - // 防止 React Strict Mode 双重调用 - if (hasFetchedExercisesRef.current) return; - hasFetchedExercisesRef.current = true; - fetchExercises(); - }, [fetchExercises]); - - // Cleanup - useEffect(() => { - return () => { - stopCamera(); - stopFrameSending(); - stopAllAudio(); - if (wsRef.current) wsRef.current.close(); - }; - }, [stopCamera, stopFrameSending, stopAllAudio]); - - return ( - -
      - {/* 背景层 */} - - - {/* Header */} - -
      -
      - - ← 首页 - - 🧘‍♀️ - - 身体重塑 - -
      - -
      -
      - -
      - {/* Exercise Selection View */} - - {currentView === "exercises" && ( -
      -

      - 康复动作 -

      - - {/* Category Filter */} -
      - {Object.entries(categoryNames).map(([key, name]) => ( - - ))} -
      - - {/* 加载状态 */} - {isLoadingExercises && ( -
      -
      加载中...
      -
      - )} - - {/* Exercise Cards */} - {!isLoadingExercises && ( -
      - - {filteredExercises.map((exercise, index) => ( - - startSession(exercise)} - /> - - ))} - -
      - )} -
      - )} - - {/* Progress View */} - {currentView === "progress" && ( - -

      - 我的进度 -

      - - {/* Energy Ring Section */} -
      -
      - - -
      -
      - - {/* Stats Summary */} -
      - - - {progressSummary?.total_sessions || 0} - - 训练次数 - - - - {progressSummary?.current_streak || 0} - - 连续天数 - - - - {Math.round(progressSummary?.total_minutes || 0)} - - 训练分钟 - -
      - - {/* Achievements */} -
      -
      -

      - 成就勋章 -

      - a.is_earned).length} - total={achievements.length} - /> -
      -
      - {achievements.map((achievement) => ( - - ))} -
      -
      -
      - )} - - {/* Session View */} - {currentView === "session" && ( - -
      -

      - {selectedExercise?.name || "训练中"} -

      - -
      - -
      - {/* Video Area */} -
      -
      -
      -
      - - {/* Info Panel */} -
      - {/* Progress */} - - - {/* Feedback */} - - - {/* Controls */} -
      - {sessionState === "preparing" && ( - - )} - {sessionState === "exercising" && ( - - )} - {sessionState === "paused" && ( - - )} -
      -
      -
      -
      - )} -
      -
      - - {/* Session Complete Modal */} - - {showModal && sessionSummary && ( - - -
      🎉
      -

      - 做得很棒! -

      - -
      -
      - - {Math.round(sessionSummary.average_score || 0)} - - 平均得分 -
      -
      - - {sessionSummary.completed_reps || 0} - - 完成次数 -
      -
      - - {formatDuration(sessionSummary.session_duration || 0)} - - 训练时长 -
      -
      - - {sessionSummary.new_achievements && - sessionSummary.new_achievements.length > 0 && ( -
      -

      - 🏆 获得新成就! -

      -
      - {sessionSummary.new_achievements.map((a) => ( - - {achievementIcons[a.icon] || "🌟"} - - ))} -
      -
      - )} - - -
      -
      - )} -
      -
      -
      - ); -} diff --git a/frontend/app/community/admin/certifications/page.tsx b/frontend/app/community/admin/certifications/page.tsx deleted file mode 100644 index 85f1059d..00000000 --- a/frontend/app/community/admin/certifications/page.tsx +++ /dev/null @@ -1,556 +0,0 @@ -"use client"; - -// frontend/app/community/admin/certifications/page.tsx -/** - * 管理员认证审核页面 - * 查看和审核用户的专业认证申请 - */ - -import { useState, useEffect, useRef } from "react"; -import Link from "next/link"; -import { motion, AnimatePresence } from "framer-motion"; -import apiClient from "../../../../lib/apiClient"; -import CommunityBackground from "../../../../components/community/CommunityBackground"; -import { AuthGuard } from "../../../../components/AuthGuard"; - -const COMMUNITY_API = "/api/v1/community"; - -// Certification types -const certificationTypeNames: Record = { - certified_doctor: "医生", - certified_therapist: "康复师", - certified_nurse: "护士", -}; - -// Status config -const statusConfig: Record< - string, - { label: string; color: string; bgColor: string } -> = { - pending: { - label: "待审核", - color: "text-amber-700", - bgColor: "bg-amber-100", - }, - approved: { - label: "已通过", - color: "text-emerald-700", - bgColor: "bg-emerald-100", - }, - rejected: { label: "已拒绝", color: "text-red-700", bgColor: "bg-red-100" }, - expired: { - label: "已过期", - color: "text-stone-700", - bgColor: "bg-stone-100", - }, - revoked: { - label: "已撤销", - color: "text-orange-700", - bgColor: "bg-orange-100", - }, -}; - -interface CertificationItem { - id: string; - user_id: string; - user_nickname: string; - certification_type: string; - real_name: string; - license_number: string; - hospital_or_institution: string; - status: string; - created_at: string; -} - -interface PaginatedResponse { - items: CertificationItem[]; - total: number; - page: number; - page_size: number; - total_pages: number; -} - -function AdminCertificationsContent() { - const [certifications, setCertifications] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [statusFilter, setStatusFilter] = useState("pending"); - const [page, setPage] = useState(1); - const [totalPages, setTotalPages] = useState(1); - const [selectedCert, setSelectedCert] = useState( - null, - ); - const [reviewComment, setReviewComment] = useState(""); - const [isReviewing, setIsReviewing] = useState(false); - const [revokeTarget, setRevokeTarget] = useState( - null, - ); - const [revokeReason, setRevokeReason] = useState(""); - const [message, setMessage] = useState<{ - show: boolean; - success: boolean; - text: string; - }>({ - show: false, - success: true, - text: "", - }); - const hasFetched = useRef(false); - - const fetchCertifications = async () => { - setIsLoading(true); - try { - const params: Record = { page, page_size: 20 }; - if (statusFilter) { - params.status = statusFilter; - } - const response = await apiClient.get( - `${COMMUNITY_API}/certifications/`, - { params }, - ); - setCertifications(response.data.items); - setTotalPages(response.data.total_pages); - setError(null); - } catch (err: unknown) { - console.error("Failed to load certifications:", err); - if (err && typeof err === "object" && "response" in err) { - const axiosErr = err as { response?: { status?: number } }; - if (axiosErr.response?.status === 403) { - setError("无权限访问,仅管理员可用"); - } else { - setError("加载失败,请刷新重试"); - } - } else { - setError("加载失败,请刷新重试"); - } - } finally { - setIsLoading(false); - } - }; - - useEffect(() => { - if (hasFetched.current) return; - hasFetched.current = true; - fetchCertifications(); - }, []); - - useEffect(() => { - if (hasFetched.current) { - fetchCertifications(); - } - }, [statusFilter, page]); - - const showMessage = (success: boolean, text: string) => { - setMessage({ show: true, success, text }); - setTimeout( - () => setMessage({ show: false, success: true, text: "" }), - 3000, - ); - }; - - const handleReview = async ( - certId: string, - status: "approved" | "rejected", - ) => { - setIsReviewing(true); - try { - await apiClient.put(`${COMMUNITY_API}/certifications/${certId}/review`, { - status, - review_comment: reviewComment.trim() || null, - }); - showMessage(true, status === "approved" ? "已通过认证" : "已拒绝认证"); - setSelectedCert(null); - setReviewComment(""); - await fetchCertifications(); - } catch (err: unknown) { - console.error("Failed to review certification:", err); - let errorMessage = "审核失败,请重试"; - if (err && typeof err === "object" && "response" in err) { - const axiosErr = err as { response?: { data?: { detail?: string } } }; - if (axiosErr.response?.data?.detail) { - errorMessage = axiosErr.response.data.detail; - } - } - showMessage(false, errorMessage); - } finally { - setIsReviewing(false); - } - }; - - const handleRevoke = async (certId: string) => { - setIsReviewing(true); - try { - await apiClient.put( - `${COMMUNITY_API}/certifications/${certId}/revoke`, - null, - { - params: { reason: revokeReason.trim() || null }, - }, - ); - showMessage(true, "已撤销认证"); - setRevokeTarget(null); - setRevokeReason(""); - await fetchCertifications(); - } catch (err: unknown) { - console.error("Failed to revoke certification:", err); - let errorMessage = "撤销失败,请重试"; - if (err && typeof err === "object" && "response" in err) { - const axiosErr = err as { response?: { data?: { detail?: string } } }; - if (axiosErr.response?.data?.detail) { - errorMessage = axiosErr.response.data.detail; - } - } - showMessage(false, errorMessage); - } finally { - setIsReviewing(false); - } - }; - - return ( -
      - - - {/* Message toast */} - - {message.show && ( - - {message.text} - - )} - - - {/* Review Modal */} - - {selectedCert && ( - setSelectedCert(null)} - > - e.stopPropagation()} - > -

      - 审核认证申请 -

      - -
      -
      - 用户昵称: - - {selectedCert.user_nickname} - -
      -
      - 认证类型: - - {certificationTypeNames[selectedCert.certification_type] || - selectedCert.certification_type} - -
      -
      - 真实姓名: - - {selectedCert.real_name} - -
      -
      - 执业证号: - - {selectedCert.license_number} - -
      -
      - 医院/机构: - - {selectedCert.hospital_or_institution} - -
      -
      - -
      - -